diff --git a/INSTALL.md b/INSTALL.md index 370c8d1..12ad859 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -57,7 +57,7 @@ The MCP server runs whenever Claude Code is open. ## Verifying the install 1. Open the PluginOS Bridge plugin in Figma. The status pill should turn green ("Connected") within a few seconds. -2. In your agent, ask: "list available pluginos operations". You should get a list of 39 operations. +2. In your agent, ask: "list available pluginos operations". You should get a list of operations and their categories. --- diff --git a/README.md b/README.md index 16b939f..061be41 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The agent calls write operations to create frames, set text, and modify fills ``` Agent ── MCP (stdio) ──> PluginOS Server ── WebSocket ──> Bridge Plugin ──> Figma - 5 tools thin router localhost 26 operations full API + 5 tools thin router localhost many operations full API ~600 tokens routes by name ports 9500- executes locally figma.* per turn + params only 9510 returns summaries ``` 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/claude-plugin/__tests__/sync-recipes.test.ts b/packages/claude-plugin/__tests__/sync-recipes.test.ts new file mode 100644 index 0000000..b691e20 --- /dev/null +++ b/packages/claude-plugin/__tests__/sync-recipes.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest"; +import { generateRecipesSection } from "../scripts/sync-recipes.ts"; + +describe("sync-recipes generator", () => { + it("includes all five helpers", () => { + const section = generateRecipesSection(); + expect(section).toContain("PluginOS.createStyledText"); + expect(section).toContain("PluginOS.bindSpacing"); + expect(section).toContain("PluginOS.combineAsVariantsTiled"); + expect(section).toContain("PluginOS.tileTopLevel"); + expect(section).toContain("PluginOS.layoutSpaceBetween"); + }); + + it("starts with the section header", () => { + expect(generateRecipesSection()).toMatch(/^## Recipes for bulk-seed scripts/m); + }); +}); diff --git a/packages/claude-plugin/package.json b/packages/claude-plugin/package.json index eba7463..a333f72 100644 --- a/packages/claude-plugin/package.json +++ b/packages/claude-plugin/package.json @@ -5,12 +5,13 @@ "description": "Claude Code plugin for PluginOS — bundles MCP server registration and the pluginos-figma skill.", "scripts": { "sync-ops": "tsx scripts/sync-ops.ts", + "sync-recipes": "tsx scripts/sync-recipes.ts", "build": "npm run sync-ops", "test": "vitest run" }, "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/scripts/sync-recipes.ts b/packages/claude-plugin/scripts/sync-recipes.ts new file mode 100644 index 0000000..14e7d9b --- /dev/null +++ b/packages/claude-plugin/scripts/sync-recipes.ts @@ -0,0 +1,88 @@ +import { writeFileSync, readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +interface Recipe { + title: string; + dont: string; + doLine: string; + why: string; +} + +const RECIPES: Recipe[] = [ + { + title: "Styled text nodes", + dont: "createText + set fontName + set fontSize manually — leaves text unbound to styles.", + doLine: "PluginOS.createStyledText({ characters, textStyleId, fillStyleId, name })", + why: "load-font + create + bind-style + set-fill in one async call.", + }, + { + title: "Spacing variable bindings", + dont: "setBoundVariable per padding field — 5 lines per frame.", + doLine: "PluginOS.bindSpacing(node, { padding: spacingVar, itemSpacing: spacingVar })", + why: "binds all 4 padding fields + itemSpacing in one call. paddingX/Y/specific keys win over `padding`.", + }, + { + title: "Variant sets", + dont: "combineAsVariants then manually set layoutMode, wrap, sizing — variants stack at (0,0).", + doLine: "PluginOS.combineAsVariantsTiled(cells, parent, { cols, gutter })", + why: "applies HORIZONTAL + WRAP + FIXED width so variants render tiled instead of stacked.", + }, + { + title: "Top-level placement", + dont: "figma.createComponent() in a loop — every node lands at (0,0) and overlaps.", + doLine: + "const place = PluginOS.tileTopLevel(figma.currentPage, { cols, gutter }); place(node);", + why: "maintains a placement cursor across a single execute_figma call.", + }, + { + title: "SPACE_BETWEEN auto-layout", + dont: 'frame.itemSpacing = "AUTO" — runtime-rejected. primaryAxisAlignItems = "SPACE_BETWEEN" collapses on inspect.', + doLine: "PluginOS.layoutSpaceBetween(frame, { growChild })", + why: "sets primaryAxisAlignItems = MIN and gives the grow target layoutGrow=1 (or layoutSizingHorizontal=FILL for TEXT).", + }, +]; + +export function generateRecipesSection(): string { + const intro = `## Recipes for bulk-seed scripts + +These helpers are available inside every \`execute_figma\` script. They prevent the most common bulk-seed bugs. +`; + const body = RECIPES.map( + (r) => `### ${r.title} +Don't: ${r.dont} +Do: ${r.doLine} +Why: ${r.why} +` + ).join("\n"); + return intro + "\n" + body; +} + +const MARKER_START = ""; +const MARKER_END = ""; + +export function applyRecipes(skillContent: string, recipes: string): string { + const block = `${MARKER_START}\n${recipes}\n${MARKER_END}`; + if (skillContent.includes(MARKER_START) && skillContent.includes(MARKER_END)) { + return skillContent.replace(new RegExp(`${MARKER_START}[\\s\\S]*?${MARKER_END}`), block); + } + return `${skillContent.trimEnd()}\n\n${block}\n`; +} + +function main(): void { + const __dirname = dirname(fileURLToPath(import.meta.url)); + const path = resolve(__dirname, "..", "skills", "pluginos-figma", "SKILL.md"); + const before = readFileSync(path, "utf8"); + const after = applyRecipes(before, generateRecipesSection()); + if (before !== after) { + writeFileSync(path, after); + console.warn("sync-recipes: updated SKILL.md"); + } else { + console.warn("sync-recipes: SKILL.md already in sync"); + } +} + +const __filename = fileURLToPath(import.meta.url); +if (process.argv[1] && process.argv[1] === __filename) { + main(); +} diff --git a/packages/claude-plugin/skills/pluginos-figma/SKILL.md b/packages/claude-plugin/skills/pluginos-figma/SKILL.md index 9df2c55..03e335b 100644 --- a/packages/claude-plugin/skills/pluginos-figma/SKILL.md +++ b/packages/claude-plugin/skills/pluginos-figma/SKILL.md @@ -70,3 +70,41 @@ If any `pluginos.*` tool returns "No plugin connected" or times out: ## Operations quick-list Use the Read tool on `references/operations.md` for the full operations table (names, categories, default scopes, descriptions). Call `pluginos.list_operations` if the reference seems stale. + + + +## Recipes for bulk-seed scripts + +These helpers are available inside every `execute_figma` script. They prevent the most common bulk-seed bugs. + +### Styled text nodes + +Don't: createText + set fontName + set fontSize manually — leaves text unbound to styles. +Do: PluginOS.createStyledText({ characters, textStyleId, fillStyleId, name }) +Why: load-font + create + bind-style + set-fill in one async call. + +### Spacing variable bindings + +Don't: setBoundVariable per padding field — 5 lines per frame. +Do: PluginOS.bindSpacing(node, { padding: spacingVar, itemSpacing: spacingVar }) +Why: binds all 4 padding fields + itemSpacing in one call. paddingX/Y/specific keys win over `padding`. + +### Variant sets + +Don't: combineAsVariants then manually set layoutMode, wrap, sizing — variants stack at (0,0). +Do: PluginOS.combineAsVariantsTiled(cells, parent, { cols, gutter }) +Why: applies HORIZONTAL + WRAP + FIXED width so variants render tiled instead of stacked. + +### Top-level placement + +Don't: figma.createComponent() in a loop — every node lands at (0,0) and overlaps. +Do: const place = PluginOS.tileTopLevel(figma.currentPage, { cols, gutter }); place(node); +Why: maintains a placement cursor across a single execute_figma call. + +### SPACE_BETWEEN auto-layout + +Don't: frame.itemSpacing = "AUTO" — runtime-rejected. primaryAxisAlignItems = "SPACE_BETWEEN" collapses on inspect. +Do: PluginOS.layoutSpaceBetween(frame, { growChild }) +Why: sets primaryAxisAlignItems = MIN and gives the grow target layoutGrow=1 (or layoutSizingHorizontal=FILL for TEXT). + + 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__/execute-with-lint.test.ts b/packages/mcp-server/src/__tests__/execute-with-lint.test.ts new file mode 100644 index 0000000..6eb5f5b --- /dev/null +++ b/packages/mcp-server/src/__tests__/execute-with-lint.test.ts @@ -0,0 +1,80 @@ +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 createMockBridge(): IPluginBridge { + return { + sendAndWait: vi.fn().mockResolvedValue({ + id: "test", + type: "result", + success: true, + result: { foo: "bar" }, + }), + getStatus: vi.fn().mockReturnValue({ + connected: true, + fileKey: "mock-file", + fileName: "Mock File", + currentPage: "Page 1", + port: 9500, + connectedFiles: 1, + }), + listFiles: vi.fn().mockReturnValue([]), + isConnected: vi.fn().mockReturnValue(true), + }; +} + +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("execute_figma with lint + prelude", () => { + it("returns lint warnings alongside the result for a script using figma.notify", async () => { + const bridge = createMockBridge(); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "execute_figma", + arguments: { code: `figma.notify("hi"); return 1;` }, + })) as ToolResult; + expect(res.isError).toBeFalsy(); + const payload = JSON.parse(res.content[0].text); + expect(payload.result).toEqual({ foo: "bar" }); + expect(Array.isArray(payload.lint)).toBe(true); + expect(payload.lint.some((r: { ruleId: string }) => r.ruleId === "no-notify")).toBe(true); + expect(typeof payload.preludeVersion).toBe("string"); + expect(typeof payload.durationMs).toBe("number"); + }); + + it("sends the wrapped (prelude + user) script to the bridge", async () => { + const bridge = createMockBridge(); + const client = await setupClient(bridge); + await client.callTool({ + name: "execute_figma", + arguments: { code: `return PluginOS.version;` }, + }); + const sendMock = bridge.sendAndWait as ReturnType; + const [msg] = sendMock.mock.calls[0]; + expect(msg.type).toBe("execute"); + expect(msg.code).toContain("PluginOS"); + expect(msg.code).toContain("return PluginOS.version;"); + }); + + it("returns empty lint array for clean scripts", async () => { + const bridge = createMockBridge(); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "execute_figma", + arguments: { code: `return figma.currentPage.name;` }, + })) as ToolResult; + const payload = JSON.parse(res.content[0].text); + expect(payload.lint).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/__tests__/server-tools.test.ts b/packages/mcp-server/src/__tests__/server-tools.test.ts index 1ff30ed..d0cf74f 100644 --- a/packages/mcp-server/src/__tests__/server-tools.test.ts +++ b/packages/mcp-server/src/__tests__/server-tools.test.ts @@ -62,7 +62,8 @@ describe("list_operations tool", () => { arguments: {}, })) as ToolResult; const parsed = JSON.parse(result.content[0].text); - expect(parsed).toEqual(ops); + expect(parsed.operations).toEqual(ops); + expect(parsed.total).toBe(2); expect(result.isError).toBeFalsy(); await clientTransport.close(); @@ -123,6 +124,48 @@ describe("list_operations tool", () => { }); }); +// ─── list_operations total field ──────────────────────────────────── + +describe("list_operations tool total field", () => { + it("wraps the bridge result with operations + total", async () => { + const fakeManifests = [ + { name: "lint_styles", category: "lint", description: "x" }, + { name: "check_contrast", category: "accessibility", description: "y" }, + { name: "export_tokens", category: "tokens", description: "z" }, + ]; + const bridge = createMockBridge({ + sendAndWait: vi.fn().mockResolvedValue({ + id: "test", + type: "result", + success: true, + result: fakeManifests, + }), + }); + const { client } = await setupClientServer(bridge); + const res = (await client.callTool({ name: "list_operations", arguments: {} })) as ToolResult; + expect(res.isError).toBeFalsy(); + const payload = JSON.parse(res.content[0].text); + expect(payload.total).toBe(3); + expect(payload.operations).toEqual(fakeManifests); + }); + + it("preserves total when bridge result is already an empty array", async () => { + const bridge = createMockBridge({ + sendAndWait: vi.fn().mockResolvedValue({ + id: "test", + type: "result", + success: true, + result: [], + }), + }); + const { client } = await setupClientServer(bridge); + const res = (await client.callTool({ name: "list_operations", arguments: {} })) as ToolResult; + const payload = JSON.parse(res.content[0].text); + expect(payload.total).toBe(0); + expect(payload.operations).toEqual([]); + }); +}); + // ─── run_operation ────────────────────────────────────────────────── describe("run_operation tool", () => { @@ -263,7 +306,7 @@ describe("execute_figma tool", () => { })) as ToolResult; const parsed = JSON.parse(result.content[0].text); - expect(parsed.nodeCount).toBe(42); + expect(parsed.result.nodeCount).toBe(42); expect(result.isError).toBeFalsy(); await clientTransport.close(); diff --git a/packages/mcp-server/src/lint/__tests__/invalid-variable-name.test.ts b/packages/mcp-server/src/lint/__tests__/invalid-variable-name.test.ts new file mode 100644 index 0000000..f9c501a --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/invalid-variable-name.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { invalidVariableNameRule } from "../rules/invalid-variable-name.js"; + +describe("invalid-variable-name rule", () => { + it("flags dot in variable name", () => { + const code = `figma.variables.createVariable("Spacing/1.5", coll, "FLOAT");`; + const results = invalidVariableNameRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + expect(results[0].message).toContain("."); + expect(results[0].fix).toContain("1_5"); + }); + + it("flags hyphen in variable name", () => { + const code = `figma.variables.createVariable("body-medium", coll, "STRING");`; + const results = invalidVariableNameRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].fix).toContain("body_medium"); + }); + + it("flags space in variable name", () => { + const code = `figma.variables.createVariable("body text", coll, "STRING");`; + expect(invalidVariableNameRule.check(code)).toHaveLength(1); + }); + + it("does not flag valid names", () => { + expect( + invalidVariableNameRule.check(`figma.variables.createVariable("Spacing_1_5", coll, "FLOAT");`) + ).toEqual([]); + expect( + invalidVariableNameRule.check(`figma.variables.createVariable("h1", coll, "STRING");`) + ).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/lint/__tests__/no-hyphenated-plugindata-key.test.ts b/packages/mcp-server/src/lint/__tests__/no-hyphenated-plugindata-key.test.ts new file mode 100644 index 0000000..b2eb211 --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/no-hyphenated-plugindata-key.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import { noHyphenatedPluginDataKeyRule } from "../rules/no-hyphenated-plugindata-key.js"; + +describe("no-hyphenated-plugindata-key rule", () => { + it("flags hyphen in setPluginData key", () => { + const code = `figma.root.setPluginData("my-key", "value");`; + const results = noHyphenatedPluginDataKeyRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + expect(results[0].fix).toContain("my_key"); + }); + + it("flags hyphen in setSharedPluginData key", () => { + const code = `node.setSharedPluginData("ns", "shared-key", "v");`; + expect(noHyphenatedPluginDataKeyRule.check(code)).toHaveLength(1); + }); + + it("does not flag valid keys", () => { + expect(noHyphenatedPluginDataKeyRule.check(`figma.root.setPluginData("my_key", "v");`)).toEqual( + [] + ); + }); +}); diff --git a/packages/mcp-server/src/lint/__tests__/no-itemspacing-auto.test.ts b/packages/mcp-server/src/lint/__tests__/no-itemspacing-auto.test.ts new file mode 100644 index 0000000..a3b3982 --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/no-itemspacing-auto.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { noItemSpacingAutoRule } from "../rules/no-itemspacing-auto.js"; + +describe("no-itemspacing-auto rule", () => { + it.each([ + `frame.itemSpacing = "AUTO";`, + `frame.itemSpacing = 'AUTO';`, + `{ itemSpacing: "AUTO" }`, + `{itemSpacing:'AUTO'}`, + ])("flags %s", (code) => { + const results = noItemSpacingAutoRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + }); + + it("does not flag numeric itemSpacing", () => { + expect(noItemSpacingAutoRule.check(`frame.itemSpacing = 16;`)).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/lint/__tests__/no-notify.test.ts b/packages/mcp-server/src/lint/__tests__/no-notify.test.ts new file mode 100644 index 0000000..63ed37f --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/no-notify.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { noNotifyRule } from "../rules/no-notify.js"; + +describe("no-notify rule", () => { + it("flags figma.notify calls", () => { + const results = noNotifyRule.check(`figma.notify("hi");`); + expect(results).toHaveLength(1); + expect(results[0].ruleId).toBe("no-notify"); + expect(results[0].severity).toBe("error"); + expect(results[0].line).toBe(1); + }); + + it("flags figma.notify on later line with correct line number", () => { + const code = `const x = 1;\nconst y = 2;\nfigma.notify("hi");`; + const results = noNotifyRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].line).toBe(3); + }); + + it("does not flag .notify on other objects", () => { + expect(noNotifyRule.check(`emitter.notify("hi");`)).toEqual([]); + }); + + it("does not flag in code without notify", () => { + expect(noNotifyRule.check(`const x = 1;`)).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/lint/__tests__/no-sync-style-setters.test.ts b/packages/mcp-server/src/lint/__tests__/no-sync-style-setters.test.ts new file mode 100644 index 0000000..174b71a --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/no-sync-style-setters.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from "vitest"; +import { noSyncStyleSettersRule } from "../rules/no-sync-style-setters.js"; + +describe("no-sync-style-setters rule", () => { + it.each([ + ["node.fillStyleId = id;", "fillStyleId"], + ["foo.textStyleId = '123';", "textStyleId"], + ["x.strokeStyleId='abc'", "strokeStyleId"], + ["a.effectStyleId = 'eee';", "effectStyleId"], + ["b.gridStyleId='ggg';", "gridStyleId"], + ])("flags %s", (code, field) => { + const results = noSyncStyleSettersRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("warn"); + expect(results[0].message).toContain(field); + }); + + it("does not flag async setters", () => { + expect(noSyncStyleSettersRule.check(`await node.setFillStyleIdAsync(id);`)).toEqual([]); + }); + + it("does not flag reads", () => { + expect(noSyncStyleSettersRule.check(`const id = node.fillStyleId;`)).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/lint/__tests__/no-text-encoders.test.ts b/packages/mcp-server/src/lint/__tests__/no-text-encoders.test.ts new file mode 100644 index 0000000..2a366f1 --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/no-text-encoders.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { noTextEncodersRule } from "../rules/no-text-encoders.js"; + +describe("no-text-encoders rule", () => { + it.each([ + [`new TextEncoder().encode("x")`, "TextEncoder"], + [`const d = new TextDecoder();`, "TextDecoder"], + [`await crypto.subtle.digest("SHA-256", buf);`, "crypto.subtle"], + ])("flags %s", (code, mention) => { + const results = noTextEncodersRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + expect(results[0].message).toContain(mention); + }); + + it("does not flag arbitrary code", () => { + expect(noTextEncodersRule.check(`const x = "encoder";`)).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/lint/__tests__/prefer-helpers.test.ts b/packages/mcp-server/src/lint/__tests__/prefer-helpers.test.ts new file mode 100644 index 0000000..0758179 --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/prefer-helpers.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { preferHelpersRule } from "../rules/prefer-helpers.js"; + +describe("prefer-helpers rule", () => { + it("hints createStyledText when createText + loadFontAsync co-occur", () => { + const code = ` +await figma.loadFontAsync({ family: "Inter", style: "Regular" }); +const t = figma.createText(); +t.characters = "hi"; +`; + const results = preferHelpersRule.check(code); + expect(results.some((r) => r.message.includes("createStyledText"))).toBe(true); + expect(results.every((r) => r.severity === "hint")).toBe(true); + }); + + it("hints bindSpacing when 3+ padding bindings are set", () => { + const code = ` +node.setBoundVariable("paddingTop", v); +node.setBoundVariable("paddingBottom", v); +node.setBoundVariable("paddingLeft", v); +`; + const results = preferHelpersRule.check(code); + expect(results.some((r) => r.message.includes("bindSpacing"))).toBe(true); + }); + + it("does not hint for unrelated code", () => { + expect(preferHelpersRule.check(`const x = 1;`)).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/lint/__tests__/registry.test.ts b/packages/mcp-server/src/lint/__tests__/registry.test.ts new file mode 100644 index 0000000..b5770d5 --- /dev/null +++ b/packages/mcp-server/src/lint/__tests__/registry.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { runLint, registerRule } from "../index.js"; +import type { LintRule } from "../types.js"; + +describe("lint registry", () => { + it("returns empty array when no rules registered match", () => { + expect(runLint("const x = 1;")).toEqual([]); + }); + + it("aggregates results from all rules", () => { + const dummy: LintRule = { + id: "dummy", + severity: "error", + check: () => [{ ruleId: "dummy", severity: "error", line: 1, message: "x" }], + }; + registerRule(dummy); + const results = runLint("anything"); + expect(results.some((r) => r.ruleId === "dummy")).toBe(true); + }); +}); + +describe("default ruleset wired", () => { + it("flags figma.notify via runLint", () => { + const results = runLint(`figma.notify("hi")`); + expect(results.some((r) => r.ruleId === "no-notify")).toBe(true); + }); + + it("flags itemSpacing AUTO via runLint", () => { + const results = runLint(`frame.itemSpacing = "AUTO"`); + expect(results.some((r) => r.ruleId === "no-itemspacing-auto")).toBe(true); + }); +}); diff --git a/packages/mcp-server/src/lint/index.ts b/packages/mcp-server/src/lint/index.ts new file mode 100644 index 0000000..b4e2264 --- /dev/null +++ b/packages/mcp-server/src/lint/index.ts @@ -0,0 +1,35 @@ +import type { LintResult, LintRule } from "./types.js"; +import { noNotifyRule } from "./rules/no-notify.js"; +import { noSyncStyleSettersRule } from "./rules/no-sync-style-setters.js"; +import { noItemSpacingAutoRule } from "./rules/no-itemspacing-auto.js"; +import { invalidVariableNameRule } from "./rules/invalid-variable-name.js"; +import { noHyphenatedPluginDataKeyRule } from "./rules/no-hyphenated-plugindata-key.js"; +import { noTextEncodersRule } from "./rules/no-text-encoders.js"; +import { preferHelpersRule } from "./rules/prefer-helpers.js"; + +const rules: LintRule[] = []; + +export function registerRule(rule: LintRule): void { + rules.push(rule); +} + +export function runLint(code: string): LintResult[] { + const out: LintResult[] = []; + for (const rule of rules) { + for (const result of rule.check(code)) { + out.push(result); + } + } + return out; +} + +// Register default ruleset +registerRule(noNotifyRule); +registerRule(noSyncStyleSettersRule); +registerRule(noItemSpacingAutoRule); +registerRule(invalidVariableNameRule); +registerRule(noHyphenatedPluginDataKeyRule); +registerRule(noTextEncodersRule); +registerRule(preferHelpersRule); + +export type { LintResult, LintRule, LintSeverity } from "./types.js"; diff --git a/packages/mcp-server/src/lint/rules/invalid-variable-name.ts b/packages/mcp-server/src/lint/rules/invalid-variable-name.ts new file mode 100644 index 0000000..3d0b38a --- /dev/null +++ b/packages/mcp-server/src/lint/rules/invalid-variable-name.ts @@ -0,0 +1,33 @@ +import type { LintRule, LintResult } from "../types.js"; + +const CALL = /createVariable\s*\(\s*["']([^"']+)["']/g; +const VALID = /^[A-Za-z0-9_/]+$/; + +export const invalidVariableNameRule: LintRule = { + id: "invalid-variable-name", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + CALL.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = CALL.exec(line)) !== null) { + const name = m[1]; + if (!VALID.test(name)) { + const bad = [...name].find((ch) => !/[A-Za-z0-9_/]/.test(ch)) ?? "?"; + const sanitized = name.replace(/[^A-Za-z0-9_/]/g, "_"); + out.push({ + ruleId: "invalid-variable-name", + severity: "error", + line: i + 1, + message: `Variable name "${name}" contains invalid character "${bad}". Use [A-Za-z0-9_] (slashes allowed for nesting).`, + fix: sanitized, + }); + } + } + } + return out; + }, +}; diff --git a/packages/mcp-server/src/lint/rules/no-hyphenated-plugindata-key.ts b/packages/mcp-server/src/lint/rules/no-hyphenated-plugindata-key.ts new file mode 100644 index 0000000..c98f2df --- /dev/null +++ b/packages/mcp-server/src/lint/rules/no-hyphenated-plugindata-key.ts @@ -0,0 +1,33 @@ +import type { LintRule, LintResult } from "../types.js"; + +const SET_PLUGIN_DATA = /setPluginData\s*\(\s*["']([^"']+)["']/g; +const SET_SHARED = /setSharedPluginData\s*\(\s*["'][^"']*["']\s*,\s*["']([^"']+)["']/g; + +export const noHyphenatedPluginDataKeyRule: LintRule = { + id: "no-hyphenated-plugindata-key", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + for (const pattern of [SET_PLUGIN_DATA, SET_SHARED]) { + pattern.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = pattern.exec(line)) !== null) { + const key = m[1]; + if (key.includes("-")) { + out.push({ + ruleId: "no-hyphenated-plugindata-key", + severity: "error", + line: i + 1, + message: `Plugin data key "${key}" contains a hyphen. Use underscore: "${key.replace(/-/g, "_")}".`, + fix: key.replace(/-/g, "_"), + }); + } + } + } + } + return out; + }, +}; diff --git a/packages/mcp-server/src/lint/rules/no-itemspacing-auto.ts b/packages/mcp-server/src/lint/rules/no-itemspacing-auto.ts new file mode 100644 index 0000000..0d99283 --- /dev/null +++ b/packages/mcp-server/src/lint/rules/no-itemspacing-auto.ts @@ -0,0 +1,24 @@ +import type { LintRule, LintResult } from "../types.js"; + +const PATTERN = /itemSpacing\s*[:=]\s*["']AUTO["']/; + +export const noItemSpacingAutoRule: LintRule = { + id: "no-itemspacing-auto", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (PATTERN.test(lines[i])) { + out.push({ + ruleId: "no-itemspacing-auto", + severity: "error", + line: i + 1, + message: + 'itemSpacing = "AUTO" is rejected at runtime. Use PluginOS.layoutSpaceBetween(frame, { growChild }) instead.', + }); + } + } + return out; + }, +}; diff --git a/packages/mcp-server/src/lint/rules/no-notify.ts b/packages/mcp-server/src/lint/rules/no-notify.ts new file mode 100644 index 0000000..daa8047 --- /dev/null +++ b/packages/mcp-server/src/lint/rules/no-notify.ts @@ -0,0 +1,23 @@ +import type { LintRule, LintResult } from "../types.js"; + +const PATTERN = /\bfigma\.notify\s*\(/; + +export const noNotifyRule: LintRule = { + id: "no-notify", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (PATTERN.test(lines[i])) { + out.push({ + ruleId: "no-notify", + severity: "error", + line: i + 1, + message: "figma.notify() is forbidden in the plugin sandbox. Remove the call.", + }); + } + } + return out; + }, +}; diff --git a/packages/mcp-server/src/lint/rules/no-sync-style-setters.ts b/packages/mcp-server/src/lint/rules/no-sync-style-setters.ts new file mode 100644 index 0000000..4746253 --- /dev/null +++ b/packages/mcp-server/src/lint/rules/no-sync-style-setters.ts @@ -0,0 +1,27 @@ +import type { LintRule, LintResult } from "../types.js"; + +const FIELDS = ["fillStyleId", "textStyleId", "strokeStyleId", "effectStyleId", "gridStyleId"]; +const PATTERN = new RegExp(`\\.(${FIELDS.join("|")})\\s*=(?!=)`); + +export const noSyncStyleSettersRule: LintRule = { + id: "no-sync-style-setters", + severity: "warn", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + const m = lines[i].match(PATTERN); + if (m) { + const field = m[1]; + out.push({ + ruleId: "no-sync-style-setters", + severity: "warn", + line: i + 1, + message: `Sync setter '.${field} = ...' is deprecated. Use .set${field.charAt(0).toUpperCase() + field.slice(1)}Async(...).`, + fix: `await node.set${field.charAt(0).toUpperCase() + field.slice(1)}Async(...)`, + }); + } + } + return out; + }, +}; diff --git a/packages/mcp-server/src/lint/rules/no-text-encoders.ts b/packages/mcp-server/src/lint/rules/no-text-encoders.ts new file mode 100644 index 0000000..05d7c9d --- /dev/null +++ b/packages/mcp-server/src/lint/rules/no-text-encoders.ts @@ -0,0 +1,29 @@ +import type { LintRule, LintResult } from "../types.js"; + +const PATTERNS: Array<[RegExp, string]> = [ + [/\bTextEncoder\b/, "TextEncoder"], + [/\bTextDecoder\b/, "TextDecoder"], + [/\bcrypto\.subtle\b/, "crypto.subtle"], +]; + +export const noTextEncodersRule: LintRule = { + id: "no-text-encoders", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + for (const [pattern, name] of PATTERNS) { + if (pattern.test(lines[i])) { + out.push({ + ruleId: "no-text-encoders", + severity: "error", + line: i + 1, + message: `${name} is unavailable in the Figma plugin sandbox. Compute via plain JS string/array operations.`, + }); + } + } + } + return out; + }, +}; diff --git a/packages/mcp-server/src/lint/rules/prefer-helpers.ts b/packages/mcp-server/src/lint/rules/prefer-helpers.ts new file mode 100644 index 0000000..d091c5f --- /dev/null +++ b/packages/mcp-server/src/lint/rules/prefer-helpers.ts @@ -0,0 +1,53 @@ +import type { LintRule, LintResult } from "../types.js"; + +const PADDING_FIELDS = [ + "paddingTop", + "paddingBottom", + "paddingLeft", + "paddingRight", + "itemSpacing", +]; +const PADDING_REGEXES: Array = PADDING_FIELDS.map( + (field) => new RegExp(`setBoundVariable\\s*\\(\\s*["']${field}["']`) +); + +export const preferHelpersRule: LintRule = { + id: "prefer-helpers", + severity: "hint", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + const hasCreateText = /\bfigma\.createText\s*\(/.test(code); + const hasLoadFont = /\bfigma\.loadFontAsync\s*\(/.test(code); + if (hasCreateText && hasLoadFont) { + const idx = lines.findIndex((l) => /\bfigma\.createText\s*\(/.test(l)); + out.push({ + ruleId: "prefer-helpers", + severity: "hint", + line: idx >= 0 ? idx + 1 : 1, + message: + "Consider PluginOS.createStyledText({ characters, textStyleId, family, weight, size, fillStyleId, name }) — handles font load + create + style binding in one call.", + }); + } + let paddingCount = 0; + let firstPaddingLine = -1; + for (let i = 0; i < lines.length; i++) { + for (const pattern of PADDING_REGEXES) { + if (pattern.test(lines[i])) { + paddingCount++; + if (firstPaddingLine === -1) firstPaddingLine = i + 1; + } + } + } + if (paddingCount >= 3) { + out.push({ + ruleId: "prefer-helpers", + severity: "hint", + line: firstPaddingLine, + message: + "Consider PluginOS.bindSpacing(node, { padding, itemSpacing }) — binds all padding/itemSpacing fields in one call.", + }); + } + return out; + }, +}; diff --git a/packages/mcp-server/src/lint/types.ts b/packages/mcp-server/src/lint/types.ts new file mode 100644 index 0000000..3928d8d --- /dev/null +++ b/packages/mcp-server/src/lint/types.ts @@ -0,0 +1,16 @@ +// packages/mcp-server/src/lint/types.ts +export type LintSeverity = "error" | "warn" | "hint"; + +export interface LintResult { + ruleId: string; + severity: LintSeverity; + line: number; + message: string; + fix?: string; +} + +export interface LintRule { + id: string; + severity: LintSeverity; + check(code: string): LintResult[]; +} diff --git a/packages/mcp-server/src/prelude/__tests__/helpers.test.ts b/packages/mcp-server/src/prelude/__tests__/helpers.test.ts new file mode 100644 index 0000000..8107882 --- /dev/null +++ b/packages/mcp-server/src/prelude/__tests__/helpers.test.ts @@ -0,0 +1,259 @@ +import { describe, it, expect } from "vitest"; +import vm from "node:vm"; +import { wrapScript } from "../index.js"; + +function makeContext(figma: object) { + const ctx: Record = { figma, console }; + vm.createContext(ctx); + return ctx; +} + +function runWith(figma: object, userJs: string): Record { + const ctx = makeContext(figma); + const { wrapped } = wrapScript(`${userJs}\nglobalThis.__out = out;`); + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + return ctx; +} + +describe("createStyledText", () => { + it("calls loadFontAsync, createText, setTextStyleIdAsync, and sets characters", async () => { + const calls: string[] = []; + const textNode: Record = { + setTextStyleIdAsync: async (id: string) => { + calls.push("setTextStyle:" + id); + }, + setFillStyleIdAsync: async (id: string) => { + calls.push("setFill:" + id); + }, + }; + const figma = { + loadFontAsync: async (font: { family: string; style: string }) => { + calls.push(`loadFont:${font.family}/${font.style}`); + }, + createText: () => { + calls.push("createText"); + return textNode; + }, + }; + const userJs = ` +const out = await PluginOS.createStyledText({ + characters: "Hello", + family: "Inter", + weight: "Bold", + size: 16, + textStyleId: "tsid", + fillStyleId: "fsid", + name: "Title", +}); +`; + const ctx = runWith(figma, userJs); + await new Promise((r) => setTimeout(r, 10)); + expect(calls).toContain("loadFont:Inter/Bold"); + expect(calls).toContain("createText"); + expect(calls).toContain("setTextStyle:tsid"); + expect(calls).toContain("setFill:fsid"); + expect(textNode.characters).toBe("Hello"); + expect(textNode.name).toBe("Title"); + expect(ctx.__out).toBe(textNode); + }); + + it("throws when neither textStyleId nor family+size is provided", async () => { + const figma = { loadFontAsync: async () => {}, createText: () => ({}) }; + const ctx = makeContext(figma); + const { wrapped } = wrapScript(` +try { + await PluginOS.createStyledText({ characters: "x" }); + globalThis.__err = null; +} catch (e) { + globalThis.__err = String(e.message); +} +`); + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(String(ctx.__err)).toContain("[PluginOS.createStyledText]"); + }); +}); + +describe("bindSpacing", () => { + it("binds all four padding fields when given `padding`", async () => { + const bound: Array<[string, string]> = []; + const node = { + layoutMode: "VERTICAL", + setBoundVariable: (field: string, v: { id: string }) => bound.push([field, v.id]), + }; + const figma = {}; + const ctx = makeContext(figma); + const { wrapped } = wrapScript(` +const node = globalThis.__node; +await PluginOS.bindSpacing(node, { padding: { id: "v1" } }); +`); + (ctx as Record).__node = node; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + const fields = bound.map(([f]) => f).sort(); + expect(fields).toEqual(["paddingBottom", "paddingLeft", "paddingRight", "paddingTop"]); + expect(bound.every(([, id]) => id === "v1")).toBe(true); + }); + + it("specificity: paddingTop overrides padding", async () => { + const bound: Array<[string, string]> = []; + const node = { + layoutMode: "VERTICAL", + setBoundVariable: (field: string, v: { id: string }) => bound.push([field, v.id]), + }; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +await PluginOS.bindSpacing(globalThis.__node, { padding: { id: "all" }, paddingTop: { id: "top" } }); +`); + (ctx as Record).__node = node; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + const top = bound.find(([f]) => f === "paddingTop"); + expect(top).toEqual(["paddingTop", "top"]); + }); + + it("no-ops on non-autolayout node", async () => { + const bound: Array<[string, string]> = []; + const node = { + layoutMode: "NONE", + setBoundVariable: (field: string, v: { id: string }) => bound.push([field, v.id]), + }; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +await PluginOS.bindSpacing(globalThis.__node, { padding: { id: "v1" } }); +`); + (ctx as Record).__node = node; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(bound).toEqual([]); + }); +}); + +describe("combineAsVariantsTiled", () => { + it("calls combineAsVariants and sets layout fields", async () => { + const set: Record = { + resize: function (w: number, h: number) { + this.width = w; + this.height = h; + }, + }; + set.width = 0; + set.height = 0; + const calls: string[] = []; + const figma = { + combineAsVariants: (cells: unknown[], _parent: unknown) => { + calls.push("combine:" + cells.length); + return set; + }, + }; + const cells = [ + { width: 100, height: 50 }, + { width: 100, height: 50 }, + { width: 100, height: 50 }, + { width: 100, height: 50 }, + ]; + const ctx = makeContext(figma); + const { wrapped } = wrapScript(` +const set = PluginOS.combineAsVariantsTiled(globalThis.__cells, {}, { cols: 2, gutter: 10 }); +globalThis.__out = set; +`); + (ctx as Record).__cells = cells; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(calls).toContain("combine:4"); + expect(set.layoutMode).toBe("HORIZONTAL"); + expect(set.layoutWrap).toBe("WRAP"); + expect(set.itemSpacing).toBe(10); + expect(set.primaryAxisSizingMode).toBe("FIXED"); + expect(set.counterAxisSizingMode).toBe("AUTO"); + expect(set.width).toBeGreaterThan(0); + expect(ctx.__out).toBe(set); + }); +}); + +describe("tileTopLevel", () => { + it("places nodes in a grid with the configured cols and gutter", async () => { + const appended: unknown[] = []; + const page = { appendChild: (n: unknown) => appended.push(n) }; + const nodes = [ + { width: 100, height: 50, x: 0, y: 0 }, + { width: 100, height: 50, x: 0, y: 0 }, + { width: 100, height: 50, x: 0, y: 0 }, + { width: 100, height: 50, x: 0, y: 0 }, + ]; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +const place = PluginOS.tileTopLevel(globalThis.__page, { cols: 2, gutter: 10 }); +globalThis.__nodes.forEach(place); +`); + (ctx as Record).__page = page; + (ctx as Record).__nodes = nodes; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(nodes[0].x).toBe(0); + expect(nodes[0].y).toBe(0); + expect(nodes[1].x).toBe(110); + expect(nodes[1].y).toBe(0); + expect(nodes[2].x).toBe(0); + expect(nodes[2].y).toBe(60); + expect(nodes[3].x).toBe(110); + expect(nodes[3].y).toBe(60); + expect(appended).toEqual(nodes); + }); +}); + +describe("layoutSpaceBetween", () => { + it("for 2 children, picks the last child as grow target (non-TEXT → layoutGrow=1)", async () => { + const left = { type: "FRAME", layoutGrow: 0 }; + const right = { type: "FRAME", layoutGrow: 0 }; + const frame: Record = { primaryAxisAlignItems: "INIT" }; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +PluginOS.layoutSpaceBetween(globalThis.__frame, { children: [globalThis.__left, globalThis.__right] }); +`); + (ctx as Record).__frame = frame; + (ctx as Record).__left = left; + (ctx as Record).__right = right; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(frame.primaryAxisAlignItems).toBe("MIN"); + expect(right.layoutGrow).toBe(1); + expect(left.layoutGrow).toBe(0); + }); + + it("for TEXT grow target, uses layoutSizingHorizontal=FILL", async () => { + const left = { type: "TEXT", layoutSizingHorizontal: "HUG" }; + const right = { type: "FRAME", layoutGrow: 0 }; + const frame: Record = {}; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +PluginOS.layoutSpaceBetween(globalThis.__frame, { growChild: globalThis.__left }); +`); + (ctx as Record).__frame = frame; + (ctx as Record).__left = left; + (ctx as Record).__right = right; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(left.layoutSizingHorizontal).toBe("FILL"); + }); + + it("for 3 children, picks the middle child", async () => { + const a = { type: "FRAME", layoutGrow: 0 }; + const b = { type: "FRAME", layoutGrow: 0 }; + const c = { type: "FRAME", layoutGrow: 0 }; + const frame: Record = {}; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +PluginOS.layoutSpaceBetween(globalThis.__frame, { children: [globalThis.__a, globalThis.__b, globalThis.__c] }); +`); + (ctx as Record).__frame = frame; + (ctx as Record).__a = a; + (ctx as Record).__b = b; + (ctx as Record).__c = c; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(b.layoutGrow).toBe(1); + expect(a.layoutGrow).toBe(0); + expect(c.layoutGrow).toBe(0); + }); +}); diff --git a/packages/mcp-server/src/prelude/__tests__/wrap.test.ts b/packages/mcp-server/src/prelude/__tests__/wrap.test.ts new file mode 100644 index 0000000..7d84d46 --- /dev/null +++ b/packages/mcp-server/src/prelude/__tests__/wrap.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from "vitest"; +import { wrapScript, PRELUDE_VERSION } from "../index.js"; + +describe("wrapScript", () => { + it("returns wrapped with userJs after prelude", () => { + const userJs = `return figma.currentPage.name;`; + const { wrapped } = wrapScript(userJs); + expect(wrapped.endsWith(userJs)).toBe(true); + expect(wrapped).toContain("PluginOS"); + }); + + it("reports prelude line count consistent with prelude size", () => { + const { wrapped, preludeLineCount } = wrapScript(`x`); + const lines = wrapped.split("\n"); + expect(lines[preludeLineCount]).toBe("x"); + }); + + it("exports a non-empty PRELUDE_VERSION", () => { + expect(PRELUDE_VERSION).toMatch(/^\d+\.\d+\.\d+/); + }); +}); diff --git a/packages/mcp-server/src/prelude/index.ts b/packages/mcp-server/src/prelude/index.ts new file mode 100644 index 0000000..d54ba31 --- /dev/null +++ b/packages/mcp-server/src/prelude/index.ts @@ -0,0 +1,39 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { PRELUDE_SOURCE } from "./source.js"; + +function readPackageVersion(): string { + const dir = dirname(fileURLToPath(import.meta.url)); + const candidates = [ + join(dir, "..", "..", "package.json"), + join(dir, "..", "..", "..", "package.json"), + ]; + for (const p of candidates) { + try { + const pkg = JSON.parse(readFileSync(p, "utf8")); + if ( + typeof pkg.name === "string" && + (pkg.name === "pluginos" || pkg.name.includes("pluginos")) && + typeof pkg.version === "string" + ) { + return pkg.version; + } + } catch { + // try next + } + } + return "0.0.0"; +} + +export const PRELUDE_VERSION: string = readPackageVersion(); + +const RESOLVED_PRELUDE = PRELUDE_SOURCE.replace("__PRELUDE_VERSION__", PRELUDE_VERSION); +const PRELUDE_LINES = RESOLVED_PRELUDE.split("\n").length; + +export function wrapScript(userJs: string): { wrapped: string; preludeLineCount: number } { + return { + wrapped: RESOLVED_PRELUDE + userJs, + preludeLineCount: PRELUDE_LINES - 1, + }; +} diff --git a/packages/mcp-server/src/prelude/source.ts b/packages/mcp-server/src/prelude/source.ts new file mode 100644 index 0000000..6065c2d --- /dev/null +++ b/packages/mcp-server/src/prelude/source.ts @@ -0,0 +1,123 @@ +// The JS source injected into every execute_figma script. Runs in Figma's plugin sandbox. +export const PRELUDE_SOURCE = `// --- PluginOS prelude --- +;(function(){ + var P = {}; + P.version = '__PRELUDE_VERSION__'; + + P.createStyledText = async function(opts) { + if (!opts.textStyleId && (!opts.family || opts.size == null)) { + throw new Error('[PluginOS.createStyledText] requires textStyleId or (family + size)'); + } + var weight = opts.weight || 'Regular'; + if (opts.family) { + await figma.loadFontAsync({ family: opts.family, style: weight }); + } + var node = figma.createText(); + if (opts.textStyleId) { + await node.setTextStyleIdAsync(opts.textStyleId); + } else { + node.fontName = { family: opts.family, style: weight }; + node.fontSize = opts.size; + } + node.characters = opts.characters; + if (opts.fillStyleId) { + await node.setFillStyleIdAsync(opts.fillStyleId); + } + if (opts.name) node.name = opts.name; + return node; + }; + + P.bindSpacing = async function(node, vars) { + if (!vars) return; + if (!node || !('layoutMode' in node) || node.layoutMode === 'NONE') return; + function pick(specific, axis, all) { + if (specific) return specific; + if (axis) return axis; + if (all) return all; + return null; + } + var pairs = [ + ['paddingTop', pick(vars.paddingTop, vars.paddingY, vars.padding)], + ['paddingBottom', pick(vars.paddingBottom, vars.paddingY, vars.padding)], + ['paddingLeft', pick(vars.paddingLeft, vars.paddingX, vars.padding)], + ['paddingRight', pick(vars.paddingRight, vars.paddingX, vars.padding)], + ['itemSpacing', vars.itemSpacing || null], + ]; + for (var i = 0; i < pairs.length; i++) { + var field = pairs[i][0]; + var v = pairs[i][1]; + if (v) node.setBoundVariable(field, v); + } + }; + + P.combineAsVariantsTiled = function(cells, parent, opts) { + opts = opts || {}; + var cols = opts.cols || Math.ceil(Math.sqrt(cells.length)); + var gutter = opts.gutter == null ? 16 : opts.gutter; + var layoutMode = opts.layoutMode || 'HORIZONTAL'; + var wrap = opts.wrap !== false; + var set = figma.combineAsVariants(cells, parent); + set.layoutMode = layoutMode; + if (wrap) set.layoutWrap = 'WRAP'; + set.itemSpacing = gutter; + set.counterAxisSpacing = gutter; + set.primaryAxisSizingMode = 'FIXED'; + set.counterAxisSizingMode = 'AUTO'; + var width = opts.width; + if (width == null) { + var cellW = cells[0] && cells[0].width ? cells[0].width : 0; + width = cols * cellW + (cols - 1) * gutter + 32; + } + set.resize(width, set.height || 100); + return set; + }; + + P.tileTopLevel = function(page, opts) { + opts = opts || {}; + var cols = opts.cols || 4; + var gutter = opts.gutter == null ? 64 : opts.gutter; + var origin = opts.origin || { x: 0, y: 0 }; + var i = 0; + var rowH = 0; + var cursorX = origin.x; + var cursorY = origin.y; + return function place(node) { + var col = i % cols; + if (col === 0 && i > 0) { + cursorY += rowH + gutter; + cursorX = origin.x; + rowH = 0; + } + node.x = cursorX; + node.y = cursorY; + page.appendChild(node); + cursorX += node.width + gutter; + if (node.height > rowH) rowH = node.height; + i++; + }; + }; + + P.layoutSpaceBetween = function(frame, opts) { + if (!frame) throw new Error('[PluginOS.layoutSpaceBetween] frame is required'); + opts = opts || {}; + frame.primaryAxisAlignItems = 'MIN'; + var growChild = opts.growChild; + if (!growChild && opts.children) { + var kids = opts.children; + if (kids.length >= 3) growChild = kids[Math.floor(kids.length / 2)]; + else if (kids.length === 2) growChild = kids[kids.length - 1]; + } + if (!growChild) throw new Error('[PluginOS.layoutSpaceBetween] no growChild resolvable'); + var isVertical = frame.layoutMode === 'VERTICAL'; + if (growChild.type === 'TEXT') { + if (isVertical) growChild.layoutSizingVertical = 'FILL'; + else growChild.layoutSizingHorizontal = 'FILL'; + } else { + growChild.layoutGrow = 1; + } + }; + + globalThis.PluginOS = P; +})(); +// --- end prelude --- +`; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 8e8e4be..2dade6d 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -6,6 +6,8 @@ import { CATEGORY_DESCRIPTIONS, } from "@pluginos/shared"; import type { IPluginBridge } from "@pluginos/shared"; +import { wrapScript, PRELUDE_VERSION } from "./prelude/index.js"; +import { runLint } from "./lint/index.js"; export function createPluginOSServer(bridge: IPluginBridge) { const server = new McpServer({ @@ -34,11 +36,12 @@ export function createPluginOSServer(bridge: IPluginBridge) { try { const result = await bridge.sendAndWait(msg, 5000); if (result.success) { + const ops = Array.isArray(result.result) ? result.result : []; return { content: [ { type: "text" as const, - text: JSON.stringify(result.result, null, 2), + text: JSON.stringify({ operations: ops, total: ops.length }, null, 2), }, ], }; @@ -130,16 +133,29 @@ export function createPluginOSServer(bridge: IPluginBridge) { }, async ({ code, timeout, file_key }) => { const safeTimeout = Math.min(timeout, 30000); - const msg = createExecuteMessage(code, safeTimeout); + const lint = runLint(code); + const { wrapped } = wrapScript(code); + const msg = createExecuteMessage(wrapped, safeTimeout); + const startedAt = Date.now(); try { const result = await bridge.sendAndWait(msg, safeTimeout + 2000, file_key); + const durationMs = Date.now() - startedAt; if (result.success) { return { content: [ { type: "text" as const, - text: JSON.stringify(result.result, null, 2), + text: JSON.stringify( + { + result: result.result, + lint, + preludeVersion: PRELUDE_VERSION, + durationMs, + }, + null, + 2 + ), }, ], }; 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" } }