diff --git a/packages/desktop/build/bin/kanban b/packages/desktop/build/bin/kanban index c393f9631..bb79281f2 100755 --- a/packages/desktop/build/bin/kanban +++ b/packages/desktop/build/bin/kanban @@ -14,6 +14,18 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" RESOURCES_DIR="$(dirname "$SCRIPT_DIR")" CLI_ENTRY="$RESOURCES_DIR/app.asar.unpacked/cli/cli.js" +# Runtime-update override. The desktop orchestrator pre-validates the +# path; we fail loudly on a missing file rather than silently falling +# back, so the parent's rollback bookkeeping stays in sync with what +# actually ran. +if [ -n "$KANBAN_CLI_OVERRIDE" ]; then + if [ ! -f "$KANBAN_CLI_OVERRIDE" ]; then + echo "error: KANBAN_CLI_OVERRIDE points to missing file: $KANBAN_CLI_OVERRIDE" >&2 + exit 1 + fi + CLI_ENTRY="$KANBAN_CLI_OVERRIDE" +fi + if [ ! -f "$CLI_ENTRY" ]; then echo "error: Kanban CLI not found at $CLI_ENTRY" >&2 exit 1 diff --git a/packages/desktop/build/bin/kanban.cmd b/packages/desktop/build/bin/kanban.cmd index bdd14c7d5..b08a7b333 100755 --- a/packages/desktop/build/bin/kanban.cmd +++ b/packages/desktop/build/bin/kanban.cmd @@ -11,6 +11,17 @@ set "SCRIPT_DIR=%~dp0" set "RESOURCES_DIR=%SCRIPT_DIR%.." set "CLI_ENTRY=%RESOURCES_DIR%\app.asar.unpacked\cli\cli.js" +REM Runtime-update override (see POSIX shim for rationale): fail loud +REM on missing file rather than silently falling back to bundled. +if defined KANBAN_CLI_OVERRIDE ( + if not exist "%KANBAN_CLI_OVERRIDE%" ( + echo error: KANBAN_CLI_OVERRIDE points to missing file: %KANBAN_CLI_OVERRIDE% >&2 + endlocal + exit /b 1 + ) + set "CLI_ENTRY=%KANBAN_CLI_OVERRIDE%" +) + REM Windows packaged layout: REM Kanban\resources\bin\kanban.cmd (this file) REM RESOURCES_DIR = Kanban\resources diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json index 6a66ae095..85fa3641d 100644 --- a/packages/desktop/package-lock.json +++ b/packages/desktop/package-lock.json @@ -9,11 +9,15 @@ "version": "0.0.1", "hasInstallScript": true, "dependencies": { - "node-pty": "^1.2.0-beta.11" + "node-pty": "^1.2.0-beta.11", + "pacote": "^21.5.0", + "semver": "^7.6.3" }, "devDependencies": { "@electron/notarize": "^3.0.0", "@types/node": "^22.10.5", + "@types/pacote": "^11.1.8", + "@types/semver": "^7.5.8", "electron": "^41.5.0", "electron-builder": "^26.8.1", "esbuild": "^0.27.0", @@ -165,6 +169,16 @@ "global-agent": "^3.0.0" } }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@electron/notarize": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz", @@ -310,9 +324,9 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "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": { @@ -386,9 +400,9 @@ } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", "optional": true, @@ -905,11 +919,19 @@ "node": ">=18" } }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -918,6 +940,15 @@ "node": ">=18.0.0" } }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1060,6 +1091,264 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/package-json/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@npmcli/package-json/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@oxc-project/types": { "version": "0.124.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", @@ -1163,6 +1452,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1180,6 +1472,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1197,6 +1492,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1214,6 +1512,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1231,6 +1532,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1248,6 +1552,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1334,54 +1641,360 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" + "node_modules/@sigstore/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.0.tgz", + "integrity": "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==", + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "license": "Apache-2.0", "dependencies": { - "defer-to-connect": "^2.0.0" + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" }, "engines": { - "node": ">=10" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@sigstore/sign/node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", "dependencies": { - "tslib": "^2.4.0" + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "node_modules/@sigstore/sign/node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/sign/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sigstore/sign/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sigstore/sign/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@sigstore/sign/node_modules/make-fetch-happen": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", + "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/sign/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@sigstore/sign/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@sigstore/sign/node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/@sigstore/sign/node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sigstore/sign/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sigstore/sign/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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sigstore/sign/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sigstore/sign/node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", + "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "license": "MIT", "dependencies": { @@ -1470,6 +2083,61 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/npm-package-arg": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/npm-package-arg/-/npm-package-arg-6.1.4.tgz", + "integrity": "sha512-vDgdbMy2QXHnAruzlv68pUtXCjmqUk3WrBAsRboRovsOmxbfn/WiYCjmecyKjGztnMps5dWp4Uq2prp+Ilo17Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/npm-registry-fetch": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/@types/npm-registry-fetch/-/npm-registry-fetch-8.0.9.tgz", + "integrity": "sha512-7NxvodR5Yrop3pb6+n8jhJNyzwOX0+6F+iagNEoi9u1CGxruYAwZD8pvGc9prIkL0+FdX5Xp0p80J9QPrGUp/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/node-fetch": "*", + "@types/npm-package-arg": "*", + "@types/npmlog": "*", + "@types/ssri": "*" + } + }, + "node_modules/@types/npmlog": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/npmlog/-/npmlog-7.0.0.tgz", + "integrity": "sha512-hJWbrKFvxKyWwSUXjZMYTINsSOY6IclhvGOZ97M8ac2tmR9hMwmTnYaMdpGhvju9ctWLTPhCS+eLfQNluiEjQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pacote": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/@types/pacote/-/pacote-11.1.8.tgz", + "integrity": "sha512-/XLR0VoTh2JEO0jJg1q/e6Rh9bxjBq9vorJuQmtT7rRrXSiWz7e7NsvXVYJQ0i8JxMlBMPPYDTnrRe7MZRFA8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/npm-registry-fetch": "*", + "@types/npmlog": "*", + "@types/ssri": "*" + } + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -1492,6 +2160,23 @@ "@types/node": "*" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ssri": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.5.tgz", + "integrity": "sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -1645,7 +2330,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -1655,7 +2339,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -1928,19 +2611,6 @@ "node": ">= 10.0.0" } }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2018,7 +2688,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -2058,7 +2727,6 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2274,7 +2942,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -2479,7 +3146,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3009,7 +3675,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3281,7 +3946,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/extract-zip": { @@ -3634,26 +4298,12 @@ "node": ">=10.0" } }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", + "license": "MIT", "optional": true, "dependencies": { "define-properties": "^1.2.1", @@ -3709,7 +4359,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -3795,14 +4444,12 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, "license": "BSD-2-Clause" }, "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", @@ -3830,7 +4477,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -3893,6 +4539,18 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3912,6 +4570,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -3922,6 +4589,15 @@ "node": ">= 0.10" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -4030,9 +4706,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -4059,6 +4735,15 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4097,6 +4782,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4257,6 +4951,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4278,6 +4975,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4299,6 +4999,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4320,6 +5023,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4513,7 +5219,6 @@ "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -4536,20 +5241,45 @@ } }, "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -4558,6 +5288,15 @@ "node": ">= 18" } }, + "node_modules/minizlib/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -4576,7 +5315,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4606,9 +5344,9 @@ "license": "MIT" }, "node_modules/node-abi": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.29.0.tgz", - "integrity": "sha512-bGc7hHz6lrdpMqH3XqfiHc5PKzEhjgUj6OLpTXynkLi9JZKyMByI/tdpm4Liu6O2BjtE1lakBWXjOQS1EnSQLQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", "dev": true, "license": "MIT", "dependencies": { @@ -4618,19 +5356,6 @@ "node": ">=22.12.0" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", @@ -4649,24 +5374,10 @@ "semver": "^7.3.5" } }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-gyp": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", - "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", @@ -4691,30 +5402,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=20" } }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-gyp/node_modules/which": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^4.0.0" @@ -4746,7 +5442,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, "license": "ISC", "dependencies": { "abbrev": "^4.0.0" @@ -4771,6 +5466,332 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm-registry-fetch/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", + "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-registry-fetch/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm-registry-fetch/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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-registry-fetch/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm-registry-fetch/node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -4816,50 +5837,213 @@ ], "license": "MIT" }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.5.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.0.tgz", + "integrity": "sha512-VtZ0SB8mb5Tzw3dXDfVAIjhyVKUHZkS/ZH9/5mpKenwC9sFOXNI0JI7kEF7IMkwOnsWMFrvAZHzx1T5fmrp9FQ==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pacote/node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pacote/node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pacote/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pacote/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pacote/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pacote/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "license": "ISC", "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", + "minipass": "^7.0.3" + }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, + "node_modules/pacote/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==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", + "node_modules/pacote/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", "dependencies": { - "yocto-queue": "^0.1.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pacote/node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/path-is-absolute": { @@ -5016,7 +6200,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -5325,7 +6508,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sanitize-filename": { @@ -5349,13 +6532,15 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-compare": { @@ -5456,6 +6641,23 @@ "dev": true, "license": "ISC" }, + "node_modules/sigstore": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", + "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -5469,19 +6671,6 @@ "node": ">=10" } }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -5502,14 +6691,26 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5541,6 +6742,28 @@ "source-map": "^0.6.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==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "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==", + "license": "CC0-1.0" + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -5651,10 +6874,9 @@ } }, "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -5667,11 +6889,19 @@ "node": ">=18" } }, + "node_modules/tar/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -5782,7 +7012,6 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -5799,7 +7028,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -5817,7 +7045,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5887,6 +7114,230 @@ "license": "0BSD", "optional": true }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tuf-js/node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tuf-js/node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tuf-js/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tuf-js/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/tuf-js/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/tuf-js/node_modules/make-fetch-happen": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", + "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tuf-js/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tuf-js/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tuf-js/node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/tuf-js/node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tuf-js/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/tuf-js/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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tuf-js/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tuf-js/node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -5919,7 +7370,6 @@ "version": "6.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18.17" @@ -5959,6 +7409,15 @@ "dev": true, "license": "(WTFPL OR MIT)" }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -6261,7 +7720,6 @@ "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/yargs": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 479fea2c7..4fd445523 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -23,6 +23,8 @@ "devDependencies": { "@electron/notarize": "^3.0.0", "@types/node": "^22.10.5", + "@types/pacote": "^11.1.8", + "@types/semver": "^7.5.8", "electron": "^41.5.0", "electron-builder": "^26.8.1", "esbuild": "^0.27.0", @@ -31,6 +33,8 @@ "vitest": "^4.1.0" }, "dependencies": { - "node-pty": "^1.2.0-beta.11" + "node-pty": "^1.2.0-beta.11", + "pacote": "^21.5.0", + "semver": "^7.6.3" } } diff --git a/packages/desktop/scripts/stage-cli.mjs b/packages/desktop/scripts/stage-cli.mjs index e482e269b..969e38cb6 100644 --- a/packages/desktop/scripts/stage-cli.mjs +++ b/packages/desktop/scripts/stage-cli.mjs @@ -5,7 +5,7 @@ * was skipped. */ -import { cpSync, existsSync, rmSync, writeFileSync } from "node:fs"; +import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -16,6 +16,9 @@ const distDir = resolve(repoRoot, "dist"); const webUiIndex = resolve(distDir, "web-ui/index.html"); const cliEntry = resolve(distDir, "cli.js"); const stageDir = resolve(desktopRoot, "cli"); +const runtimeVersion = JSON.parse( + readFileSync(resolve(repoRoot, "package.json"), "utf8"), +).version; function fail(message) { console.error(`\n[stage:cli] ERROR: ${message}\n`); @@ -47,9 +50,12 @@ cpSync(distDir, stageDir, { recursive: true }); // `import` statement at module top. Drop a minimal package.json next to // the staged cli.js so Node treats it as ESM regardless of what lives // further up the tree. +// Embed the runtime's version next to the staged cli.js so the desktop +// shell can read the actual bundled-runtime version at boot (separate +// from `app.getVersion()`, which returns the Electron shell version). writeFileSync( resolve(stageDir, "package.json"), - `${JSON.stringify({ type: "module" }, null, 2)}\n`, + `${JSON.stringify({ type: "module", version: runtimeVersion }, null, 2)}\n`, ); -console.log(`[stage:cli] Staged ${distDir} → ${stageDir}`); +console.log(`[stage:cli] Staged ${distDir} → ${stageDir} (runtime ${runtimeVersion})`); diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index e5d79a207..d6c70d2e2 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -9,6 +9,7 @@ import { parseProtocolUrl, registerProtocol, } from "./protocol-handler.js"; +import { createRuntimeAutoUpdate } from "./runtime-auto-update.js"; import { RuntimeOrchestrator } from "./runtime-orchestrator.js"; import { WindowFactory } from "./window-factory.js"; import { WindowRegistry } from "./window-registry.js"; @@ -34,11 +35,21 @@ let isQuitting = false; const registry = new WindowRegistry(); +const autoUpdate = createRuntimeAutoUpdate({ + isPackaged: app.isPackaged, + userData: app.getPath("userData"), + resourcesPath: process.resourcesPath, + shellVersion: app.getVersion(), + broadcast: broadcastToAllRenderers, +}); + const orchestrator = new RuntimeOrchestrator({ host: DEFAULT_HOST, port: DEFAULT_PORT, healthTimeoutMs: HEALTH_TIMEOUT_MS, resolveCliShimPath, + resolveCliEntryOverride: autoUpdate?.resolveCliEntryOverride, + onCliEntryOverrideFailed: autoUpdate?.onCliEntryOverrideFailed, }); const windowFactory = new WindowFactory({ @@ -83,6 +94,27 @@ orchestrator.on("url-changed", (url) => { }); orchestrator.on("crashed", () => windowFactory.showDisconnectedScreen()); +/** + * Fan an IPC notification out to every renderer. Update banners are + * global facts and should appear regardless of focused window. Uses + * `BrowserWindow.getAllWindows()` (not the registry) so transient + * windows like the OAuth popup are also covered. Best-effort: a + * destroyed-but-not-reaped window can throw synchronously. + */ +function broadcastToAllRenderers(channel: string, ...args: unknown[]): void { + for (const win of BrowserWindow.getAllWindows()) { + if (win.isDestroyed()) continue; + try { + win.webContents.send(channel, ...args); + } catch (err) { + console.warn( + `[desktop] IPC broadcast on ${channel} failed for one window:`, + err instanceof Error ? err.message : err, + ); + } + } +} + function handleProtocolUrl(raw: string): void { const parsed = parseProtocolUrl(raw); if (!parsed) { @@ -264,6 +296,8 @@ function wireAppLifecycle(): void { windowFactory.showDisconnectedScreen(); } + // Background runtime-update checks. Packaged-only. + autoUpdate?.scheduleChecks(); }); app.on("window-all-closed", () => { @@ -286,6 +320,11 @@ function wireAppLifecycle(): void { // kill any post-teardown spawn. event.preventDefault(); try { + // Stop the update timers before shutdown so a check can't + // fire mid-teardown. Any extract already past pacote.extract + // finishes cleanly and writes the pointer; an earlier-stage + // one gets dropped — its `.partial/` is swept next boot. + autoUpdate?.stop(); await orchestrator.shutdown(); } catch (err) { console.error( diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index 071f0b3af..9baf5d9e6 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -1,5 +1,22 @@ import { contextBridge, ipcRenderer } from "electron"; +/** + * Subscribe to a main→renderer channel and return a detach function. + * Returning detach (instead of exposing `removeListener` directly) + * prevents one renderer from removing listeners installed by another. + */ +function subscribe( + channel: string, + listener: (...args: T) => void, +): () => void { + const wrapped = (_e: Electron.IpcRendererEvent, ...args: T): void => + listener(...args); + ipcRenderer.on(channel, wrapped); + return () => { + ipcRenderer.removeListener(channel, wrapped); + }; +} + const desktopApi = { platform: process.platform, @@ -10,8 +27,20 @@ const desktopApi = { restartRuntime(): void { ipcRenderer.send("restart-runtime"); }, + + /** Fires after the background updater stages a new runtime. The + * renderer should surface a "Restart to apply " banner. */ + onUpdateStaged(listener: (version: string) => void): () => void { + return subscribe<[string]>("runtime:update-staged", listener); + }, + + /** Fires after a staged runtime failed startup and was rolled back. + * Payload is the demoted version (or `null` if unknown). */ + onRuntimeRolledBack( + listener: (demotedVersion: string | null) => void, + ): () => void { + return subscribe<[string | null]>("runtime:rolled-back", listener); + }, } as const; contextBridge.exposeInMainWorld("desktop", desktopApi); - -export type DesktopApi = typeof desktopApi; diff --git a/packages/desktop/src/runtime-auto-update.ts b/packages/desktop/src/runtime-auto-update.ts new file mode 100644 index 000000000..bd779b2f0 --- /dev/null +++ b/packages/desktop/src/runtime-auto-update.ts @@ -0,0 +1,257 @@ +/** + * Wires runtime-store + runtime-update into the orchestrator's + * `cliEntryOverride` callbacks and a 30s/30min background check + * schedule. Packaged-only — returns `null` in dev so the orchestrator + * just spawns the bundled cli. + */ + +import { readFileSync } from "node:fs"; +import path from "node:path"; + +import semver from "semver"; + +import { + cleanupPartials, + clearPointer, + markBadVersion, + pointerFileExists, + readPointer, + removeVersionDir, + resolvePointerCliEntry, + versionFromCliEntry, +} from "./runtime-store.js"; +import { checkAndStageLatestRuntime } from "./runtime-update.js"; + +const FIRST_CHECK_DELAY_MS = 30_000; +const CHECK_INTERVAL_MS = 30 * 60_000; + +export interface RuntimeAutoUpdate { + resolveCliEntryOverride: () => string | null; + /** + * `cliEntry` must be exactly the path returned by an earlier + * `resolveCliEntryOverride()` call — i.e. captured at spawn time + * by the orchestrator. We use it to roll back the version that + * actually ran, even if a concurrent background stage has since + * advanced the pointer to a newer version. + */ + onCliEntryOverrideFailed: (reason: string, cliEntry: string) => void; + scheduleChecks(): void; + stop(): void; +} + +export interface RuntimeAutoUpdateDeps { + isPackaged: boolean; + userData: string; + resourcesPath: string; + shellVersion: string; + broadcast: (channel: string, ...args: unknown[]) => void; +} + +export function createRuntimeAutoUpdate( + deps: RuntimeAutoUpdateDeps, +): RuntimeAutoUpdate | null { + if (!deps.isPackaged) return null; + + const unpacked = path.join(deps.resourcesPath, "app.asar.unpacked"); + const bundledVersion = + readBundledVersion(path.join(unpacked, "cli")) ?? deps.shellVersion; + // `node-pty` lives under `app.asar.unpacked/node_modules/` after + // `electron-builder install-app-deps` rebuilds it for this Electron. + const nativeDepsSource = path.join(unpacked, "node_modules"); + + // Sweep stale `.partial/` from interrupted extracts before any + // new staging can collide with them. + try { + cleanupPartials(deps.userData); + } catch (e) { + warn("cleanupPartials", e); + } + + let firstTimer: NodeJS.Timeout | null = null; + let interval: NodeJS.Timeout | null = null; + let inFlight = false; + + const dropPointer = (why: string): void => { + console.warn(`[desktop] ${why} — clearing pointer.`); + try { + clearPointer(deps.userData); + } catch (e) { + warn("clearPointer", e); + } + }; + + // Effective launch version is `max(pointer, bundled)`. A pointer + // at-or-below bundled is stale (e.g. user upgraded the shell while + // userData still pointed at an older staged runtime); without this + // guard we'd keep launching the older runtime forever. Also + // self-repairs pointers whose `cliEntry` no longer exists, and + // removes invalid `current.json` files (corrupt JSON, non-canonical + // path, non-absolute path) so they can't linger as renderer-visible + // state forever. + const loadOverride = (): string | null => { + const pointer = readPointer(deps.userData); + if (!pointer) { + if (pointerFileExists(deps.userData)) { + dropPointer("Invalid current.json"); + } + return null; + } + if (semver.lte(pointer.version, bundledVersion)) { + dropPointer( + `Staged ${pointer.version} <= bundled ${bundledVersion}`, + ); + return null; + } + const cli = resolvePointerCliEntry(deps.userData); + if (cli) return cli; + dropPointer("Staged cliEntry missing"); + return null; + }; + + // Rollback for the version that *actually ran* — derived from the + // captured cliEntry, not from re-reading the pointer. The orchestrator + // runs the readiness probe asynchronously after spawn; in the + // meantime, a background `runCheck()` may have completed a successful + // staging and replaced the pointer with a newer version. Rolling back + // "whatever the pointer says now" would mark/remove the *new* + // version that hasn't even been tried yet. + const onFailed = (reason: string, cliEntry: string): void => { + const failedVersion = versionFromCliEntry(deps.userData, cliEntry); + const current = readPointer(deps.userData); + const pointerStillFailed = + current !== null && current.cliEntry === cliEntry; + console.warn( + `[desktop] Staged runtime failed (${reason})${ + failedVersion ? `; rolling back ${failedVersion}` : "" + }${pointerStillFailed ? "" : " (pointer already advanced)"}.`, + ); + // `markBadVersion` is the critical step: without it the next + // `runCheck` would just re-stage the same broken version. We + // gate `clearPointer` on it so a transient write failure (disk + // full, EPERM) doesn't leave the system in a state where the + // pointer is dropped *and* the version isn't blacklisted — + // which would loop on every `runCheck` ad infinitum. With this + // gating, the user still launches successfully (via the + // orchestrator's same-launch fallback to bundled), and we + // retry `markBadVersion` on every subsequent boot until it + // succeeds. + let marked = false; + if (failedVersion) { + try { + markBadVersion(deps.userData, failedVersion); + marked = true; + } catch (e) { + warn("markBadVersion", e); + } + if (marked) { + try { + removeVersionDir(deps.userData, failedVersion); + } catch (e) { + warn("removeVersionDir", e); + } + } + } + // Two independent gates on `clearPointer`: + // - `marked`: don't drop the pointer if we couldn't blacklist + // the failed version (see comment above). + // - `pointerStillFailed`: a concurrent `runCheck` may have + // already advanced the pointer to a newer presumed-good + // version; don't clobber that. + if (marked && pointerStillFailed) { + try { + clearPointer(deps.userData); + } catch (e) { + warn("clearPointer", e); + } + } + deps.broadcast("runtime:rolled-back", failedVersion ?? null); + }; + + const runCheck = async (): Promise => { + // Single-flight: a slow extract racing the periodic interval + // would otherwise re-enter pacote.extract on the same partial. + if (inFlight) return; + inFlight = true; + try { + // Side effect: drops a stale-or-broken pointer so the + // version gate below sees an accurate `max(pointer, bundled)`. + loadOverride(); + const ptr = readPointer(deps.userData); + const currentVersion = + ptr && semver.gt(ptr.version, bundledVersion) + ? ptr.version + : bundledVersion; + const outcome = await checkAndStageLatestRuntime({ + userData: deps.userData, + currentVersion, + nativeDepsSource, + }); + if (outcome.kind === "staged") { + console.log( + `[desktop] Staged kanban@${outcome.version} — restart to apply.`, + ); + deps.broadcast("runtime:update-staged", outcome.version); + } else if (outcome.kind === "bad-version") { + console.log( + `[desktop] Skipping kanban@${outcome.version}: previously failed startup.`, + ); + } + } catch (e) { + console.warn( + "[desktop] Runtime update check failed:", + e instanceof Error ? e.message : e, + ); + } finally { + inFlight = false; + } + }; + + return { + resolveCliEntryOverride: loadOverride, + onCliEntryOverrideFailed: onFailed, + scheduleChecks() { + firstTimer = setTimeout(() => void runCheck(), FIRST_CHECK_DELAY_MS); + firstTimer.unref(); + interval = setInterval(() => void runCheck(), CHECK_INTERVAL_MS); + interval.unref(); + }, + stop() { + if (firstTimer) { + clearTimeout(firstTimer); + firstTimer = null; + } + if (interval) { + clearInterval(interval); + interval = null; + } + }, + }; +} + +/** + * Read `version` from `/package.json` and validate as semver. + * Defends against a corrupt/hand-edited `cli/package.json`: a non-string + * or non-semver `version` field would otherwise propagate into + * `bundledVersion`, and the very first `semver.lte/gt` against it (in + * `loadOverride` or `runCheck`) would throw a TypeError on the hot + * startup path. Returning `null` here lets the caller fall back to + * `shellVersion` instead. + */ +function readBundledVersion(cliDir: string): string | null { + try { + const parsed = JSON.parse( + readFileSync(path.join(cliDir, "package.json"), "utf8"), + ) as { version?: unknown }; + if (typeof parsed.version !== "string") return null; + return semver.valid(parsed.version) ? parsed.version : null; + } catch { + return null; + } +} + +function warn(label: string, err: unknown): void { + console.warn( + `[desktop] ${label} failed:`, + err instanceof Error ? err.message : err, + ); +} diff --git a/packages/desktop/src/runtime-child.ts b/packages/desktop/src/runtime-child.ts index b5149e42b..e42914b18 100644 --- a/packages/desktop/src/runtime-child.ts +++ b/packages/desktop/src/runtime-child.ts @@ -49,6 +49,9 @@ export interface RuntimeChildManagerOptions { * Node process, so generous headroom matters for multi-agent workloads. */ maxOldSpaceMb?: number; + /** Absolute path to a cli.js the shim should run instead of the bundled + * one. Forwarded via `KANBAN_CLI_OVERRIDE`. */ + cliEntryOverride?: string; spawnFn?: typeof spawn; } @@ -147,6 +150,7 @@ export class RuntimeChildManager extends EventEmitter pollIntervalMs: number; startupTimeoutMs: number; maxOldSpaceMb: number; + cliEntryOverride: string | undefined; spawnFn: typeof spawn; }; @@ -163,6 +167,7 @@ export class RuntimeChildManager extends EventEmitter pollIntervalMs: options.pollIntervalMs ?? 200, startupTimeoutMs: options.startupTimeoutMs ?? 30_000, maxOldSpaceMb: options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB, + cliEntryOverride: options.cliEntryOverride || undefined, spawnFn: options.spawnFn ?? spawn, }; } @@ -214,6 +219,9 @@ export class RuntimeChildManager extends EventEmitter const env = buildFilteredEnv(); env.KANBAN_DESKTOP = "1"; + if (this.opts.cliEntryOverride !== undefined) { + env.KANBAN_CLI_OVERRIDE = this.opts.cliEntryOverride; + } // Merge our V8 heap limit with any existing NODE_OPTIONS from parent. // Strip both hyphen and underscore variants to avoid duplicates. const existingNodeOptions = env.NODE_OPTIONS?.trim() || ""; diff --git a/packages/desktop/src/runtime-orchestrator.ts b/packages/desktop/src/runtime-orchestrator.ts index 1e021211c..0fdef673a 100644 --- a/packages/desktop/src/runtime-orchestrator.ts +++ b/packages/desktop/src/runtime-orchestrator.ts @@ -4,13 +4,21 @@ import { powerSaveBlocker } from "electron"; import { RuntimeChildManager } from "./runtime-child.js"; - interface RuntimeOrchestratorOptions { - host: string; port: number; healthTimeoutMs: number; resolveCliShimPath: () => string; + /** Re-evaluated on every spawn. `null` ⇒ use the shim's bundled cli. */ + resolveCliEntryOverride?: () => string | null; + /** Called when a staged spawn fails its readiness probe. The + * orchestrator retries once with the bundled cli on this same launch; + * the callback should clear/roll back the version it just tried. + * `cliEntry` is the exact override path used for the failed spawn — + * capturing it at spawn time avoids racing with a concurrent + * background staging that may have moved the pointer to a *new* + * version (which we must not roll back). */ + onCliEntryOverrideFailed?: (reason: string, cliEntry: string) => void; fetchImpl?: typeof fetch; attachedProbeIntervalMs?: number; attachedProbeFailureThreshold?: number; @@ -71,6 +79,15 @@ export class RuntimeOrchestrator extends EventEmitter/ — finalized runtime + * versions/.partial/ — in-flight extract; never read at boot + * + * The bundled runtime in `app.asar.unpacked/cli/` is the fallback — + * if the pointer is missing or its spawn fails, we drop the pointer + * and the shim launches bundled. + */ + +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + renameSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; + +import semver from "semver"; + +export interface RuntimePointer { + version: string; + /** Absolute path to `versions//dist/cli.js`. */ + cliEntry: string; +} + +const POINTER_FILE = "current.json"; +const BAD_VERSIONS_FILE = "bad-versions.json"; + +const root = (userData: string): string => path.join(userData, "runtime-store"); +const pointerPath = (userData: string): string => + path.join(root(userData), POINTER_FILE); +const badVersionsPath = (userData: string): string => + path.join(root(userData), BAD_VERSIONS_FILE); + +const isSemver = (v: unknown): v is string => + typeof v === "string" && semver.valid(v) !== null; + +export function versionDir(userData: string, version: string): string { + if (!isSemver(version)) { + throw new Error(`runtime-store: invalid semver: ${version}`); + } + return path.join(root(userData), "versions", version); +} + +export function partialDir(userData: string, version: string): string { + return `${versionDir(userData, version)}.partial`; +} + +export function cliEntryFor(userData: string, version: string): string { + return path.join(versionDir(userData, version), "dist", "cli.js"); +} + +/** + * Inverse of `cliEntryFor`. Walks back from a canonical cliEntry to + * its `` segment. Returns `null` unless the full path fits the + * `/runtime-store/versions//dist/cli.js` shape AND + * `` is valid semver — checked by re-deriving via `cliEntryFor` + * and string-comparing. Validating the full shape (not just the + * `` segment) means a stray path like `/tmp/1.2.3/dist/not-cli.js` + * or `/elsewhere/1.2.3/dist/cli.js` doesn't accidentally produce + * `"1.2.3"`, which would let the rollback path mark a real-but- + * unrelated version bad and remove its on-disk dir. + * + * Callers that capture the override path at spawn time use this to + * roll back the version that *actually* ran — without re-reading the + * pointer (which a concurrent background stage may have replaced). + */ +export function versionFromCliEntry( + userData: string, + cliEntry: string, +): string | null { + if (!path.isAbsolute(cliEntry)) return null; + const v = path.basename(path.dirname(path.dirname(cliEntry))); + if (!isSemver(v)) return null; + return cliEntry === cliEntryFor(userData, v) ? v : null; +} + +function atomicWrite(target: string, body: string): void { + mkdirSync(path.dirname(target), { recursive: true }); + const tmp = `${target}.${process.pid}.${Date.now()}.tmp`; + writeFileSync(tmp, body); + renameSync(tmp, target); +} + +/** + * Pointer's `cliEntry` must be the canonical path for the pointer's + * version. We pass `cliEntry` to the shim as `KANBAN_CLI_OVERRIDE`, + * so a non-canonical or out-of-tree path would let a tampered + * `current.json` execute arbitrary on-disk JS. Returns the canonical + * absolute path so callers don't have to re-resolve. + */ +export function readPointer(userData: string): RuntimePointer | null { + let parsed: unknown; + try { + parsed = JSON.parse(readFileSync(pointerPath(userData), "utf8")); + } catch { + return null; + } + if (!parsed || typeof parsed !== "object") return null; + const { version, cliEntry } = parsed as Record; + if (!isSemver(version)) return null; + if (typeof cliEntry !== "string" || cliEntry.length === 0) return null; + // Require absolute paths only — the on-disk contract is "absolute + // canonical path under runtime-store/". Accepting a relative form + // here would make pointer validity depend on `process.cwd()` at the + // moment of read, which is cwd-dependent footgun for no benefit. + if (!path.isAbsolute(cliEntry)) return null; + if (cliEntry !== cliEntryFor(userData, version)) return null; + return { version, cliEntry }; +} + +/** + * Whether a `current.json` exists on disk regardless of whether it + * parses/validates. Used by callers that need to clean up an invalid + * pointer file (e.g. tampered or hand-edited) — `readPointer()` returning + * null doesn't tell them apart from "no pointer at all". + */ +export function pointerFileExists(userData: string): boolean { + try { + return statSync(pointerPath(userData)).isFile(); + } catch { + return false; + } +} + +export function writePointer(userData: string, p: RuntimePointer): void { + if (!isSemver(p.version)) { + throw new Error(`runtime-store: invalid semver: ${p.version}`); + } + // Symmetric with `readPointer`'s absolute-path contract — a relative + // `cliEntry` that happens to resolve to the canonical path from the + // caller's `process.cwd()` would round-trip through writer + reader + // today, but pointer validity must not depend on cwd at *either* + // boundary. Require absolute input here so the on-disk contract + // ("`cliEntry` is the canonical absolute path") is enforced uniformly. + const canonical = cliEntryFor(userData, p.version); + if (!path.isAbsolute(p.cliEntry) || p.cliEntry !== canonical) { + throw new Error( + `runtime-store: cliEntry for ${p.version} must be ${canonical}, got ${p.cliEntry}`, + ); + } + atomicWrite( + pointerPath(userData), + `${JSON.stringify({ version: p.version, cliEntry: canonical })}\n`, + ); +} + +export function clearPointer(userData: string): void { + rmSync(pointerPath(userData), { force: true }); +} + +/** Pointer's cliEntry iff the file exists on disk. */ +export function resolvePointerCliEntry(userData: string): string | null { + const p = readPointer(userData); + if (!p) return null; + try { + return statSync(p.cliEntry).isFile() ? p.cliEntry : null; + } catch { + return null; + } +} + +/** Sweep `.partial/` left over from interrupted extracts. Best-effort. */ +export function cleanupPartials(userData: string): void { + const versions = path.join(root(userData), "versions"); + if (!existsSync(versions)) return; + for (const e of readdirSync(versions, { withFileTypes: true })) { + if (e.isDirectory() && e.name.endsWith(".partial")) { + rmSync(path.join(versions, e.name), { recursive: true, force: true }); + } + } +} + +export function removeVersionDir(userData: string, version: string): void { + if (!isSemver(version)) return; + rmSync(versionDir(userData, version), { recursive: true, force: true }); +} + +// ----------------------------------------------------------------- +// Bad-version markers — stop the updater from re-staging a version +// that already failed startup. Entries are never pruned; the registry +// only publishes monotonically increasing versions and we only ever +// check `isBadVersion(latest)`, so old entries are dead weight (a few +// bytes) but never re-examined. If the file ever needs trimming, do +// it lazily here against an `effectiveCurrentVersion` argument. +// ----------------------------------------------------------------- + +function readBadVersions(userData: string): string[] { + try { + const parsed = JSON.parse(readFileSync(badVersionsPath(userData), "utf8")); + return Array.isArray(parsed) ? parsed.filter(isSemver) : []; + } catch { + return []; + } +} + +export function isBadVersion(userData: string, version: string): boolean { + return isSemver(version) && readBadVersions(userData).includes(version); +} + +export function markBadVersion(userData: string, version: string): void { + if (!isSemver(version)) { + throw new Error(`runtime-store: invalid semver: ${version}`); + } + const set = new Set(readBadVersions(userData)); + set.add(version); + atomicWrite( + badVersionsPath(userData), + `${JSON.stringify([...set].sort(semver.compare))}\n`, + ); +} diff --git a/packages/desktop/src/runtime-update.ts b/packages/desktop/src/runtime-update.ts new file mode 100644 index 000000000..a96644a6a --- /dev/null +++ b/packages/desktop/src/runtime-update.ts @@ -0,0 +1,108 @@ +/** + * Stage the latest published `kanban` runtime under userData so the + * installed shell can run a newer runtime than it was packaged with — + * without requiring a shell reinstall. + * + * Failures before the pointer write leave the existing pointer + * untouched. The bundled runtime under `app.asar.unpacked/cli/` + * remains the fallback. + */ + +import { existsSync } from "node:fs"; +import { cp, mkdir, rename, rm } from "node:fs/promises"; +import path from "node:path"; + +import pacote from "pacote"; +import semver from "semver"; + +import { + cleanupPartials, + cliEntryFor, + isBadVersion, + partialDir, + resolvePointerCliEntry, + versionDir, + versionFromCliEntry, + writePointer, +} from "./runtime-store.js"; + +const PACKAGE = "kanban"; + +export interface CheckOptions { + userData: string; + /** Version we'd launch right now (pointer or bundled). */ + currentVersion: string; + /** `app.asar.unpacked/node_modules/` — source for bundled `node-pty`. */ + nativeDepsSource: string; +} + +export type StageOutcome = + | { kind: "staged"; version: string } + | { kind: "up-to-date" } + | { kind: "already-staged" } + | { kind: "bad-version"; version: string }; + +export async function checkAndStageLatestRuntime( + opts: CheckOptions, +): Promise { + const manifest = await pacote.manifest(`${PACKAGE}@latest`); + const latest = manifest.version; + if (!semver.valid(latest)) { + throw new Error(`runtime-update: registry returned non-semver: ${latest}`); + } + + // `currentVersion` may come from a stale/invalid pointer — defend + // against semver.gt throwing on garbage input. + if ( + semver.valid(opts.currentVersion) && + !semver.gt(latest, opts.currentVersion) + ) { + return { kind: "up-to-date" }; + } + if (isBadVersion(opts.userData, latest)) { + return { kind: "bad-version", version: latest }; + } + // `already-staged` requires both a pointer at `latest` AND its + // `cliEntry` actually present on disk. Without the file-exists + // check this gate would silently lie when the version dir was + // wiped (corrupt userData, manual cleanup, partial uninstall), + // leaving the user's runtime in a state where loadOverride keeps + // returning null *and* the updater keeps short-circuiting on + // "already-staged" forever. + const stagedCli = resolvePointerCliEntry(opts.userData); + if (stagedCli && versionFromCliEntry(opts.userData, stagedCli) === latest) { + return { kind: "already-staged" }; + } + + cleanupPartials(opts.userData); + const stage = partialDir(opts.userData, latest); + await rm(stage, { recursive: true, force: true }); + await mkdir(path.dirname(stage), { recursive: true }); + await pacote.extract(`${PACKAGE}@${latest}`, stage); + + // `node-pty` is the sole external in `kanban`'s esbuild build + // (see scripts/build.mjs). pacote.extract doesn't install deps, + // so reuse the desktop's bundled prebuilt — already ABI-matched + // to this Electron, no `npm` required at runtime. + const ptySrc = path.join(opts.nativeDepsSource, "node-pty"); + if (!existsSync(ptySrc)) { + throw new Error(`runtime-update: bundled node-pty missing at ${ptySrc}`); + } + await cp(ptySrc, path.join(stage, "node_modules", "node-pty"), { + recursive: true, + dereference: true, + }); + + if (!existsSync(path.join(stage, "dist", "cli.js"))) { + throw new Error("runtime-update: extracted package missing dist/cli.js"); + } + + const finalDir = versionDir(opts.userData, latest); + await rm(finalDir, { recursive: true, force: true }); + await rename(stage, finalDir); + writePointer(opts.userData, { + version: latest, + cliEntry: cliEntryFor(opts.userData, latest), + }); + return { kind: "staged", version: latest }; +} diff --git a/packages/desktop/test/runtime-auto-update.test.ts b/packages/desktop/test/runtime-auto-update.test.ts new file mode 100644 index 000000000..b15e930b9 --- /dev/null +++ b/packages/desktop/test/runtime-auto-update.test.ts @@ -0,0 +1,426 @@ +/** + * Direct tests for `createRuntimeAutoUpdate()` — the wiring layer + * between the orchestrator's cliEntryOverride callbacks and the + * runtime-store/runtime-update modules. + * + * `checkAndStageLatestRuntime` is mocked via `vi.mock()` because the + * real implementation hits the npm registry. Everything else (the + * pointer/bad-versions store, the file-existence check, the rollback + * sequence) runs against a real userData tmpdir so the on-disk + * invariants are exercised. + */ + +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; + +import { + cliEntryFor, + isBadVersion, + readPointer, + versionDir, + writePointer, +} from "../src/runtime-store.js"; +import { checkAndStageLatestRuntime } from "../src/runtime-update.js"; +import { createRuntimeAutoUpdate } from "../src/runtime-auto-update.js"; + +vi.mock("../src/runtime-update.js", () => ({ + checkAndStageLatestRuntime: vi.fn(), +})); + +const stagedManifest = checkAndStageLatestRuntime as unknown as ReturnType< + typeof vi.fn +>; + +let userData: string; +let resourcesPath: string; +// `broadcast` matches `RuntimeAutoUpdateDeps.broadcast`; vi.fn() with no +// arg type infers to `Mock`, which TS won't +// assign to a fixed signature without an explicit cast. +type Broadcast = (channel: string, ...args: unknown[]) => void; +let broadcast: Broadcast & { mock: ReturnType["mock"] }; + +const SHELL_VERSION = "0.1.70"; +const BUNDLED_VERSION = "0.1.70"; + +function setBundled(version: string): void { + const cliDir = path.join(resourcesPath, "app.asar.unpacked", "cli"); + mkdirSync(cliDir, { recursive: true }); + writeFileSync( + path.join(cliDir, "package.json"), + JSON.stringify({ name: "kanban", version }), + ); +} + +/** Lay out `versions//dist/cli.js` so a pointer to it is valid. */ +function stageVersion(version: string): string { + const cliEntry = cliEntryFor(userData, version); + mkdirSync(path.dirname(cliEntry), { recursive: true }); + writeFileSync(cliEntry, "// runtime"); + return cliEntry; +} + +beforeEach(() => { + userData = mkdtempSync(path.join(tmpdir(), "auto-update-userData-")); + resourcesPath = mkdtempSync(path.join(tmpdir(), "auto-update-resources-")); + setBundled(BUNDLED_VERSION); + broadcast = vi.fn() as unknown as typeof broadcast; + stagedManifest.mockReset(); + vi.spyOn(console, "warn").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => {}); +}); + +afterEach(() => { + rmSync(userData, { recursive: true, force: true }); + rmSync(resourcesPath, { recursive: true, force: true }); + vi.restoreAllMocks(); +}); + +function buildAutoUpdate(overrides: { + isPackaged?: boolean; + shellVersion?: string; +} = {}) { + return createRuntimeAutoUpdate({ + isPackaged: overrides.isPackaged ?? true, + userData, + resourcesPath, + shellVersion: overrides.shellVersion ?? SHELL_VERSION, + broadcast, + }); +} + +describe("createRuntimeAutoUpdate", () => { + it("returns null when not packaged (dev runs the bundled cli)", () => { + expect(buildAutoUpdate({ isPackaged: false })).toBeNull(); + }); +}); + +describe("createRuntimeAutoUpdate: resolveCliEntryOverride", () => { + it("returns null and leaves the store untouched when no pointer exists", () => { + const auto = buildAutoUpdate(); + expect(auto?.resolveCliEntryOverride()).toBeNull(); + expect(broadcast).not.toHaveBeenCalled(); + }); + + it("returns the staged cliEntry when pointer.version > bundled", () => { + const cliEntry = stageVersion("0.1.71"); + writePointer(userData, { version: "0.1.71", cliEntry }); + const auto = buildAutoUpdate(); + expect(auto?.resolveCliEntryOverride()).toBe(cliEntry); + // Pointer must remain on disk — this is the happy path, not a + // self-repair branch. + expect(readPointer(userData)).not.toBeNull(); + }); + + it("clears a stale pointer whose version <= bundled (older staged + newer shell)", () => { + // Regression: previously loadOverride() ignored pointer.version + // and would keep launching an older staged runtime forever after + // a shell upgrade. + const cliEntry = stageVersion("0.1.69"); + writePointer(userData, { version: "0.1.69", cliEntry }); + + const auto = buildAutoUpdate(); + expect(auto?.resolveCliEntryOverride()).toBeNull(); + expect(readPointer(userData)).toBeNull(); + }); + + it("clears a pointer whose cliEntry no longer exists on disk", () => { + const cliEntry = stageVersion("0.1.71"); + writePointer(userData, { version: "0.1.71", cliEntry }); + rmSync(versionDir(userData, "0.1.71"), { recursive: true }); + + const auto = buildAutoUpdate(); + expect(auto?.resolveCliEntryOverride()).toBeNull(); + expect(readPointer(userData)).toBeNull(); + }); + + it("clears a non-canonical pointer file (deletes it, not just rejects it)", () => { + // A tampered current.json with cliEntry pointing outside the + // runtime-store should never be honored — and the bad file + // itself must be removed, otherwise it lingers as visible state + // forever even though every loadOverride() call ignores it. + const pointerFile = path.join( + userData, + "runtime-store", + "current.json", + ); + mkdirSync(path.dirname(pointerFile), { recursive: true }); + writeFileSync( + pointerFile, + JSON.stringify({ version: "0.1.71", cliEntry: "/etc/passwd" }), + ); + + const auto = buildAutoUpdate(); + expect(auto?.resolveCliEntryOverride()).toBeNull(); + expect(existsSync(pointerFile)).toBe(false); + }); + + it("clears a corrupt-JSON pointer file", () => { + const pointerFile = path.join( + userData, + "runtime-store", + "current.json", + ); + mkdirSync(path.dirname(pointerFile), { recursive: true }); + writeFileSync(pointerFile, "{not-json"); + + const auto = buildAutoUpdate(); + expect(auto?.resolveCliEntryOverride()).toBeNull(); + expect(existsSync(pointerFile)).toBe(false); + }); + + it("falls back to shellVersion as bundled when app.asar.unpacked/cli/package.json is missing", () => { + rmSync(path.join(resourcesPath, "app.asar.unpacked"), { + recursive: true, + force: true, + }); + const cliEntry = stageVersion("0.1.71"); + writePointer(userData, { version: "0.1.71", cliEntry }); + + const auto = buildAutoUpdate({ shellVersion: "0.1.71" }); + // shellVersion 0.1.71 == pointer 0.1.71 → still <= bundled, so cleared. + expect(auto?.resolveCliEntryOverride()).toBeNull(); + }); + + it("falls back to shellVersion when cli/package.json has a non-semver version (no TypeError on hot path)", () => { + // Regression: `readBundledVersion` previously returned any string + // `version` field unchecked, so a corrupt/hand-edited + // `cli/package.json` (e.g. truncated mid-write, or a packaging + // bug producing a placeholder) would propagate "abc" into + // `bundledVersion`. The very first `semver.lte/gt` against it + // would then throw TypeError on the hot startup path. We now + // validate-and-fall-back to `shellVersion`, so this is safe. + const cliDir = path.join(resourcesPath, "app.asar.unpacked", "cli"); + mkdirSync(cliDir, { recursive: true }); + writeFileSync( + path.join(cliDir, "package.json"), + JSON.stringify({ name: "kanban", version: "abc" }), + ); + const cliEntry = stageVersion("0.1.71"); + writePointer(userData, { version: "0.1.71", cliEntry }); + + // shellVersion 0.1.71 == pointer 0.1.71 → cleared, no TypeError. + const auto = buildAutoUpdate({ shellVersion: "0.1.71" }); + expect(() => auto?.resolveCliEntryOverride()).not.toThrow(); + expect(auto?.resolveCliEntryOverride()).toBeNull(); + }); + + it("falls back to shellVersion when cli/package.json's version is not a string", () => { + const cliDir = path.join(resourcesPath, "app.asar.unpacked", "cli"); + mkdirSync(cliDir, { recursive: true }); + writeFileSync( + path.join(cliDir, "package.json"), + JSON.stringify({ name: "kanban", version: 42 }), + ); + const cliEntry = stageVersion("0.1.72"); + writePointer(userData, { version: "0.1.72", cliEntry }); + + // shellVersion 0.1.71 < pointer 0.1.72 → returns the staged cli. + const auto = buildAutoUpdate({ shellVersion: "0.1.71" }); + expect(auto?.resolveCliEntryOverride()).toBe(cliEntry); + }); +}); + +describe("createRuntimeAutoUpdate: onCliEntryOverrideFailed (rollback)", () => { + it("marks the failed version bad, removes its dir, clears pointer, and broadcasts", () => { + const cliEntry = stageVersion("0.1.71"); + writePointer(userData, { version: "0.1.71", cliEntry }); + + const auto = buildAutoUpdate(); + auto?.onCliEntryOverrideFailed("spawn ENOENT", cliEntry); + + expect(isBadVersion(userData, "0.1.71")).toBe(true); + expect(readPointer(userData)).toBeNull(); + // Version dir is removed so a future staging of the same version + // can extract cleanly without a leftover-files conflict. + expect(() => readFileSync(cliEntry, "utf8")).toThrow(); + expect(broadcast).toHaveBeenCalledWith("runtime:rolled-back", "0.1.71"); + }); + + it("does NOT clear the pointer when a concurrent stage already advanced it", () => { + // Race: orchestrator spawned the staged 0.1.71 cli; while its + // readiness probe is still running, runCheck() finishes staging + // 0.1.72 and writes the pointer. The probe then fails. Rolling + // back "whatever the pointer says now" would mark/remove 0.1.72 + // — a version we haven't even tried yet. The captured failed + // cliEntry is the source of truth. + const failedCli = stageVersion("0.1.71"); + const newerCli = stageVersion("0.1.72"); + writePointer(userData, { version: "0.1.72", cliEntry: newerCli }); + + const auto = buildAutoUpdate(); + auto?.onCliEntryOverrideFailed("spawn ENOENT", failedCli); + + // The failed (older) version is rolled back... + expect(isBadVersion(userData, "0.1.71")).toBe(true); + expect(existsSync(versionDir(userData, "0.1.71"))).toBe(false); + // ...but the newer pointer is left intact. + expect(readPointer(userData)?.version).toBe("0.1.72"); + expect(isBadVersion(userData, "0.1.72")).toBe(false); + expect(existsSync(versionDir(userData, "0.1.72"))).toBe(true); + // Broadcast still names the version that *failed*, not the one in pointer. + expect(broadcast).toHaveBeenCalledWith("runtime:rolled-back", "0.1.71"); + }); + + it("broadcasts rollback with null when cliEntry doesn't fit the canonical layout", () => { + // Defensive: the orchestrator should only ever pass cliEntry + // values that came from `resolveCliEntryOverride()`, but if + // something exotic gets through, we still want the renderer to + // hear *some* rollback signal rather than silently swallow. + const auto = buildAutoUpdate(); + auto?.onCliEntryOverrideFailed("bundled spawn failed", "/exotic/path"); + expect(broadcast).toHaveBeenCalledWith("runtime:rolled-back", null); + }); + + it("does NOT clear the pointer when markBadVersion fails (avoid re-stage loop)", () => { + // Regression: `clearPointer` previously fired unconditionally after + // `markBadVersion`'s try/catch. If `markBadVersion` throws + // (disk full, EPERM on bad-versions.json), the pointer would + // still be dropped — and since the version isn't blacklisted, + // the next `runCheck()` would re-extract the same broken + // version, write the pointer again, and crash on the next + // launch. Loop forever until disk frees. + // + // Fix: gate `clearPointer` on `markBadVersion` succeeding. The + // user still launches successfully via the orchestrator's + // same-launch retry to bundled; the pointer just stays in place + // so the failure keeps retrying `markBadVersion` rather than + // looping `runCheck → re-extract → fail`. + const cliEntry = stageVersion("0.1.71"); + writePointer(userData, { version: "0.1.71", cliEntry }); + + // Force the bad-versions write to fail by replacing its parent + // (the runtime-store dir) with a regular file. Atomic-write's + // mkdirSync(recursive) → writeFileSync → renameSync chain will + // trip on whichever step hits the file-where-dir-is-expected. + // We create the file *after* the pointer write above, since + // that needs the dir to exist. + const badVersions = path.join( + userData, + "runtime-store", + "bad-versions.json", + ); + // Replace bad-versions.json's would-be location with a directory + // so writeFileSync at that path EISDIRs. + mkdirSync(badVersions, { recursive: true }); + + const auto = buildAutoUpdate(); + auto?.onCliEntryOverrideFailed("spawn ENOENT", cliEntry); + + // Pointer must remain — without it, a future `runCheck()` would + // re-stage the same broken version (since !isBadVersion is true). + expect(readPointer(userData)).not.toBeNull(); + expect(isBadVersion(userData, "0.1.71")).toBe(false); + // Version dir cleanup is gated on markBad too — both stay so + // the next attempt has the same starting state to retry against. + expect(existsSync(versionDir(userData, "0.1.71"))).toBe(true); + // Renderer still hears the rollback so the UI can react. + expect(broadcast).toHaveBeenCalledWith("runtime:rolled-back", "0.1.71"); + }); +}); + +describe("createRuntimeAutoUpdate: runCheck (background updater)", () => { + async function tick(): Promise { + // Yield long enough for setTimeout(0) + the awaited mock to settle. + await vi.advanceTimersByTimeAsync(30_000); + await Promise.resolve(); + } + + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("broadcasts runtime:update-staged on a successful staged outcome", async () => { + stagedManifest.mockResolvedValueOnce({ kind: "staged", version: "0.1.72" }); + const auto = buildAutoUpdate(); + auto?.scheduleChecks(); + + await tick(); + + expect(stagedManifest).toHaveBeenCalledTimes(1); + expect(broadcast).toHaveBeenCalledWith("runtime:update-staged", "0.1.72"); + }); + + it("uses max(pointer, bundled) as currentVersion (not the stale older pointer)", async () => { + // User just upgraded the shell: bundled is 0.1.70, but userData + // still has a pointer from before that says 0.1.69. The check + // must compare 'latest' against bundled (0.1.70), not 0.1.69 — + // otherwise we'd think 0.1.70 is "newer" and re-stage every interval. + const cliEntry = stageVersion("0.1.69"); + writePointer(userData, { version: "0.1.69", cliEntry }); + stagedManifest.mockResolvedValueOnce({ kind: "up-to-date" }); + + const auto = buildAutoUpdate(); + auto?.scheduleChecks(); + await tick(); + + expect(stagedManifest).toHaveBeenCalledWith( + expect.objectContaining({ currentVersion: BUNDLED_VERSION }), + ); + }); + + it("uses the staged version as currentVersion when pointer > bundled", async () => { + const cliEntry = stageVersion("0.1.71"); + writePointer(userData, { version: "0.1.71", cliEntry }); + stagedManifest.mockResolvedValueOnce({ kind: "already-staged" }); + + const auto = buildAutoUpdate(); + auto?.scheduleChecks(); + await tick(); + + expect(stagedManifest).toHaveBeenCalledWith( + expect.objectContaining({ currentVersion: "0.1.71" }), + ); + }); + + it("does not broadcast on up-to-date / already-staged / bad-version outcomes", async () => { + stagedManifest.mockResolvedValueOnce({ kind: "up-to-date" }); + const auto = buildAutoUpdate(); + auto?.scheduleChecks(); + await tick(); + + expect(broadcast).not.toHaveBeenCalledWith( + "runtime:update-staged", + expect.anything(), + ); + }); + + it("swallows pacote/network errors so the timer keeps firing", async () => { + stagedManifest.mockRejectedValueOnce(new Error("ENOTFOUND registry")); + const auto = buildAutoUpdate(); + auto?.scheduleChecks(); + await tick(); + // No throw, no broadcast — the next interval tick can run normally. + expect(broadcast).not.toHaveBeenCalled(); + }); + + it("stop() prevents pending and future checks from running", async () => { + stagedManifest.mockResolvedValue({ kind: "up-to-date" }); + const auto = buildAutoUpdate(); + auto?.scheduleChecks(); + auto?.stop(); + + await vi.advanceTimersByTimeAsync(30 * 60_000 * 5); + expect(stagedManifest).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/desktop/test/runtime-child-manager.test.ts b/packages/desktop/test/runtime-child-manager.test.ts index 2f98d065c..f3cb6727c 100644 --- a/packages/desktop/test/runtime-child-manager.test.ts +++ b/packages/desktop/test/runtime-child-manager.test.ts @@ -240,6 +240,50 @@ describe("RuntimeChildManager", () => { expect(options.env.NODE_OPTIONS).not.toContain("4096"); }); + // `cliEntryOverride` is forwarded to the child env as + // `KANBAN_CLI_OVERRIDE` for the shim to pick up. + it("forwards cliEntryOverride to the child env as KANBAN_CLI_OVERRIDE", async () => { + const spawnSpy = createSpawnFn(mockChild); + manager = new RuntimeChildManager({ + cliPath: CLI_PATH, + spawnFn: spawnSpy, + cliEntryOverride: "/some/abs/path/cli.js", + }); + await manager.start(TEST_CONFIG); + + const spawnCall = (spawnSpy as ReturnType).mock.calls[0]; + const options = spawnCall[2] as { env: NodeJS.ProcessEnv }; + expect(options.env.KANBAN_CLI_OVERRIDE).toBe("/some/abs/path/cli.js"); + }); + + it("does not set KANBAN_CLI_OVERRIDE when override is omitted", async () => { + const spawnSpy = createSpawnFn(mockChild); + manager = new RuntimeChildManager({ + cliPath: CLI_PATH, + spawnFn: spawnSpy, + }); + await manager.start(TEST_CONFIG); + + const spawnCall = (spawnSpy as ReturnType).mock.calls[0]; + const options = spawnCall[2] as { env: NodeJS.ProcessEnv }; + expect(options.env.KANBAN_CLI_OVERRIDE).toBeUndefined(); + }); + + it("treats an empty string cliEntryOverride as 'no override'", async () => { + // Optional-chained callers can surface "" instead of undefined. + const spawnSpy = createSpawnFn(mockChild); + manager = new RuntimeChildManager({ + cliPath: CLI_PATH, + spawnFn: spawnSpy, + cliEntryOverride: "", + }); + await manager.start(TEST_CONFIG); + + const spawnCall = (spawnSpy as ReturnType).mock.calls[0]; + const options = spawnCall[2] as { env: NodeJS.ProcessEnv }; + expect(options.env.KANBAN_CLI_OVERRIDE).toBeUndefined(); + }); + // Platform-aware spawn options — pinned because regressing either // one breaks a specific failure mode: // - POSIX `detached: true` : required so treeKill(-pid) walks PTYs diff --git a/packages/desktop/test/runtime-orchestrator.test.ts b/packages/desktop/test/runtime-orchestrator.test.ts index 41ba9712d..491340966 100644 --- a/packages/desktop/test/runtime-orchestrator.test.ts +++ b/packages/desktop/test/runtime-orchestrator.test.ts @@ -13,11 +13,22 @@ vi.mock("electron", () => ({ const childManagers: FakeChildManager[] = []; class FakeChildManager extends EventEmitter { - constructor() { + /** One-shot: next `start()` rejects with this error, then clears. */ + static nextStartError: Error | null = null; + /** Constructor options observed across spawns, in order. */ + static lastConstructorOptions: Array> = []; + + constructor(options: Record = {}) { super(); + FakeChildManager.lastConstructorOptions.push(options); childManagers.push(this); } async start(): Promise { + const err = FakeChildManager.nextStartError; + if (err) { + FakeChildManager.nextStartError = null; + throw err; + } return "http://127.0.0.1:3484"; } async shutdown(): Promise {} @@ -1787,7 +1798,7 @@ describe("RuntimeOrchestrator health-probe runtime identification", () => { await orchestrator.shutdown(); }); - it("checkHealth() public method returns false for title-less 200", async () => { + it("checkHealth() public method returns false for title-less 200 (regression: title-grep no-op)", async () => { // Direct API-surface check — guards against the title grep being // regressed into a no-op (e.g. someone removing the .text() read // during a refactor). @@ -1812,5 +1823,262 @@ describe("RuntimeOrchestrator health-probe runtime identification", () => { }); }); +// --------------------------------------------------------------------- +// `cliEntryOverride` callback wiring + same-launch fallback. The host +// owns runtime-store / staging concerns; the orchestrator just plumbs +// a callback into RuntimeChildManager and signals back when a staged +// spawn fails so the host can clear its persistent pointer. +// --------------------------------------------------------------------- +describe("RuntimeOrchestrator cliEntryOverride wiring + fallback", () => { + beforeEach(() => { + childManagers.length = 0; + FakeChildManager.lastConstructorOptions.length = 0; + FakeChildManager.nextStartError = null; + }); + + it("forwards resolveCliEntryOverride() result into the child manager on every spawn", async () => { + // Returns a different override per call so we can verify the + // resolver is re-evaluated on each spawn (a freshly-staged runtime + // must take effect on the next restart, not be cached at construct + // time). Using an iterator instead of a counter so the resolver + // itself stays a pure value-producing function. + const overrides = ["/staged/v1/dist/cli.js", "/staged/v2/dist/cli.js"]; + let i = 0; + const resolveCliEntryOverride = vi.fn(() => overrides[i++] ?? null); + + const orchestrator = new RuntimeOrchestrator({ + host: "127.0.0.1", + port: 3484, + healthTimeoutMs: 500, + resolveCliShimPath: () => process.execPath, + fetchImpl: vi.fn(async () => + Promise.reject(new Error("ECONNREFUSED")), + ) as unknown as typeof fetch, + resolveCliEntryOverride, + attachedProbeIntervalMs: 0, + recoveryProbeIntervalMs: 0, + }); + + // First spawn — connect → startOwnRuntime → createManager. + await orchestrator.connect(); + expect(resolveCliEntryOverride).toHaveBeenCalledTimes(1); + expect( + FakeChildManager.lastConstructorOptions.at(-1)?.cliEntryOverride, + ).toBe("/staged/v1/dist/cli.js"); + + // Second spawn — restart() shuts the manager down and respawns, + // re-querying the resolver. + await orchestrator.restart(); + expect(resolveCliEntryOverride).toHaveBeenCalledTimes(2); + expect( + FakeChildManager.lastConstructorOptions.at(-1)?.cliEntryOverride, + ).toBe("/staged/v2/dist/cli.js"); + + await orchestrator.shutdown(); + }); + + it("passes undefined cliEntryOverride when resolveCliEntryOverride returns null (use bundled cli)", async () => { + const orchestrator = new RuntimeOrchestrator({ + host: "127.0.0.1", + port: 3484, + healthTimeoutMs: 500, + resolveCliShimPath: () => process.execPath, + fetchImpl: vi.fn(async () => + Promise.reject(new Error("ECONNREFUSED")), + ) as unknown as typeof fetch, + resolveCliEntryOverride: () => null, + attachedProbeIntervalMs: 0, + recoveryProbeIntervalMs: 0, + }); + + await orchestrator.connect(); + expect( + FakeChildManager.lastConstructorOptions.at(-1)?.cliEntryOverride, + ).toBeUndefined(); + + await orchestrator.shutdown(); + }); + + it("passes undefined cliEntryOverride when no resolver is wired (dev / unmanaged)", async () => { + const orchestrator = new RuntimeOrchestrator({ + host: "127.0.0.1", + port: 3484, + healthTimeoutMs: 500, + resolveCliShimPath: () => process.execPath, + fetchImpl: vi.fn(async () => + Promise.reject(new Error("ECONNREFUSED")), + ) as unknown as typeof fetch, + attachedProbeIntervalMs: 0, + recoveryProbeIntervalMs: 0, + }); + + await orchestrator.connect(); + expect( + FakeChildManager.lastConstructorOptions.at(-1)?.cliEntryOverride, + ).toBeUndefined(); + + await orchestrator.shutdown(); + }); + + it("on staged-spawn failure: invokes onCliEntryOverrideFailed and retries once with bundled cli", async () => { + // Spawn 1: resolver returns staged cli; FakeChildManager rejects. + // → onCliEntryOverrideFailed fires, orchestrator retries. + // Spawn 2: resolver returns null (host cleared the pointer); + // FakeChildManager succeeds. + const overrides: Array = ["/staged/v1/dist/cli.js", null]; + let i = 0; + const resolveCliEntryOverride = vi.fn(() => overrides[i++] ?? null); + const onCliEntryOverrideFailed = vi.fn(); + + // Single-shot failure on the first spawn — second spawn (bundled) + // resolves to the default URL. + FakeChildManager.nextStartError = new Error( + "runtime exited during startup: ENOENT staged/cli.js", + ); + + const orchestrator = new RuntimeOrchestrator({ + host: "127.0.0.1", + port: 3484, + healthTimeoutMs: 500, + resolveCliShimPath: () => process.execPath, + fetchImpl: vi.fn(async () => + Promise.reject(new Error("ECONNREFUSED")), + ) as unknown as typeof fetch, + resolveCliEntryOverride, + onCliEntryOverrideFailed, + attachedProbeIntervalMs: 0, + recoveryProbeIntervalMs: 0, + }); + + // connect() must succeed thanks to the same-launch retry, *not* + // reject — the user shouldn't see the failure. + await orchestrator.connect(); + expect(orchestrator.isOwned()).toBe(true); + expect(orchestrator.getUrl()).toBe("http://127.0.0.1:3484"); + + expect(onCliEntryOverrideFailed).toHaveBeenCalledTimes(1); + expect(onCliEntryOverrideFailed.mock.calls[0]?.[0]).toMatch( + /ENOENT staged\/cli.js/, + ); + // `cliEntry` must be captured at spawn time and forwarded — + // not re-derived from the pointer at failure time, which a + // concurrent background stage could have advanced. See + // runtime-auto-update onFailed for the matching consumer. + expect(onCliEntryOverrideFailed.mock.calls[0]?.[1]).toBe( + "/staged/v1/dist/cli.js", + ); + + // Two child managers were constructed — first staged (failed), + // second bundled (succeeded). + expect(FakeChildManager.lastConstructorOptions).toHaveLength(2); + expect( + FakeChildManager.lastConstructorOptions[0]?.cliEntryOverride, + ).toBe("/staged/v1/dist/cli.js"); + expect( + FakeChildManager.lastConstructorOptions[1]?.cliEntryOverride, + ).toBeUndefined(); + + await orchestrator.shutdown(); + }); + + it("does not loop forever if the bundled retry also fails (single retry only)", async () => { + // Both spawns fail. After the staged failure → callback → retry, + // the bundled spawn fails too — and that error propagates without + // triggering another callback or another retry. + const resolveCliEntryOverride = vi.fn(() => "/staged/v1/dist/cli.js"); + const onCliEntryOverrideFailed = vi.fn(); + + // Make every FakeChildManager.start() call fail. Using a + // once-per-instance error setter doesn't compose cleanly across + // two manager instances, so spy directly on the prototype. + const startSpy = vi + .spyOn(FakeChildManager.prototype, "start") + .mockRejectedValue(new Error("spawn failed")); + + const orchestrator = new RuntimeOrchestrator({ + host: "127.0.0.1", + port: 3484, + healthTimeoutMs: 500, + resolveCliShimPath: () => process.execPath, + fetchImpl: vi.fn(async () => + Promise.reject(new Error("ECONNREFUSED")), + ) as unknown as typeof fetch, + resolveCliEntryOverride, + onCliEntryOverrideFailed, + attachedProbeIntervalMs: 0, + recoveryProbeIntervalMs: 0, + }); + + await expect(orchestrator.connect()).rejects.toThrow(/spawn failed/); + + // Callback fired exactly once for the staged failure; the bundled + // retry's failure does not re-enter the callback. + expect(onCliEntryOverrideFailed).toHaveBeenCalledTimes(1); + // Two spawn attempts total: staged + bundled retry. + expect(FakeChildManager.lastConstructorOptions).toHaveLength(2); + + startSpy.mockRestore(); + }); + + it("does not invoke onCliEntryOverrideFailed when a *bundled* spawn fails", async () => { + // No override → first spawn is bundled. Failure is a packaging + // issue, not a staged-runtime issue; the callback must not fire + // and the error propagates directly. + const resolveCliEntryOverride = vi.fn(() => null); + const onCliEntryOverrideFailed = vi.fn(); + + FakeChildManager.nextStartError = new Error( + "runtime exited during startup: ENOENT bundled/cli.js", + ); + + const orchestrator = new RuntimeOrchestrator({ + host: "127.0.0.1", + port: 3484, + healthTimeoutMs: 500, + resolveCliShimPath: () => process.execPath, + fetchImpl: vi.fn(async () => + Promise.reject(new Error("ECONNREFUSED")), + ) as unknown as typeof fetch, + resolveCliEntryOverride, + onCliEntryOverrideFailed, + attachedProbeIntervalMs: 0, + recoveryProbeIntervalMs: 0, + }); + + await expect(orchestrator.connect()).rejects.toThrow(/ENOENT bundled/); + expect(onCliEntryOverrideFailed).not.toHaveBeenCalled(); + // Single spawn attempt — no retry. + expect(FakeChildManager.lastConstructorOptions).toHaveLength(1); + }); + + it("falls back to bundled when resolveCliEntryOverride throws (does not brick the spawn)", async () => { + const resolveCliEntryOverride = vi.fn(() => { + throw new Error("disk I/O"); + }); + + const orchestrator = new RuntimeOrchestrator({ + host: "127.0.0.1", + port: 3484, + healthTimeoutMs: 500, + resolveCliShimPath: () => process.execPath, + fetchImpl: vi.fn(async () => + Promise.reject(new Error("ECONNREFUSED")), + ) as unknown as typeof fetch, + resolveCliEntryOverride, + attachedProbeIntervalMs: 0, + recoveryProbeIntervalMs: 0, + }); + + // connect() succeeds with bundled. + await orchestrator.connect(); + expect(orchestrator.isOwned()).toBe(true); + expect( + FakeChildManager.lastConstructorOptions.at(-1)?.cliEntryOverride, + ).toBeUndefined(); + + await orchestrator.shutdown(); + }); +}); + diff --git a/packages/desktop/test/runtime-store.test.ts b/packages/desktop/test/runtime-store.test.ts new file mode 100644 index 000000000..6d97f311e --- /dev/null +++ b/packages/desktop/test/runtime-store.test.ts @@ -0,0 +1,283 @@ +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + cleanupPartials, + clearPointer, + cliEntryFor, + isBadVersion, + markBadVersion, + partialDir, + readPointer, + removeVersionDir, + resolvePointerCliEntry, + versionDir, + versionFromCliEntry, + writePointer, +} from "../src/runtime-store.js"; + +let userData: string; + +beforeEach(() => { + userData = mkdtempSync(path.join(tmpdir(), "runtime-store-")); +}); + +afterEach(() => { + rmSync(userData, { recursive: true, force: true }); +}); + +const pointerPathFor = (root: string): string => + path.join(root, "runtime-store", "current.json"); + +/** Lay out `versions//dist/cli.js` so a pointer to it is valid. */ +function stageVersion(root: string, version: string): string { + const cliEntry = cliEntryFor(root, version); + mkdirSync(path.dirname(cliEntry), { recursive: true }); + writeFileSync(cliEntry, "// runtime"); + return cliEntry; +} + +describe("runtime-store: pointer", () => { + it("returns null when missing or corrupt", () => { + expect(readPointer(userData)).toBeNull(); + mkdirSync(path.dirname(pointerPathFor(userData)), { recursive: true }); + writeFileSync(pointerPathFor(userData), "{not-json"); + expect(readPointer(userData)).toBeNull(); + }); + + it("round-trips atomically (tmp + rename, no leftover *.tmp)", () => { + const cliEntry = stageVersion(userData, "0.1.66"); + writePointer(userData, { version: "0.1.66", cliEntry }); + + const dir = path.dirname(pointerPathFor(userData)); + expect(readdirSync(dir).some((n) => n.endsWith(".tmp"))).toBe(false); + expect(readPointer(userData)).toEqual({ + version: "0.1.66", + cliEntry, + }); + }); + + it("rejects garbage on read (semver-invalid version, missing cliEntry)", () => { + const writeRaw = (body: unknown): void => { + mkdirSync(path.dirname(pointerPathFor(userData)), { recursive: true }); + writeFileSync(pointerPathFor(userData), JSON.stringify(body)); + }; + + writeRaw({ version: "abc", cliEntry: "/x" }); + expect(readPointer(userData)).toBeNull(); + + writeRaw({ version: "1.0.0" }); + expect(readPointer(userData)).toBeNull(); + + writeRaw({ version: "1.0.0", cliEntry: "" }); + expect(readPointer(userData)).toBeNull(); + }); + + it("writePointer rejects a non-semver version", () => { + expect(() => + writePointer(userData, { version: "abc", cliEntry: "/x" }), + ).toThrow(/invalid semver/); + }); + + it("readPointer rejects a non-canonical cliEntry", () => { + // `cliEntry` is forwarded to the shim as KANBAN_CLI_OVERRIDE. + // A non-canonical path would let a tampered current.json execute + // arbitrary on-disk JS, so the pointer must be rejected. + mkdirSync(path.dirname(pointerPathFor(userData)), { recursive: true }); + writeFileSync( + pointerPathFor(userData), + JSON.stringify({ version: "1.0.0", cliEntry: "/elsewhere/cli.js" }), + ); + expect(readPointer(userData)).toBeNull(); + }); + + it("readPointer rejects a relative cliEntry even if it would resolve to the canonical path", () => { + // Pointer validity must not depend on `process.cwd()` at the + // moment of read — a relative form is always a packaging / + // hand-edit bug, not a legitimate state. + const canonical = cliEntryFor(userData, "1.0.0"); + mkdirSync(path.dirname(pointerPathFor(userData)), { recursive: true }); + const relative = path.relative(process.cwd(), canonical); + writeFileSync( + pointerPathFor(userData), + JSON.stringify({ version: "1.0.0", cliEntry: relative }), + ); + expect(readPointer(userData)).toBeNull(); + }); + + it("writePointer rejects a non-canonical cliEntry", () => { + expect(() => + writePointer(userData, { + version: "1.0.0", + cliEntry: "/elsewhere/cli.js", + }), + ).toThrow(/cliEntry for 1\.0\.0 must be/); + }); + + it("writePointer rejects a relative cliEntry (symmetric with readPointer)", () => { + // Even if the relative form would resolve to the canonical path + // from the current cwd, accepting it would let pointer validity + // depend on `process.cwd()` at write time. The on-disk contract + // is "absolute canonical path" at both boundaries. + const canonical = cliEntryFor(userData, "1.0.0"); + const relative = path.relative(process.cwd(), canonical); + expect(() => + writePointer(userData, { version: "1.0.0", cliEntry: relative }), + ).toThrow(/cliEntry for 1\.0\.0 must be/); + }); + + it("clearPointer is a no-op when missing and removes when present", () => { + expect(() => clearPointer(userData)).not.toThrow(); + const cliEntry = stageVersion(userData, "0.1.0"); + writePointer(userData, { version: "0.1.0", cliEntry }); + clearPointer(userData); + expect(readPointer(userData)).toBeNull(); + }); +}); + +describe("runtime-store: versionFromCliEntry", () => { + it("extracts the version from a canonical cliEntry", () => { + const cli = cliEntryFor(userData, "1.2.3"); + expect(versionFromCliEntry(userData, cli)).toBe("1.2.3"); + }); + + it("returns null for a relative cliEntry", () => { + expect(versionFromCliEntry(userData, "versions/1.2.3/dist/cli.js")).toBeNull(); + }); + + it("returns null when the path's segment isn't valid semver", () => { + const bogus = path.join(userData, "runtime-store", "versions", "abc", "dist", "cli.js"); + expect(versionFromCliEntry(userData, bogus)).toBeNull(); + }); + + it("returns null when the path-shape doesn't match `versions//dist/cli.js`", () => { + // Right `` segment in the right *position* but wrong root or + // wrong leaf must not yield a version — otherwise the rollback + // path could mark a real-but-unrelated version bad and remove + // its on-disk dir, just because some stray path happened to have + // `/dist/cli.js` somewhere in it. + + // Wrong leaf filename. + const wrongLeaf = path.join( + userData, + "runtime-store", + "versions", + "1.2.3", + "dist", + "not-cli.js", + ); + expect(versionFromCliEntry(userData, wrongLeaf)).toBeNull(); + + // Right shape under the wrong root (different userData). + const wrongRoot = path.join( + "/elsewhere", + "runtime-store", + "versions", + "1.2.3", + "dist", + "cli.js", + ); + expect(versionFromCliEntry(userData, wrongRoot)).toBeNull(); + + // `` in the right *segment* position but the parent isn't `dist`. + const wrongParent = path.join( + userData, + "runtime-store", + "versions", + "1.2.3", + "build", + "cli.js", + ); + expect(versionFromCliEntry(userData, wrongParent)).toBeNull(); + }); +}); + +describe("runtime-store: resolvePointerCliEntry", () => { + it("returns null when no pointer exists", () => { + expect(resolvePointerCliEntry(userData)).toBeNull(); + }); + + it("returns the cliEntry when it exists on disk", () => { + const cliEntry = stageVersion(userData, "0.5.0"); + writePointer(userData, { version: "0.5.0", cliEntry }); + expect(resolvePointerCliEntry(userData)).toBe(cliEntry); + }); + + it("returns null when pointer exists but cliEntry is missing on disk", () => { + // Caller (`createRuntimeAutoUpdate.loadOverride`) uses this signal + // to drop the pointer and unfreeze the background updater. + const cliEntry = stageVersion(userData, "0.6.0"); + writePointer(userData, { version: "0.6.0", cliEntry }); + rmSync(path.dirname(cliEntry), { recursive: true }); + expect(resolvePointerCliEntry(userData)).toBeNull(); + }); +}); + +describe("runtime-store: bad-versions", () => { + it("isBadVersion is false for everything when the file is missing", () => { + expect(isBadVersion(userData, "1.0.0")).toBe(false); + }); + + it("markBadVersion persists, deduplicates, and sorts by semver", () => { + markBadVersion(userData, "1.10.0"); + markBadVersion(userData, "1.2.0"); + markBadVersion(userData, "1.10.0"); + expect(isBadVersion(userData, "1.2.0")).toBe(true); + expect(isBadVersion(userData, "1.10.0")).toBe(true); + expect(isBadVersion(userData, "1.5.0")).toBe(false); + + const raw = readFileSync( + path.join(userData, "runtime-store", "bad-versions.json"), + "utf8", + ); + expect(JSON.parse(raw)).toEqual(["1.2.0", "1.10.0"]); + }); + + it("rejects non-semver on write, ignores corrupt file on read", () => { + expect(() => markBadVersion(userData, "junk")).toThrow(/invalid semver/); + + mkdirSync(path.join(userData, "runtime-store"), { recursive: true }); + writeFileSync( + path.join(userData, "runtime-store", "bad-versions.json"), + "{not-json", + ); + expect(isBadVersion(userData, "1.0.0")).toBe(false); + }); +}); + +describe("runtime-store: cleanup", () => { + it("cleanupPartials removes only `*.partial` directories", () => { + mkdirSync(versionDir(userData, "0.1.0"), { recursive: true }); + mkdirSync(partialDir(userData, "0.5.0"), { recursive: true }); + cleanupPartials(userData); + expect(existsSync(versionDir(userData, "0.1.0"))).toBe(true); + expect(existsSync(partialDir(userData, "0.5.0"))).toBe(false); + }); + + it("cleanupPartials is a no-op when versions root does not exist", () => { + expect(() => cleanupPartials(userData)).not.toThrow(); + }); + + it("removeVersionDir clears a finalized dir and is safe with garbage input", () => { + stageVersion(userData, "1.0.0"); + removeVersionDir(userData, "1.0.0"); + expect(existsSync(versionDir(userData, "1.0.0"))).toBe(false); + expect(() => removeVersionDir(userData, "../../etc")).not.toThrow(); + }); + + it("versionDir / partialDir reject non-semver inputs", () => { + expect(() => versionDir(userData, "../../etc")).toThrow(/invalid semver/); + expect(() => partialDir(userData, "abc")).toThrow(/invalid semver/); + }); +}); diff --git a/packages/desktop/test/runtime-update.test.ts b/packages/desktop/test/runtime-update.test.ts new file mode 100644 index 000000000..e440148c3 --- /dev/null +++ b/packages/desktop/test/runtime-update.test.ts @@ -0,0 +1,206 @@ +/** + * Unit tests for `runtime-update.checkAndStageLatestRuntime`. pacote is + * mocked at the module boundary so these tests don't hit the registry. + */ + +import { + mkdirSync, + mkdtempSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + cliEntryFor, + markBadVersion, + readPointer, + versionDir, + writePointer, +} from "../src/runtime-store.js"; +import { checkAndStageLatestRuntime } from "../src/runtime-update.js"; + +const manifestMock = vi.fn(); +const extractMock = vi.fn(); + +vi.mock("pacote", () => ({ + default: { + manifest: (...args: unknown[]) => manifestMock(...args), + extract: (...args: unknown[]) => extractMock(...args), + }, +})); + +let userData: string; +let nativeDepsSource: string; + +beforeEach(() => { + userData = mkdtempSync(path.join(tmpdir(), "runtime-update-")); + nativeDepsSource = mkdtempSync(path.join(tmpdir(), "runtime-update-deps-")); + // Pretend node-pty is bundled — the updater copies it into each + // staged version. A bare directory is enough for `cp -r`. + mkdirSync(path.join(nativeDepsSource, "node-pty"), { recursive: true }); + writeFileSync( + path.join(nativeDepsSource, "node-pty", "package.json"), + JSON.stringify({ name: "node-pty", version: "1.0.0" }), + ); + + manifestMock.mockReset(); + extractMock.mockReset(); + + // Default extract: lay out a `dist/cli.js` so the post-extract + // sanity check passes. Tests override this for failure modes. + extractMock.mockImplementation(async (_spec: string, dest: string) => { + mkdirSync(path.join(dest, "dist"), { recursive: true }); + writeFileSync(path.join(dest, "dist", "cli.js"), "// runtime"); + }); +}); + +afterEach(() => { + rmSync(userData, { recursive: true, force: true }); + rmSync(nativeDepsSource, { recursive: true, force: true }); +}); + +describe("checkAndStageLatestRuntime: gates", () => { + it("returns up-to-date when latest <= currentVersion", async () => { + manifestMock.mockResolvedValueOnce({ version: "0.1.0" }); + + const outcome = await checkAndStageLatestRuntime({ + userData, + currentVersion: "0.1.0", + nativeDepsSource, + }); + + expect(outcome).toEqual({ kind: "up-to-date" }); + expect(extractMock).not.toHaveBeenCalled(); + }); + + it("returns already-staged when pointer.version === latest", async () => { + const cliEntry = cliEntryFor(userData, "0.5.0"); + mkdirSync(path.dirname(cliEntry), { recursive: true }); + writeFileSync(cliEntry, "// runtime"); + writePointer(userData, { version: "0.5.0", cliEntry }); + manifestMock.mockResolvedValueOnce({ version: "0.5.0" }); + + const outcome = await checkAndStageLatestRuntime({ + userData, + currentVersion: "0.4.0", + nativeDepsSource, + }); + + expect(outcome).toEqual({ kind: "already-staged" }); + expect(extractMock).not.toHaveBeenCalled(); + }); + + it("skips bad versions without extracting", async () => { + markBadVersion(userData, "1.0.0"); + manifestMock.mockResolvedValueOnce({ version: "1.0.0" }); + + const outcome = await checkAndStageLatestRuntime({ + userData, + currentVersion: "0.5.0", + nativeDepsSource, + }); + + expect(outcome).toEqual({ kind: "bad-version", version: "1.0.0" }); + expect(extractMock).not.toHaveBeenCalled(); + }); + + it("throws on a non-semver registry version", async () => { + manifestMock.mockResolvedValueOnce({ version: "garbage" }); + await expect( + checkAndStageLatestRuntime({ + userData, + currentVersion: "0.1.0", + nativeDepsSource, + }), + ).rejects.toThrow(/non-semver/); + }); + + it("treats a non-semver currentVersion as 'unknown' and proceeds", async () => { + // Defends against a corrupted pointer leaking a non-semver version + // into the gate; without the guard, semver.gt would throw. + manifestMock.mockResolvedValueOnce({ version: "1.0.0" }); + + const outcome = await checkAndStageLatestRuntime({ + userData, + currentVersion: "garbage", + nativeDepsSource, + }); + + expect(outcome.kind).toBe("staged"); + }); +}); + +describe("checkAndStageLatestRuntime: staging", () => { + it("stages, copies node-pty, and writes the pointer atomically", async () => { + manifestMock.mockResolvedValueOnce({ version: "1.0.0" }); + + const outcome = await checkAndStageLatestRuntime({ + userData, + currentVersion: "0.5.0", + nativeDepsSource, + }); + + expect(outcome).toEqual({ kind: "staged", version: "1.0.0" }); + expect(readPointer(userData)).toEqual({ + version: "1.0.0", + cliEntry: cliEntryFor(userData, "1.0.0"), + }); + + const finalDir = versionDir(userData, "1.0.0"); + expect( + readdirSync(path.join(finalDir, "node_modules")).includes("node-pty"), + ).toBe(true); + expect( + readdirSync(path.dirname(finalDir)).every((n) => !n.endsWith(".partial")), + ).toBe(true); + }); + + it("throws (and leaves pointer untouched) when bundled node-pty is missing", async () => { + rmSync(path.join(nativeDepsSource, "node-pty"), { recursive: true }); + manifestMock.mockResolvedValueOnce({ version: "1.0.0" }); + + await expect( + checkAndStageLatestRuntime({ + userData, + currentVersion: "0.5.0", + nativeDepsSource, + }), + ).rejects.toThrow(/bundled node-pty missing/); + expect(readPointer(userData)).toBeNull(); + }); + + it("throws when the extracted package has no dist/cli.js", async () => { + extractMock.mockImplementationOnce(async (_spec: string, dest: string) => { + mkdirSync(dest, { recursive: true }); + }); + manifestMock.mockResolvedValueOnce({ version: "1.0.0" }); + + await expect( + checkAndStageLatestRuntime({ + userData, + currentVersion: "0.5.0", + nativeDepsSource, + }), + ).rejects.toThrow(/missing dist\/cli\.js/); + expect(readPointer(userData)).toBeNull(); + }); + + it("recovers from a stale `.partial/` left by a prior interrupted run", async () => { + mkdirSync(`${versionDir(userData, "1.0.0")}.partial`, { recursive: true }); + manifestMock.mockResolvedValueOnce({ version: "1.0.0" }); + + const outcome = await checkAndStageLatestRuntime({ + userData, + currentVersion: "0.5.0", + nativeDepsSource, + }); + + expect(outcome.kind).toBe("staged"); + expect(readPointer(userData)?.version).toBe("1.0.0"); + }); +});