diff --git a/package-lock.json b/package-lock.json index aaaf674..26474d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,31 +10,19 @@ ], "devDependencies": { "@eslint/js": "^10.0.1", + "@vitest/coverage-v8": "^4.1.8", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "prettier": "^3.8.2", - "typescript-eslint": "^8.58.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "typescript-eslint": "^8.58.2", + "vitest": "^4.1.8" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -42,9 +30,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -52,13 +40,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -68,25 +56,28 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", @@ -753,76 +744,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -913,17 +834,6 @@ } } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pluginos/bridge-plugin": { "resolved": "packages/bridge-plugin", "link": true @@ -1286,10 +1196,10 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "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" }, @@ -1303,6 +1213,24 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1611,31 +1539,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", - "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "std-env": "^3.8.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.9", - "vitest": "2.1.9" + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1644,38 +1570,40 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1687,84 +1615,68 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2041,19 +1953,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/adm-zip": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", @@ -2153,6 +2052,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2371,18 +2282,11 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2404,16 +2308,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2544,6 +2438,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2640,16 +2541,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2666,16 +2557,6 @@ "node": ">= 0.8" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -2770,13 +2651,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2790,13 +2664,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2862,9 +2729,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -3193,30 +3060,6 @@ "node": ">=18.0.0" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3441,23 +3284,6 @@ "dev": true, "license": "ISC" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3500,16 +3326,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3547,19 +3363,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", @@ -3573,28 +3376,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3615,39 +3396,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3868,16 +3616,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -4010,16 +3748,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4062,19 +3790,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4116,21 +3831,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4145,22 +3845,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -4212,9 +3896,9 @@ } }, "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -4322,31 +4006,14 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^5.0.0" }, "engines": { "node": ">=10" @@ -4362,13 +4029,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -4379,13 +4039,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4397,15 +4050,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { @@ -4500,19 +4153,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -4529,16 +4169,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -4630,35 +4260,6 @@ "dev": true, "license": "MIT" }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4693,6 +4294,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4714,22 +4329,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4790,13 +4389,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -4854,23 +4446,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-to-regexp": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", @@ -4888,16 +4463,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5131,34 +4696,6 @@ "renderkid": "^3.0.0" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5221,13 +4758,6 @@ "node": ">= 0.10" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -5632,19 +5162,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5693,82 +5210,12 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5782,46 +5229,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -5955,21 +5362,6 @@ "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6055,30 +5447,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -6271,16 +5643,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -6489,36 +5851,6 @@ } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -6551,58 +5883,79 @@ } }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -6613,15 +5966,34 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/vitest/node_modules/picomatch": { + "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" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/watchpack": { "version": "2.5.1", @@ -6766,13 +6138,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -6856,107 +6221,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7026,7 +6290,7 @@ "html-webpack-plugin": "^5.6.0", "ts-loader": "^9.5.0", "typescript": "^5.5.0", - "vitest": "^2.1.0", + "vitest": "^4.1.8", "webpack": "^5.90.0", "webpack-cli": "^5.1.0" } @@ -7051,199 +6315,7 @@ "devDependencies": { "tsx": "^4.0.0", "typescript": "^5.0.0", - "vitest": "^1.0.0" - } - }, - "packages/claude-plugin/node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "packages/claude-plugin/node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "packages/claude-plugin/node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "packages/claude-plugin/node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "packages/claude-plugin/node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "packages/claude-plugin/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/claude-plugin/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "packages/claude-plugin/node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "packages/claude-plugin/node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "packages/claude-plugin/node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "vitest": "^4.1.8" } }, "packages/claude-plugin/node_modules/typescript": { @@ -7260,108 +6332,6 @@ "node": ">=14.17" } }, - "packages/claude-plugin/node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "packages/claude-plugin/node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/mcp-server": { "name": "pluginos", "version": "0.4.3", @@ -7378,12 +6348,12 @@ "@pluginos/shared": "*", "@types/adm-zip": "^0.5.8", "@types/ws": "^8.5.0", - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "adm-zip": "^0.5.17", "tsup": "^8.5.1", "tsx": "^4.19.0", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" }, "engines": { "node": ">=18" @@ -7407,9 +6377,9 @@ "name": "@pluginos/shared", "version": "0.4.3", "devDependencies": { - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" } }, "packages/shared/node_modules/typescript": { diff --git a/package.json b/package.json index c25e5e3..5eea0b2 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,18 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@vitest/coverage-v8": "^4.1.8", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "prettier": "^3.8.2", - "typescript-eslint": "^8.58.2" + "typescript-eslint": "^8.58.2", + "vitest": "^4.1.8" }, "overrides": { "vite": "^6.4.2", - "esbuild": "^0.25.0" + "esbuild": "^0.25.0", + "vitest": "^4.1.8", + "@vitest/coverage-v8": "^4.1.8" } } diff --git a/packages/bridge-plugin/package.json b/packages/bridge-plugin/package.json index 464d676..5f29f89 100644 --- a/packages/bridge-plugin/package.json +++ b/packages/bridge-plugin/package.json @@ -16,7 +16,7 @@ "html-webpack-plugin": "^5.6.0", "ts-loader": "^9.5.0", "typescript": "^5.5.0", - "vitest": "^2.1.0", + "vitest": "^4.1.8", "webpack": "^5.90.0", "webpack-cli": "^5.1.0" } diff --git a/packages/bridge-plugin/src/__tests__/discovery.test.ts b/packages/bridge-plugin/src/__tests__/discovery.test.ts new file mode 100644 index 0000000..52273cb --- /dev/null +++ b/packages/bridge-plugin/src/__tests__/discovery.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { + fetchStateJson, + rankCandidates, + discoverCandidatePorts, + type StateFile, + SUPPORTED_VERSION, +} from "../discovery.js"; + +describe("fetchStateJson", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns parsed state on 200 with a supported version", async () => { + const state: StateFile = { + version: 1, + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + startedAt: 100, + parentPid: 99, + parentAlive: true, + socketPath: null, + }; + (fetch as unknown as ReturnType).mockResolvedValueOnce({ + ok: true, + json: async () => state, + }); + const result = await fetchStateJson(9500); + expect(result).toEqual(state); + }); + + it("returns null on a non-200 response", async () => { + (fetch as unknown as ReturnType).mockResolvedValueOnce({ ok: false }); + expect(await fetchStateJson(9500)).toBeNull(); + }); + + it("returns null when fetch throws", async () => { + (fetch as unknown as ReturnType).mockRejectedValueOnce(new Error("boom")); + expect(await fetchStateJson(9500)).toBeNull(); + }); + + it("returns null for a future version", async () => { + (fetch as unknown as ReturnType).mockResolvedValueOnce({ + ok: true, + json: async () => ({ version: SUPPORTED_VERSION + 1, pid: 1, port: 9500 }), + }); + expect(await fetchStateJson(9500)).toBeNull(); + }); +}); + +describe("rankCandidates", () => { + function makeState(overrides: Partial): StateFile { + return { + version: 1, + pid: 1, + port: 9500, + serverVersion: "0.4.3", + startedAt: 0, + parentPid: 99, + parentAlive: true, + socketPath: null, + ...overrides, + }; + } + + it("filters out candidates with parentAlive=false", () => { + const ranked = rankCandidates([ + { port: 9500, state: makeState({ parentAlive: false, startedAt: 100 }) }, + { port: 9501, state: makeState({ parentAlive: true, startedAt: 50 }) }, + ]); + expect(ranked).toHaveLength(1); + expect(ranked[0].port).toBe(9501); + }); + + it("sorts by startedAt descending (newest first)", () => { + const ranked = rankCandidates([ + { port: 9500, state: makeState({ startedAt: 100 }) }, + { port: 9501, state: makeState({ startedAt: 200 }) }, + { port: 9502, state: makeState({ startedAt: 150 }) }, + ]); + expect(ranked.map((c) => c.port)).toEqual([9501, 9502, 9500]); + }); +}); + +describe("discoverCandidatePorts (probe-and-rank end-to-end)", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns ranked candidates, excluding orphans", async () => { + const orphan: StateFile = { + version: 1, + pid: 1, + port: 9500, + serverVersion: "0.4.3", + startedAt: 100, + parentPid: 99, + parentAlive: false, + socketPath: null, + }; + const live: StateFile = { + version: 1, + pid: 2, + port: 9501, + serverVersion: "0.4.3", + startedAt: 200, + parentPid: 100, + parentAlive: true, + socketPath: null, + }; + const fetchMock = fetch as unknown as ReturnType; + fetchMock.mockImplementation(async (url: string) => { + if (url.includes(":9500")) return { ok: true, json: async () => orphan }; + if (url.includes(":9501")) return { ok: true, json: async () => live }; + throw new Error("ECONNREFUSED"); + }); + const ranked = await discoverCandidatePorts([9500, 9501, 9502]); + expect(ranked).toHaveLength(1); + expect(ranked[0].port).toBe(9501); + }); + + it("returns empty when no servers respond", async () => { + const fetchMock = fetch as unknown as ReturnType; + fetchMock.mockRejectedValue(new Error("ECONNREFUSED")); + const ranked = await discoverCandidatePorts([9500, 9501]); + expect(ranked).toEqual([]); + }); +}); diff --git a/packages/bridge-plugin/src/discovery.ts b/packages/bridge-plugin/src/discovery.ts new file mode 100644 index 0000000..2a69a0a --- /dev/null +++ b/packages/bridge-plugin/src/discovery.ts @@ -0,0 +1,63 @@ +export interface StateFile { + version: 1; + pid: number; + port: number; + serverVersion: string; + startedAt: number; + parentPid: number; + parentAlive: boolean; + socketPath: string | null; +} + +export const SUPPORTED_VERSION = 1; +export const FETCH_TIMEOUT_MS = 300; + +export interface DiscoveryCandidate { + port: number; + state: StateFile; +} + +export async function fetchStateJson(port: number): Promise { + try { + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + let res: Response; + try { + res = await fetch(`http://127.0.0.1:${port}/state.json`, { + signal: controller.signal, + }); + if (!res.ok) return null; + const body = (await res.json()) as unknown; + if ( + typeof body === "object" && + body !== null && + typeof (body as { version?: unknown }).version === "number" && + (body as { version: number }).version <= SUPPORTED_VERSION + ) { + return body as StateFile; + } + return null; + } finally { + clearTimeout(t); + } + } catch { + return null; + } +} + +export function rankCandidates(candidates: DiscoveryCandidate[]): DiscoveryCandidate[] { + return candidates + .filter((c) => c.state.parentAlive !== false) + .sort((a, b) => b.state.startedAt - a.state.startedAt); +} + +export async function discoverCandidatePorts(ports: number[]): Promise { + const probed = await Promise.all( + ports.map(async (port) => { + const state = await fetchStateJson(port); + return state ? { port, state } : null; + }) + ); + const candidates = probed.filter((c): c is DiscoveryCandidate => c !== null); + return rankCandidates(candidates); +} diff --git a/packages/bridge-plugin/src/ui-entry.ts b/packages/bridge-plugin/src/ui-entry.ts index db5e8a4..63e1258 100644 --- a/packages/bridge-plugin/src/ui-entry.ts +++ b/packages/bridge-plugin/src/ui-entry.ts @@ -3,6 +3,7 @@ import { attachThemeListener, detectInitialTheme, applyTheme } from "./ui/theme" import { getLastPort, setLastPort } from "./ui/storage"; import { ActivityLog, type LogEntry } from "./ui/activity-log"; import { isCompatible } from "./ui/version-check"; +import { discoverCandidatePorts } from "./discovery"; import { VERSION, DXT_DOWNLOAD_URL, @@ -171,13 +172,29 @@ async function scanAndConnect(): Promise { if (lastPort) order.push(lastPort); for (let p = PORT_MIN; p <= PORT_MAX; p++) if (p !== lastPort) order.push(p); + // Phase 1: discovery probe — find live servers via /state.json + const ranked = await discoverCandidatePorts(order); + + // Phase 2: try ranked candidates first (parentAlive=true, newest first) + for (const candidate of ranked) { + const ok = await tryConnect(candidate.port); + if (ok) { + setLastPort(candidate.port); + return; + } + } + + // Phase 3: fallback — try all ports in original order (preserves existing scan behavior) + const triedPorts = new Set(ranked.map((c) => c.port)); for (const port of order) { + if (triedPorts.has(port)) continue; const ok = await tryConnect(port); if (ok) { setLastPort(port); return; } } + setStatus("disconnected"); scheduleReconnect(); } finally { diff --git a/packages/claude-plugin/package.json b/packages/claude-plugin/package.json index eba7463..649e9f6 100644 --- a/packages/claude-plugin/package.json +++ b/packages/claude-plugin/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "tsx": "^4.0.0", - "vitest": "^1.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vitest": "^4.1.8" } } diff --git a/packages/claude-plugin/skills/pluginos-figma/SKILL.md b/packages/claude-plugin/skills/pluginos-figma/SKILL.md index 9df2c55..a43d03b 100644 --- a/packages/claude-plugin/skills/pluginos-figma/SKILL.md +++ b/packages/claude-plugin/skills/pluginos-figma/SKILL.md @@ -59,6 +59,7 @@ If any `pluginos.*` tool returns "No plugin connected" or times out: 1. Tell the user: "Open the PluginOS Bridge plugin in Figma (Plugins → PluginOS Bridge → Run), then let me know." 2. Do NOT silently fall back to Figma MCP. 3. Wait for confirmation before retrying. +4. If the user relaunches the plugin mid-task, call `pluginos.wait_for_reconnect({ timeoutSec: 60 })` to gracefully block until reconnect, then retry the failed op. ## Don'ts diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 698f9d7..ef4241f 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -53,11 +53,11 @@ "@pluginos/shared": "*", "@types/adm-zip": "^0.5.8", "@types/ws": "^8.5.0", - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "adm-zip": "^0.5.17", "tsup": "^8.5.1", "tsx": "^4.19.0", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" } } diff --git a/packages/mcp-server/src/__tests__/http-state-endpoint.test.ts b/packages/mcp-server/src/__tests__/http-state-endpoint.test.ts new file mode 100644 index 0000000..14c407c --- /dev/null +++ b/packages/mcp-server/src/__tests__/http-state-endpoint.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { createHttpServer } from "../http-server.js"; +import type { StateFile } from "../singleton/types.js"; + +describe("HTTP /state.json endpoint", () => { + it("returns the current state object when set", async () => { + const state: StateFile = { + version: 1, + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + startedAt: 1700000000000, + parentPid: 99, + parentAlive: true, + socketPath: null, + }; + const server = createHttpServer( + () => "", + () => state + ); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const port = (server.address() as { port: number }).port; + try { + const res = await fetch(`http://127.0.0.1:${port}/state.json`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual(state); + } finally { + server.close(); + } + }); + + it("returns 503 when no state is set", async () => { + const server = createHttpServer( + () => "", + () => null + ); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const port = (server.address() as { port: number }).port; + try { + const res = await fetch(`http://127.0.0.1:${port}/state.json`); + expect(res.status).toBe(503); + } finally { + server.close(); + } + }); +}); diff --git a/packages/mcp-server/src/__tests__/wait-for-reconnect.test.ts b/packages/mcp-server/src/__tests__/wait-for-reconnect.test.ts new file mode 100644 index 0000000..3feff43 --- /dev/null +++ b/packages/mcp-server/src/__tests__/wait-for-reconnect.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi } from "vitest"; +import type { IPluginBridge } from "@pluginos/shared"; +import { createPluginOSServer } from "../server.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; + +type ToolResult = { content: Array<{ type: string; text: string }>; isError?: boolean }; + +function makeBridge(isConnected: () => boolean): IPluginBridge { + return { + sendAndWait: vi.fn(), + getStatus: vi.fn().mockReturnValue({ + connected: isConnected(), + fileKey: "mock-file", + fileName: "Mock File", + currentPage: "Page 1", + port: 9500, + connectedFiles: 1, + }), + listFiles: vi.fn().mockReturnValue([]), + isConnected: vi.fn(isConnected), + } as unknown as IPluginBridge; +} + +async function setupClient(bridge: IPluginBridge) { + const server = createPluginOSServer(bridge); + const [c, s] = InMemoryTransport.createLinkedPair(); + await server.connect(s); + const client = new Client({ name: "t", version: "1" }); + await client.connect(c); + return client; +} + +describe("wait_for_reconnect tool", () => { + it("returns connected immediately when bridge is already connected", async () => { + const bridge = makeBridge(() => true); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "wait_for_reconnect", + arguments: { timeoutSec: 5 }, + })) as ToolResult; + expect(res.isError).toBeFalsy(); + const payload = JSON.parse(res.content[0].text); + expect(payload.connected).toBe(true); + expect(payload.waitedMs).toBeLessThan(700); + }); + + it("returns timeout response when bridge never connects", async () => { + const bridge = makeBridge(() => false); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "wait_for_reconnect", + arguments: { timeoutSec: 2 }, + })) as ToolResult; + expect(res.isError).toBe(true); + const payload = JSON.parse(res.content[0].text); + expect(payload.connected).toBe(false); + expect(payload.waitedMs).toBeGreaterThanOrEqual(2000); + }, 5000); + + it("returns connected when bridge connects mid-wait", async () => { + let connected = false; + const bridge = makeBridge(() => connected); + const client = await setupClient(bridge); + setTimeout(() => { + connected = true; + }, 500); + const res = (await client.callTool({ + name: "wait_for_reconnect", + arguments: { timeoutSec: 5 }, + })) as ToolResult; + expect(res.isError).toBeFalsy(); + const payload = JSON.parse(res.content[0].text); + expect(payload.connected).toBe(true); + expect(payload.waitedMs).toBeGreaterThanOrEqual(500); + expect(payload.waitedMs).toBeLessThan(1500); + }); +}); diff --git a/packages/mcp-server/src/http-server.ts b/packages/mcp-server/src/http-server.ts index 76c2486..68e8130 100644 --- a/packages/mcp-server/src/http-server.ts +++ b/packages/mcp-server/src/http-server.ts @@ -1,6 +1,10 @@ import { createServer, IncomingMessage, ServerResponse, Server } from "http"; +import type { StateFile } from "./singleton/types.js"; -export function createHttpServer(getUiContent: () => string): Server { +export function createHttpServer( + getUiContent: () => string, + getStateFile?: () => StateFile | null +): Server { const server = createServer((req: IncomingMessage, res: ServerResponse) => { // CORS for Figma plugin iframe res.setHeader("Access-Control-Allow-Origin", "*"); @@ -13,6 +17,23 @@ export function createHttpServer(getUiContent: () => string): Server { return; } + if (req.url === "/state.json" && req.method === "GET") { + if (!getStateFile) { + res.writeHead(503, { "Content-Type": "text/plain" }); + res.end("No state available"); + return; + } + const state = getStateFile(); + if (state === null) { + res.writeHead(503, { "Content-Type": "text/plain" }); + res.end("No state available"); + return; + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(state)); + return; + } + if (req.url === "/ui" || req.url === "/ui.html") { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 742eeed..d02c75a 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -5,6 +5,15 @@ import { createHttpServer } from "./http-server.js"; import { readFileSync, existsSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; +import { + acquireSingletonLock, + writeSingletonState, + clearSingletonState, + buildStateFile, + writeStateFile, +} from "./singleton/index.js"; +import { unlinkSync } from "node:fs"; +import type { SingletonInfo, StateFile } from "./singleton/index.js"; export { createPluginOSServer } from "./server.js"; export { @@ -34,16 +43,120 @@ function loadUiContent(): string { return "

PluginOS UI not found. Run: npm run build -w packages/bridge-plugin

"; } -async function main() { - // Re-read on every request so rebuilds land without restarting the server. - // ui.html is ~70KB; the tradeoff is worth the smoother dev loop and avoids - // stale UIs when users swap between local and published builds. - const httpServer = createHttpServer(() => loadUiContent()); +// Capture the original parent PID at module load. process.ppid returns 1 (init) +// on Unix after the actual parent dies due to re-parenting — checking against +// the captured initial PID detects the orphan condition reliably. +const INITIAL_PARENT_PID = process.ppid; + +let singletonInfo: SingletonInfo | null = null; +let currentParentAlive = true; +let parentLivenessInterval: NodeJS.Timeout | null = null; +let selfTerminateTimeout: NodeJS.Timeout | null = null; +let currentState: StateFile | null = null; + +const PARENT_LIVENESS_INTERVAL_MS = 10_000; +const ORPHAN_GRACE_MS = 30_000; + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code === "EPERM"; + } +} + +function registerShutdownHandlers(): void { + const cleanup = async (): Promise => { + if (singletonInfo) { + await clearSingletonState(singletonInfo); + } + if (parentLivenessInterval) { + clearInterval(parentLivenessInterval); + parentLivenessInterval = null; + } + if (selfTerminateTimeout) { + clearTimeout(selfTerminateTimeout); + selfTerminateTimeout = null; + } + }; + process.on("SIGTERM", async () => { + await cleanup(); + process.exit(0); + }); + process.on("SIGINT", async () => { + await cleanup(); + process.exit(0); + }); + process.on("exit", () => { + if (singletonInfo) { + try { + unlinkSync(singletonInfo.stateFilePath); + } catch { + // ignored + } + try { + unlinkSync(singletonInfo.pidFilePath); + } catch { + // ignored + } + } + }); +} + +async function startParentLivenessHeartbeat(initialState: StateFile): Promise { + parentLivenessInterval = setInterval(async () => { + if (!singletonInfo) return; + const alive = isProcessAlive(INITIAL_PARENT_PID); + if (alive !== currentParentAlive) { + currentParentAlive = alive; + const updated: StateFile = { ...initialState, parentAlive: alive }; + currentState = updated; + await writeStateFile(singletonInfo.stateFilePath, updated); + } + if (!alive && selfTerminateTimeout === null) { + console.error( + `[singleton] Parent PID ${initialState.parentPid} is dead. Self-terminating in ${ORPHAN_GRACE_MS / 1000}s.` + ); + selfTerminateTimeout = setTimeout(() => { + console.error("[singleton] Grace period elapsed. Exiting."); + process.exit(0); + }, ORPHAN_GRACE_MS); + } + }, PARENT_LIVENESS_INTERVAL_MS); +} + +async function main(): Promise { + singletonInfo = await acquireSingletonLock(); + if (singletonInfo.takeoverFromPid !== undefined) { + console.error(`PluginOS server: took over from PID ${singletonInfo.takeoverFromPid}`); + } + registerShutdownHandlers(); + + const httpServer = createHttpServer( + () => loadUiContent(), + () => currentState + ); const wsServer = new WebSocketPluginBridge({ httpServer }); const port = await wsServer.start(); console.error(`PluginOS WebSocket + HTTP server on port ${port}`); + // Read package version for state.json + const pkgPath = join(__dirname, "..", "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string }; + + const state = buildStateFile({ + pid: process.pid, + port, + serverVersion: pkg.version, + parentPid: INITIAL_PARENT_PID, + parentAlive: true, + }); + currentState = state; + await writeSingletonState(singletonInfo, state); + await startParentLivenessHeartbeat(state); + const mcpServer = createPluginOSServer(wsServer); const transport = new StdioServerTransport(); await mcpServer.connect(transport); diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 8e8e4be..8d91106 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -206,5 +206,69 @@ export function createPluginOSServer(bridge: IPluginBridge) { } ); + server.tool( + "wait_for_reconnect", + "Wait for the PluginOS Bridge plugin to reconnect after a disconnect. " + + "Returns when the bridge reports connected, or when timeoutSec elapses. " + + "Use this when a prior tool call returned 'No plugin connected' to gracefully " + + "wait for the user to relaunch the plugin instead of immediately failing back to chat.", + { + timeoutSec: z + .number() + .int() + .min(1) + .max(300) + .default(60) + .describe("Maximum seconds to wait. Default 60, max 300."), + }, + async ({ timeoutSec }) => { + const startedAt = Date.now(); + const startedHrTime = process.hrtime.bigint(); + const timeoutNs = BigInt(timeoutSec) * 1_000_000_000n; + + while (process.hrtime.bigint() - startedHrTime < timeoutNs) { + if (bridge.isConnected()) { + const status = bridge.getStatus(); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + connected: true, + waitedMs: Date.now() - startedAt, + fileName: status.fileName, + fileKey: status.fileKey, + }, + null, + 2 + ), + }, + ], + }; + } + await new Promise((r) => setTimeout(r, 500)); + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + connected: false, + waitedMs: Date.now() - startedAt, + timeoutSec, + }, + null, + 2 + ), + }, + ], + isError: true, + }; + } + ); + return server; } diff --git a/packages/mcp-server/src/singleton/__tests__/fixtures/mock-server.ts b/packages/mcp-server/src/singleton/__tests__/fixtures/mock-server.ts new file mode 100644 index 0000000..21a0cd5 --- /dev/null +++ b/packages/mcp-server/src/singleton/__tests__/fixtures/mock-server.ts @@ -0,0 +1,45 @@ +import { + acquireSingletonLock, + writeSingletonState, + buildStateFile, + clearSingletonState, +} from "../../index.js"; + +const stateDir = process.env.PLUGINOS_STATE_DIR; +if (!stateDir) { + console.error("PLUGINOS_STATE_DIR not set"); + process.exit(2); +} + +async function main(): Promise { + const info = await acquireSingletonLock({ stateDir }); + const state = buildStateFile({ + pid: process.pid, + port: 9500, + serverVersion: "test", + parentPid: process.ppid, + parentAlive: true, + }); + await writeSingletonState(info, state); + + if (process.send) { + process.send({ ready: true, takeoverFromPid: info.takeoverFromPid }); + } + + await new Promise((resolve) => { + // keepalive timer — without an active handle Node exits immediately + const keepalive = setInterval(() => {}, 60_000); + + process.on("SIGTERM", async () => { + clearInterval(keepalive); + await clearSingletonState(info); + resolve(); + process.exit(0); + }); + }); +} + +main().catch((err) => { + console.error("mock-server fatal:", err); + process.exit(1); +}); diff --git a/packages/mcp-server/src/singleton/__tests__/integration.test.ts b/packages/mcp-server/src/singleton/__tests__/integration.test.ts new file mode 100644 index 0000000..ad26e4e --- /dev/null +++ b/packages/mcp-server/src/singleton/__tests__/integration.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { fork, ChildProcess } from "node:child_process"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const fixturePath = join(__dirname, "fixtures", "mock-server.ts"); + +interface ReadyMessage { + ready: boolean; + takeoverFromPid?: number; +} + +function spawnMockServer(stateDir: string): Promise<{ + proc: ChildProcess; + ready: ReadyMessage; +}> { + return new Promise((resolve, reject) => { + const proc = fork(fixturePath, { + env: { ...process.env, PLUGINOS_STATE_DIR: stateDir }, + execArgv: ["--import", "tsx"], + stdio: ["ignore", "pipe", "pipe", "ipc"], + }); + proc.once("error", reject); + proc.once("message", (msg) => resolve({ proc, ready: msg as ReadyMessage })); + }); +} + +function waitForExit(proc: ChildProcess, timeoutMs: number): Promise { + // If the process already exited, return immediately. + if (proc.exitCode !== null || proc.signalCode !== null) { + return Promise.resolve(proc.exitCode); + } + return new Promise((resolve) => { + let resolved = false; + proc.once("exit", (code) => { + if (!resolved) { + resolved = true; + resolve(code); + } + }); + setTimeout(() => { + if (!resolved) { + resolved = true; + resolve(null); + } + }, timeoutMs); + }); +} + +describe("singleton integration: two-process takeover", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-integ-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("second invocation reaps the first and reports takeoverFromPid", async () => { + const first = await spawnMockServer(dir); + expect(first.ready.ready).toBe(true); + expect(first.ready.takeoverFromPid).toBeUndefined(); + + const firstPid = first.proc.pid!; + + const second = await spawnMockServer(dir); + expect(second.ready.ready).toBe(true); + expect(second.ready.takeoverFromPid).toBe(firstPid); + + const firstExitCode = await waitForExit(first.proc, 3000); + expect(firstExitCode).not.toBeNull(); + + const pidContent = (await readFile(join(dir, "server.pid"), "utf8")).trim(); + expect(Number.parseInt(pidContent, 10)).toBe(second.proc.pid); + + second.proc.kill("SIGTERM"); + await waitForExit(second.proc, 3000); + }, 15000); + + it("a fresh start with no prior state has no takeoverFromPid", async () => { + const one = await spawnMockServer(dir); + expect(one.ready.takeoverFromPid).toBeUndefined(); + one.proc.kill("SIGTERM"); + await waitForExit(one.proc, 3000); + }, 10000); +}); diff --git a/packages/mcp-server/src/singleton/__tests__/lockfile.test.ts b/packages/mcp-server/src/singleton/__tests__/lockfile.test.ts new file mode 100644 index 0000000..7b660d7 --- /dev/null +++ b/packages/mcp-server/src/singleton/__tests__/lockfile.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { acquireLock, releaseLock } from "../lockfile.js"; + +describe("lockfile primitive", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-lock-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("acquires a lock on a fresh path", async () => { + const lockPath = join(dir, "server.pid.lock"); + const result = await acquireLock(lockPath); + expect(result.acquired).toBe(true); + expect(result.oldPid).toBeNull(); + }); + + it("fails to acquire when held by a live PID", async () => { + const lockPath = join(dir, "server.pid.lock"); + await acquireLock(lockPath); + const result = await acquireLock(lockPath, { maxRetries: 1, retryDelayMs: 10 }); + expect(result.acquired).toBe(false); + expect(result.oldPid).toBe(process.pid); + }); + + it("releases the lock", async () => { + const lockPath = join(dir, "server.pid.lock"); + await acquireLock(lockPath); + await releaseLock(lockPath); + const result = await acquireLock(lockPath); + expect(result.acquired).toBe(true); + }); + + it("treats a lockfile with a dead PID as stale and takes over", async () => { + const lockPath = join(dir, "server.pid.lock"); + const { writeFileSync } = await import("node:fs"); + writeFileSync(lockPath, "999999999"); + const result = await acquireLock(lockPath, { maxRetries: 1, retryDelayMs: 10 }); + expect(result.acquired).toBe(true); + expect(result.oldPid).toBe(999999999); + }); +}); diff --git a/packages/mcp-server/src/singleton/__tests__/orchestrator.test.ts b/packages/mcp-server/src/singleton/__tests__/orchestrator.test.ts new file mode 100644 index 0000000..f381dfc --- /dev/null +++ b/packages/mcp-server/src/singleton/__tests__/orchestrator.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { acquireSingletonLock } from "../index.js"; + +describe("acquireSingletonLock", () => { + let stateDir: string; + + beforeEach(async () => { + stateDir = await mkdtemp(join(tmpdir(), "pluginos-orch-test-")); + }); + + afterEach(async () => { + await rm(stateDir, { recursive: true, force: true }); + }); + + it("acquires on a fresh dir with no prior server", async () => { + const info = await acquireSingletonLock({ stateDir }); + expect(info.takeoverFromPid).toBeUndefined(); + expect(info.stateDir).toBe(stateDir); + expect(info.pidFilePath).toBe(join(stateDir, "server.pid")); + expect(info.stateFilePath).toBe(join(stateDir, "state.json")); + expect(info.lockFilePath).toBe(join(stateDir, "server.pid.lock")); + }); + + it("reaps a stale PID and reports takeoverFromPid", async () => { + await writeFile(join(stateDir, "server.pid"), "999999998"); + const info = await acquireSingletonLock({ stateDir }); + expect(info.takeoverFromPid).toBe(999999998); + }); + + it("creates the state dir if missing", async () => { + const missingDir = join(stateDir, "nested", "pluginos"); + const info = await acquireSingletonLock({ stateDir: missingDir }); + expect(info.stateDir).toBe(missingDir); + }); + + it("returns a degraded info object when the state dir is not writable", async () => { + const badDir = "/dev/null/not-a-dir"; + const info = await acquireSingletonLock({ stateDir: badDir }); + expect(info.stateDir).toBe(badDir); + }); +}); diff --git a/packages/mcp-server/src/singleton/__tests__/pid-file.test.ts b/packages/mcp-server/src/singleton/__tests__/pid-file.test.ts new file mode 100644 index 0000000..65383a8 --- /dev/null +++ b/packages/mcp-server/src/singleton/__tests__/pid-file.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { writePidFile, readPidFile, removePidFile } from "../pid-file.js"; + +describe("pid-file r/w", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-pid-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("writes a pid atomically (tmp + rename)", async () => { + const path = join(dir, "server.pid"); + await writePidFile(path, 12345); + const read = await readPidFile(path); + expect(read).toBe(12345); + }); + + it("returns null for a missing file", async () => { + const path = join(dir, "missing.pid"); + expect(await readPidFile(path)).toBeNull(); + }); + + it("returns null for a corrupt file", async () => { + const path = join(dir, "corrupt.pid"); + await writeFile(path, "not-a-number"); + expect(await readPidFile(path)).toBeNull(); + }); + + it("removes the pid file", async () => { + const path = join(dir, "server.pid"); + await writePidFile(path, 42); + await removePidFile(path); + expect(await readPidFile(path)).toBeNull(); + }); + + it("remove is a no-op when file is missing", async () => { + const path = join(dir, "missing.pid"); + await expect(removePidFile(path)).resolves.toBeUndefined(); + }); +}); diff --git a/packages/mcp-server/src/singleton/__tests__/state-file.test.ts b/packages/mcp-server/src/singleton/__tests__/state-file.test.ts new file mode 100644 index 0000000..6a76582 --- /dev/null +++ b/packages/mcp-server/src/singleton/__tests__/state-file.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { buildStateFile, writeStateFile, readStateFile, removeStateFile } from "../state-file.js"; +import type { StateFile } from "../types.js"; + +describe("state-file", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-state-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("builds a state object with required fields", () => { + const state = buildStateFile({ + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + parentPid: 99, + parentAlive: true, + }); + expect(state.version).toBe(1); + expect(state.pid).toBe(1234); + expect(state.port).toBe(9500); + expect(state.serverVersion).toBe("0.4.3"); + expect(state.parentPid).toBe(99); + expect(state.parentAlive).toBe(true); + expect(state.socketPath).toBeNull(); + expect(typeof state.startedAt).toBe("number"); + }); + + it("writes atomically (tmp + rename) and reads back", async () => { + const path = join(dir, "state.json"); + const state: StateFile = buildStateFile({ + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + parentPid: 99, + parentAlive: true, + }); + await writeStateFile(path, state); + const read = await readStateFile(path); + expect(read).toEqual(state); + }); + + it("reads return null for missing files", async () => { + expect(await readStateFile(join(dir, "missing.json"))).toBeNull(); + }); + + it("reads return null for malformed files", async () => { + const path = join(dir, "malformed.json"); + await writeFile(path, "not-json"); + expect(await readStateFile(path)).toBeNull(); + }); + + it("reads return null for state with wrong version", async () => { + const path = join(dir, "future.json"); + await writeFile(path, JSON.stringify({ version: 999, pid: 1, port: 9500 })); + expect(await readStateFile(path)).toBeNull(); + }); + + it("removes the file", async () => { + const path = join(dir, "state.json"); + const state = buildStateFile({ + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + parentPid: 99, + parentAlive: true, + }); + await writeStateFile(path, state); + await removeStateFile(path); + expect(await readStateFile(path)).toBeNull(); + }); +}); diff --git a/packages/mcp-server/src/singleton/__tests__/takeover.test.ts b/packages/mcp-server/src/singleton/__tests__/takeover.test.ts new file mode 100644 index 0000000..47e5878 --- /dev/null +++ b/packages/mcp-server/src/singleton/__tests__/takeover.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, vi } from "vitest"; +import { reapProcess } from "../takeover.js"; + +describe("reapProcess", () => { + it("sends SIGTERM and returns true when process exits within grace", async () => { + const calls: Array<[number, NodeJS.Signals | 0]> = []; + let alive = true; + const kill = vi.fn((pid: number, sig: NodeJS.Signals | 0) => { + calls.push([pid, sig]); + if (sig === "SIGTERM") { + setTimeout(() => { + alive = false; + }, 50); + } + if (sig === 0 && !alive) { + const e = new Error("ESRCH") as NodeJS.ErrnoException; + e.code = "ESRCH"; + throw e; + } + return true; + }); + const result = await reapProcess(12345, { kill, graceMs: 500, pollMs: 25 }); + expect(result.reaped).toBe(true); + expect(result.usedSignal).toBe("SIGTERM"); + expect(calls.some(([, s]) => s === "SIGTERM")).toBe(true); + expect(calls.some(([, s]) => s === "SIGKILL")).toBe(false); + }); + + it("escalates to SIGKILL when SIGTERM doesn't take", async () => { + const calls: Array<[number, NodeJS.Signals | 0]> = []; + let alive = true; + const kill = vi.fn((pid: number, sig: NodeJS.Signals | 0) => { + calls.push([pid, sig]); + if (sig === "SIGKILL") { + // Process dies immediately on SIGKILL + alive = false; + } + if (sig === 0 && !alive) { + const e = new Error("ESRCH") as NodeJS.ErrnoException; + e.code = "ESRCH"; + throw e; + } + return true; + }); + const result = await reapProcess(12345, { kill, graceMs: 100, pollMs: 25 }); + expect(result.reaped).toBe(true); + expect(result.usedSignal).toBe("SIGKILL"); + expect(calls.some(([, s]) => s === "SIGTERM")).toBe(true); + expect(calls.some(([, s]) => s === "SIGKILL")).toBe(true); + }); + + it("returns reaped=false if the process never dies even after SIGKILL", async () => { + const kill = vi.fn(() => true); + const result = await reapProcess(12345, { kill, graceMs: 50, pollMs: 25, postKillWaitMs: 50 }); + expect(result.reaped).toBe(false); + }); +}); diff --git a/packages/mcp-server/src/singleton/index.ts b/packages/mcp-server/src/singleton/index.ts new file mode 100644 index 0000000..24a586a --- /dev/null +++ b/packages/mcp-server/src/singleton/index.ts @@ -0,0 +1,94 @@ +import { mkdir, chmod } from "node:fs/promises"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import { acquireLock, releaseLock } from "./lockfile.js"; +import { readPidFile, writePidFile, removePidFile } from "./pid-file.js"; +import { reapProcess } from "./takeover.js"; +import { writeStateFile, removeStateFile } from "./state-file.js"; +import type { SingletonInfo, StateFile } from "./types.js"; + +export { buildStateFile, writeStateFile, readStateFile, removeStateFile } from "./state-file.js"; +export { reapProcess } from "./takeover.js"; +export type { StateFile, SingletonInfo } from "./types.js"; + +export interface AcquireOptions { + stateDir?: string; +} + +function defaultStateDir(): string { + return process.env.PLUGINOS_STATE_DIR ?? join(homedir(), ".pluginos"); +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code === "EPERM"; + } +} + +export async function acquireSingletonLock(opts: AcquireOptions = {}): Promise { + const stateDir = opts.stateDir ?? defaultStateDir(); + const pidFilePath = join(stateDir, "server.pid"); + const stateFilePath = join(stateDir, "state.json"); + const lockFilePath = join(stateDir, "server.pid.lock"); + + try { + await mkdir(stateDir, { recursive: true }); + await chmod(stateDir, 0o700).catch(() => { + // chmod can fail on Windows or special FS — ignore + }); + } catch (err) { + console.error( + `[singleton] Failed to create state dir ${stateDir}: ${(err as Error).message}. Continuing in degraded mode.` + ); + return { stateDir, pidFilePath, stateFilePath, lockFilePath }; + } + + const lock = await acquireLock(lockFilePath); + if (!lock.acquired) { + console.error( + `[singleton] Could not acquire lock at ${lockFilePath} after retries — proceeding without singleton enforcement.` + ); + return { stateDir, pidFilePath, stateFilePath, lockFilePath }; + } + + let takeoverFromPid: number | undefined; + const oldPid = await readPidFile(pidFilePath); + if (oldPid === process.pid) { + // The PID file already points to us — nothing to reap. + await releaseLock(lockFilePath); + return { stateDir, pidFilePath, stateFilePath, lockFilePath }; + } + if (oldPid !== null && isProcessAlive(oldPid)) { + const result = await reapProcess(oldPid); + if (result.reaped) { + takeoverFromPid = oldPid; + console.error(`[singleton] Reaped PID ${oldPid} (signal: ${result.usedSignal}). Took over.`); + } else { + console.error( + `[singleton] Could not reap PID ${oldPid} — proceeding anyway. Port collision may occur.` + ); + } + } else if (oldPid !== null) { + takeoverFromPid = oldPid; + console.error(`[singleton] Found stale PID file (${oldPid} not alive). Took over.`); + } + + await releaseLock(lockFilePath); + return { takeoverFromPid, stateDir, pidFilePath, stateFilePath, lockFilePath }; +} + +export async function writeSingletonState(info: SingletonInfo, state: StateFile): Promise { + try { + await writePidFile(info.pidFilePath, state.pid); + await writeStateFile(info.stateFilePath, state); + } catch (err) { + console.error(`[singleton] Failed to write state files: ${(err as Error).message}`); + } +} + +export async function clearSingletonState(info: SingletonInfo): Promise { + await Promise.allSettled([removeStateFile(info.stateFilePath), removePidFile(info.pidFilePath)]); +} diff --git a/packages/mcp-server/src/singleton/lockfile.ts b/packages/mcp-server/src/singleton/lockfile.ts new file mode 100644 index 0000000..db13ab4 --- /dev/null +++ b/packages/mcp-server/src/singleton/lockfile.ts @@ -0,0 +1,75 @@ +import { open, readFile, unlink } from "node:fs/promises"; +import type { LockAcquisition } from "./types.js"; + +export interface AcquireOptions { + maxRetries?: number; + retryDelayMs?: number; +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code === "EPERM"; + } +} + +async function readPidFromLockfile(path: string): Promise { + try { + const content = (await readFile(path, "utf8")).trim(); + const pid = Number.parseInt(content, 10); + return Number.isFinite(pid) ? pid : null; + } catch { + return null; + } +} + +export async function acquireLock( + path: string, + opts: AcquireOptions = {} +): Promise { + const maxRetries = opts.maxRetries ?? 5; + const retryDelayMs = opts.retryDelayMs ?? 200; + let stalePid: number | null = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const fh = await open(path, "wx"); + try { + await fh.write(String(process.pid)); + } finally { + await fh.close(); + } + return { acquired: true, oldPid: stalePid }; + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err; + + const oldPid = await readPidFromLockfile(path); + if (oldPid !== null && !isProcessAlive(oldPid)) { + stalePid = oldPid; + try { + await unlink(path); + } catch { + // race with another process — proceed + } + continue; + } + + if (attempt === maxRetries) { + return { acquired: false, oldPid }; + } + await new Promise((r) => setTimeout(r, retryDelayMs)); + } + } + + return { acquired: false, oldPid: null }; +} + +export async function releaseLock(path: string): Promise { + try { + await unlink(path); + } catch { + // best-effort + } +} diff --git a/packages/mcp-server/src/singleton/pid-file.ts b/packages/mcp-server/src/singleton/pid-file.ts new file mode 100644 index 0000000..aa1854b --- /dev/null +++ b/packages/mcp-server/src/singleton/pid-file.ts @@ -0,0 +1,25 @@ +import { writeFile, readFile, rename, unlink } from "node:fs/promises"; + +export async function writePidFile(path: string, pid: number): Promise { + const tmp = `${path}.tmp`; + await writeFile(tmp, String(pid)); + await rename(tmp, path); +} + +export async function readPidFile(path: string): Promise { + try { + const content = (await readFile(path, "utf8")).trim(); + const pid = Number.parseInt(content, 10); + return Number.isFinite(pid) ? pid : null; + } catch { + return null; + } +} + +export async function removePidFile(path: string): Promise { + try { + await unlink(path); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; + } +} diff --git a/packages/mcp-server/src/singleton/state-file.ts b/packages/mcp-server/src/singleton/state-file.ts new file mode 100644 index 0000000..d610462 --- /dev/null +++ b/packages/mcp-server/src/singleton/state-file.ts @@ -0,0 +1,54 @@ +import { writeFile, readFile, rename, unlink } from "node:fs/promises"; +import type { StateFile } from "./types.js"; + +export interface BuildStateInput { + pid: number; + port: number; + serverVersion: string; + parentPid: number; + parentAlive: boolean; +} + +export function buildStateFile(input: BuildStateInput): StateFile { + return { + version: 1, + pid: input.pid, + port: input.port, + serverVersion: input.serverVersion, + startedAt: Date.now(), + parentPid: input.parentPid, + parentAlive: input.parentAlive, + socketPath: null, + }; +} + +export async function writeStateFile(path: string, state: StateFile): Promise { + const tmp = `${path}.tmp`; + await writeFile(tmp, JSON.stringify(state, null, 2)); + await rename(tmp, path); +} + +export async function readStateFile(path: string): Promise { + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON.parse(raw) as unknown; + if ( + typeof parsed === "object" && + parsed !== null && + (parsed as { version?: unknown }).version === 1 + ) { + return parsed as StateFile; + } + return null; + } catch { + return null; + } +} + +export async function removeStateFile(path: string): Promise { + try { + await unlink(path); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; + } +} diff --git a/packages/mcp-server/src/singleton/takeover.ts b/packages/mcp-server/src/singleton/takeover.ts new file mode 100644 index 0000000..140efe2 --- /dev/null +++ b/packages/mcp-server/src/singleton/takeover.ts @@ -0,0 +1,80 @@ +export interface ReapOptions { + kill?: (pid: number, signal: NodeJS.Signals | 0) => boolean; + graceMs?: number; + pollMs?: number; + postKillWaitMs?: number; +} + +export interface ReapResult { + reaped: boolean; + usedSignal: NodeJS.Signals | null; +} + +function defaultKill(pid: number, signal: NodeJS.Signals | 0): boolean { + return process.kill(pid, signal as NodeJS.Signals); +} + +function isAlive(pid: number, kill: (pid: number, signal: 0) => boolean): boolean { + try { + kill(pid, 0); + return true; + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code === "EPERM") return true; + if (code === "ESRCH") return false; + return false; + } +} + +async function pollUntilDead( + pid: number, + kill: (pid: number, signal: 0) => boolean, + timeoutMs: number, + pollMs: number +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (!isAlive(pid, kill)) return true; + await new Promise((r) => setTimeout(r, pollMs)); + } + return !isAlive(pid, kill); +} + +export async function reapProcess(pid: number, opts: ReapOptions = {}): Promise { + if (pid === process.pid) { + return { reaped: false, usedSignal: null }; + } + const kill = opts.kill ?? defaultKill; + const graceMs = opts.graceMs ?? 1000; + const pollMs = opts.pollMs ?? 100; + const postKillWaitMs = opts.postKillWaitMs ?? 200; + + try { + kill(pid, "SIGTERM"); + } catch { + // process may already be dead — that's fine + } + + const diedFromSigterm = await pollUntilDead( + pid, + (p, s) => kill(p, s as NodeJS.Signals | 0), + graceMs, + pollMs + ); + if (diedFromSigterm) { + return { reaped: true, usedSignal: "SIGTERM" }; + } + + try { + kill(pid, "SIGKILL"); + } catch { + // proceed + } + const diedFromSigkill = await pollUntilDead( + pid, + (p, s) => kill(p, s as NodeJS.Signals | 0), + postKillWaitMs, + pollMs + ); + return { reaped: diedFromSigkill, usedSignal: diedFromSigkill ? "SIGKILL" : null }; +} diff --git a/packages/mcp-server/src/singleton/types.ts b/packages/mcp-server/src/singleton/types.ts new file mode 100644 index 0000000..73d8dac --- /dev/null +++ b/packages/mcp-server/src/singleton/types.ts @@ -0,0 +1,25 @@ +// packages/mcp-server/src/singleton/types.ts + +export interface StateFile { + version: 1; + pid: number; + port: number; + serverVersion: string; + startedAt: number; + parentPid: number; + parentAlive: boolean; + socketPath: string | null; +} + +export interface SingletonInfo { + takeoverFromPid?: number; + stateDir: string; + pidFilePath: string; + stateFilePath: string; + lockFilePath: string; +} + +export interface LockAcquisition { + acquired: boolean; + oldPid: number | null; +} diff --git a/packages/shared/package.json b/packages/shared/package.json index cc84225..24117b4 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -11,8 +11,8 @@ "test:coverage": "vitest run --coverage" }, "devDependencies": { - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" } }