From a41b1a0a545df06b528838f274e9b63aa5d46c77 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 09:56:01 +0200 Subject: [PATCH 01/23] feat(mcp): implement wireframe tools and services --- package-lock.json | 1829 +++++++++++++++-- packages/mcp/package.json | 11 +- packages/mcp/src/commons/qm-file.models.ts | 28 + packages/mcp/src/commons/qm-file.utils.ts | 28 + .../mcp/src/commons/tool-response.helpers.ts | 19 + .../mcp/src/commons/wireframe-file.service.ts | 13 + packages/mcp/src/core/index.ts | 3 + packages/mcp/src/core/registry.client.ts | 37 + packages/mcp/src/core/registry.models.ts | 8 + packages/mcp/src/core/registry.utils.ts | 5 + packages/mcp/src/index.ts | 53 +- packages/mcp/src/renderer/bridge.server.ts | 70 + .../mcp/src/renderer/headless.renderer.ts | 48 + packages/mcp/src/renderer/index.ts | 1 + packages/mcp/src/renderer/page.session.ts | 96 + packages/mcp/src/renderer/renderer.consts.ts | 16 + .../capture-wireframe.handler.ts | 37 + .../capture-wireframe.schema.ts | 13 + .../capture-wireframe.tool.ts | 11 + .../mcp/src/tools/capture-wireframe/index.ts | 1 + .../get-wireframe-assets.handler.ts | 105 + .../get-wireframe-assets.models.ts | 6 + .../get-wireframe-assets.schema.ts | 13 + .../get-wireframe-assets.tool.ts | 13 + .../src/tools/get-wireframe-assets/index.ts | 1 + .../get-wireframe-json.handler.ts | 14 + .../get-wireframe-json.schema.ts | 5 + .../get-wireframe-json.tool.ts | 10 + .../mcp/src/tools/get-wireframe-json/index.ts | 1 + .../get-wireframe-pages.handler.ts | 23 + .../get-wireframe-pages.models.ts | 6 + .../get-wireframe-pages.schema.ts | 5 + .../get-wireframe-pages.tool.ts | 11 + .../src/tools/get-wireframe-pages/index.ts | 1 + .../mcp/src/tools/list-wireframes/index.ts | 1 + .../list-wireframes.handler.ts | 44 + .../list-wireframes/list-wireframes.tool.ts | 8 + packages/mcp/tsdown.config.ts | 4 + packages/vscode-extension/package.json | 37 +- packages/vscode-extension/tsconfig.json | 1 + tooling/typescript/node.json | 1 + 41 files changed, 2507 insertions(+), 130 deletions(-) create mode 100644 packages/mcp/src/commons/qm-file.models.ts create mode 100644 packages/mcp/src/commons/qm-file.utils.ts create mode 100644 packages/mcp/src/commons/tool-response.helpers.ts create mode 100644 packages/mcp/src/commons/wireframe-file.service.ts create mode 100644 packages/mcp/src/core/index.ts create mode 100644 packages/mcp/src/core/registry.client.ts create mode 100644 packages/mcp/src/core/registry.models.ts create mode 100644 packages/mcp/src/core/registry.utils.ts create mode 100644 packages/mcp/src/renderer/bridge.server.ts create mode 100644 packages/mcp/src/renderer/headless.renderer.ts create mode 100644 packages/mcp/src/renderer/index.ts create mode 100644 packages/mcp/src/renderer/page.session.ts create mode 100644 packages/mcp/src/renderer/renderer.consts.ts create mode 100644 packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts create mode 100644 packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts create mode 100644 packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts create mode 100644 packages/mcp/src/tools/capture-wireframe/index.ts create mode 100644 packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts create mode 100644 packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts create mode 100644 packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts create mode 100644 packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts create mode 100644 packages/mcp/src/tools/get-wireframe-assets/index.ts create mode 100644 packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts create mode 100644 packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts create mode 100644 packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts create mode 100644 packages/mcp/src/tools/get-wireframe-json/index.ts create mode 100644 packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts create mode 100644 packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts create mode 100644 packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts create mode 100644 packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts create mode 100644 packages/mcp/src/tools/get-wireframe-pages/index.ts create mode 100644 packages/mcp/src/tools/list-wireframes/index.ts create mode 100644 packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts create mode 100644 packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts diff --git a/package-lock.json b/package-lock.json index 27781100..7dc27a20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -278,7 +278,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -293,7 +292,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -378,7 +376,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -405,6 +402,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -736,40 +734,6 @@ "sisteransi": "^1.0.5" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -1224,6 +1188,18 @@ "integrity": "sha512-LAFerSBxVKNHFy3B9kyKgkQUIG6Om2RLQ6vDayd4IQFlRmhuxdV9nOarUjoVHwKWYk8VqT+C6fBMW+EGMJ1eFA==", "license": "OFL-1.1" }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@inquirer/external-editor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", @@ -1387,6 +1363,46 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -1800,6 +1816,67 @@ "dev": true, "license": "MIT" }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/@quansync/fs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", @@ -2443,6 +2520,12 @@ "@textlint/ast-node-types": "15.5.4" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@turbo/darwin-64": { "version": "2.9.6", "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.9.6.tgz", @@ -2591,7 +2674,7 @@ "version": "24.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2615,6 +2698,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2667,6 +2751,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", @@ -2926,6 +3020,7 @@ "integrity": "sha512-q3PchVhZINX23Pv+RERgAtDlp6wzVkID/smOPnZ5YGWpeWUe3jMNYppeVh15j4il3G7JIJty1d1Kicpm0HSMig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/browser": "4.1.4", "@vitest/mocker": "4.1.4", @@ -2950,6 +3045,7 @@ "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", @@ -3079,6 +3175,7 @@ "integrity": "sha512-EgFR7nlj5iTDYZYCvavjFokNYwr3c3ry0sFiCg+N7B233Nwp+NNx7eoF/XvMWDCKY71xXAG3kFkt97ZHBJVL8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.1.4", "fflate": "^0.8.2", @@ -3312,11 +3409,48 @@ "node": ">=18" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -3326,7 +3460,6 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3339,6 +3472,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3369,7 +3519,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3402,7 +3551,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -3503,6 +3651,18 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-v8-to-istanbul": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", @@ -3550,6 +3710,97 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", + "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", + "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -3581,6 +3832,15 @@ "license": "MIT", "optional": true }, + "node_modules/basic-ftp": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.0.tgz", + "integrity": "sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/better-path-resolve": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", @@ -3639,6 +3899,30 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3707,7 +3991,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -3736,6 +4019,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", @@ -3750,7 +4042,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3764,7 +4055,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3777,6 +4067,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3879,6 +4178,28 @@ "license": "ISC", "optional": true }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -3912,35 +4233,108 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cockatiel": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", - "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", - "dev": true, - "license": "MIT", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">=16" + "node": ">=12" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" + "node_modules/cliui/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==", + "license": "MIT" + }, + "node_modules/cliui/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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==", + "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/cliui/node_modules/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==", + "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/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", @@ -3979,6 +4373,28 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "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", @@ -3986,11 +4402,89 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4053,11 +4547,19 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4149,6 +4651,20 @@ "dev": true, "license": "MIT" }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4159,6 +4675,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -4183,6 +4708,13 @@ "resolved": "tooling/dev-cli", "link": true }, + "node_modules/devtools-protocol": { + "version": "0.0.1595872", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", + "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4287,7 +4819,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4325,6 +4856,12 @@ "url": "https://bevry.me/fund" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -4342,6 +4879,15 @@ "node": ">=14" } }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -4373,9 +4919,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "once": "^1.4.0" } @@ -4407,6 +4951,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -4420,11 +4973,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4434,7 +4995,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4451,7 +5011,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4483,6 +5042,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4518,11 +5078,46 @@ "@esbuild/win32-x64": "0.27.7" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -4532,6 +5127,15 @@ "node": ">=4" } }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4542,6 +5146,24 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -4549,6 +5171,36 @@ "dev": true, "license": "MIT" }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -4570,6 +5222,92 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -4577,11 +5315,46 @@ "dev": true, "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, "node_modules/fast-glob": { @@ -4620,7 +5393,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, "funding": [ { "type": "github", @@ -4652,6 +5424,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4690,6 +5471,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -4745,6 +5547,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -4787,12 +5607,20 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", @@ -4810,7 +5638,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4835,7 +5662,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4845,6 +5671,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "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", @@ -4858,6 +5699,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -4968,7 +5823,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4998,7 +5852,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5027,7 +5880,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5036,6 +5888,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hookable": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", @@ -5109,11 +5971,30 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -5127,7 +6008,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -5167,7 +6047,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -5222,6 +6101,31 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/import-without-cache": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.3.3.tgz", @@ -5262,9 +6166,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", @@ -5274,6 +6176,30 @@ "license": "ISC", "optional": true }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -5358,6 +6284,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-subdir": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", @@ -5401,7 +6333,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -5489,6 +6420,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -5500,7 +6440,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5522,13 +6461,24 @@ "node": ">=6" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5636,7 +6586,8 @@ "url": "https://github.com/sponsors/lavrton" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/leven": { "version": "3.1.0", @@ -5909,6 +6860,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -6207,7 +7164,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6220,6 +7176,27 @@ "dev": true, "license": "MIT" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6364,6 +7341,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -6396,7 +7379,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mute-stream": { @@ -6433,6 +7415,24 @@ "license": "MIT", "optional": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-abi": { "version": "3.89.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", @@ -6555,11 +7555,19 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6579,13 +7587,23 @@ ], "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", - "optional": true, "dependencies": { "wrappy": "1" } @@ -6739,6 +7757,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "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", @@ -6756,6 +7806,18 @@ "quansync": "^0.2.7" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-json": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", @@ -6847,6 +7909,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6861,7 +7932,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6894,6 +7964,16 @@ "node": "20 || >=22" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6915,14 +7995,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -6948,6 +8026,15 @@ "node": ">=6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/playwright": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", @@ -7089,6 +8176,62 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/publish": { "resolved": "tooling/publish", "link": true @@ -7097,9 +8240,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7112,14 +8253,52 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "24.41.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.41.0.tgz", + "integrity": "sha512-W6Fk0J3TPjjtwjXOyR/qf+YaL0H/Uq8HIgHcXG4mNM/IgbKMCH/HPyK0Fi2qbTU/QpSl9bCte2yBpGHKejTpIw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1595872", + "puppeteer-core": "24.41.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.41.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.41.0.tgz", + "integrity": "sha512-rLIUri7E/NQ3APSEYCCozaSJx0u8Tu9wxO6BJwnvXmIgILSK3L0TombaVh3izp1njAGrO6H2ru0hcIrLF+gWLw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1595872", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -7179,6 +8358,30 @@ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -7214,6 +8417,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7226,6 +8430,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7253,6 +8458,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@types/react-reconciler": "^0.28.2", "its-fine": "^1.1.1", @@ -7399,11 +8605,19 @@ "node": ">=8" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7470,6 +8684,7 @@ "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/types": "=0.126.0", "@rolldown/pluginutils": "1.0.0-rc.16" @@ -7601,6 +8816,22 @@ "dev": true, "license": "MIT" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -7663,7 +8894,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sax": { @@ -7781,7 +9011,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7790,11 +9019,86 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7807,7 +9111,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7817,7 +9120,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7837,7 +9139,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7854,7 +9155,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7873,7 +9173,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8006,6 +9305,54 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8077,6 +9424,15 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", @@ -8084,6 +9440,17 @@ "dev": true, "license": "MIT" }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8155,7 +9522,6 @@ "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" @@ -8353,6 +9719,15 @@ "node": ">=6" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -8383,6 +9758,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -8488,6 +9886,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -8578,7 +9985,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tsx": { @@ -8587,6 +9993,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -8656,6 +10063,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, "node_modules/typed-rest-client": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", @@ -8672,8 +10124,9 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8741,7 +10194,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -8767,6 +10220,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unrun": { "version": "0.2.36", "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.36.tgz", @@ -8864,6 +10326,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/version-range": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", @@ -8883,6 +10354,7 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -9353,6 +10825,12 @@ } } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -9394,7 +10872,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -9492,15 +10969,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9558,6 +11032,15 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -9571,6 +11054,7 @@ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -9581,6 +11065,62 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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==", + "license": "MIT" + }, + "node_modules/yargs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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==", + "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/yauzl": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", @@ -9605,15 +11145,57 @@ "buffer-crc32": "~0.2.3" } }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "packages/mcp": { "name": "@lemoncode/quickmock-mcp", "version": "0.0.1", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "puppeteer": "^24.0.0", + "zod": "^4.0.0" + }, "devDependencies": { "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", - "@lemoncode/vitest-config": "*" + "@lemoncode/vitest-config": "*", + "@types/node": "22.x" } }, + "packages/mcp/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "packages/mcp/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "packages/vscode-extension": { "name": "quickmock", "version": "0.0.1", @@ -9622,6 +11204,7 @@ "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", + "@types/node": "^24.12.2", "@types/vscode": "1.116.0", "@vscode/vsce": "3.9.0" }, diff --git a/packages/mcp/package.json b/packages/mcp/package.json index dff5fdff..86494abc 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -19,12 +19,19 @@ "check-types": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.mjs" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "puppeteer": "^24.0.0", + "zod": "^4.0.0" }, "devDependencies": { "@lemoncode/typescript-config": "*", "@lemoncode/tsdown-config": "*", - "@lemoncode/vitest-config": "*" + "@lemoncode/vitest-config": "*", + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/packages/mcp/src/commons/qm-file.models.ts b/packages/mcp/src/commons/qm-file.models.ts new file mode 100644 index 00000000..f71d7d64 --- /dev/null +++ b/packages/mcp/src/commons/qm-file.models.ts @@ -0,0 +1,28 @@ +export interface QmShape { + id: string + type: string + otherProps?: { + imageSrc?: string + [key: string]: unknown + } + [key: string]: unknown +} + +export interface QmPage { + id: string + name: string + shapes: QmShape[] +} + +export interface QmFileContract { + version: string + pages: QmPage[] + customColors: (string | null)[] + size: { width: number; height: number } +} + +export interface QmFile { + absPath: string + content: string + parsed: QmFileContract +} diff --git a/packages/mcp/src/commons/qm-file.utils.ts b/packages/mcp/src/commons/qm-file.utils.ts new file mode 100644 index 00000000..c95bb058 --- /dev/null +++ b/packages/mcp/src/commons/qm-file.utils.ts @@ -0,0 +1,28 @@ +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import type { RegistryClient } from '../core/registry.models' +import type { QmFile, QmFileContract } from './qm-file.models' + +export type { QmFile, QmFileContract } + +/** + * Reads a .qm file (live registry first, disk fallback) and returns the raw + * content string together with the parsed contract. + * + * Throws if the file cannot be read or the JSON is invalid. + */ +export async function readQmFile(path: string, registry: RegistryClient): Promise { + const root = process.env.QM_WORKSPACE_ROOT ?? process.cwd() + const absPath = resolve(root, path) + + const live = await registry.getDocument(absPath) + const content = live ?? (await readFile(absPath, 'utf-8')) + + const parsed = JSON.parse(content) as QmFileContract + + if (!Array.isArray(parsed.pages)) { + throw new Error(`"${path}" does not contain a valid pages array.`) + } + + return { absPath, content, parsed } +} diff --git a/packages/mcp/src/commons/tool-response.helpers.ts b/packages/mcp/src/commons/tool-response.helpers.ts new file mode 100644 index 00000000..325c6ba3 --- /dev/null +++ b/packages/mcp/src/commons/tool-response.helpers.ts @@ -0,0 +1,19 @@ +type TextContent = { type: 'text'; text: string } +type ImageContent = { type: 'image'; data: string; mimeType: string } +type ToolContent = TextContent | ImageContent + +export function toolText(text: string) { + return { content: [{ type: 'text' as const, text }] } +} + +export function toolImage(data: string, mimeType: string) { + return { content: [{ type: 'image' as const, data, mimeType }] } +} + +export function toolMultiContent(items: ToolContent[]) { + return { content: items } +} + +export function toolError(text: string) { + return { content: [{ type: 'text' as const, text }], isError: true as const } +} diff --git a/packages/mcp/src/commons/wireframe-file.service.ts b/packages/mcp/src/commons/wireframe-file.service.ts new file mode 100644 index 00000000..e02455ba --- /dev/null +++ b/packages/mcp/src/commons/wireframe-file.service.ts @@ -0,0 +1,13 @@ +import type { RegistryClient } from '../core' +import type { QmFile } from './qm-file.models' +import { readQmFile } from './qm-file.utils' + +export interface WireframeFileService { + readFile(path: string): Promise +} + +export function createWireframeFileService(registry: RegistryClient): WireframeFileService { + return { + readFile: (path: string) => readQmFile(path, registry), + } +} diff --git a/packages/mcp/src/core/index.ts b/packages/mcp/src/core/index.ts new file mode 100644 index 00000000..af66b726 --- /dev/null +++ b/packages/mcp/src/core/index.ts @@ -0,0 +1,3 @@ +export * from './registry.client' +export * from './registry.models' +export * from './registry.utils' diff --git a/packages/mcp/src/core/registry.client.ts b/packages/mcp/src/core/registry.client.ts new file mode 100644 index 00000000..05aad41c --- /dev/null +++ b/packages/mcp/src/core/registry.client.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { nullClient, type RegistryClient } from './registry.models' +import { workspaceHash } from './registry.utils' + +/** HTTP client for the VSCode extension's registry server. Falls back to nullClient when the extension is not running. */ +export function createRegistryClient(): RegistryClient { + const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd() + + let port: number + try { + const hash = workspaceHash(workspaceRoot) + const portFile = join(tmpdir(), `quickmock-${hash}.port`) + port = parseInt(readFileSync(portFile, 'utf-8').trim(), 10) + if (Number.isNaN(port)) { + return nullClient + } + } catch { + return nullClient + } + + return { + async getDocument(fsPath: string): Promise { + try { + const url = `http://127.0.0.1:${port}/document?path=${encodeURIComponent(fsPath)}` + const res = await fetch(url, { signal: AbortSignal.timeout(2_000) }) + if (!res.ok) { + return null + } + return await res.text() + } catch { + return null + } + }, + } +} diff --git a/packages/mcp/src/core/registry.models.ts b/packages/mcp/src/core/registry.models.ts new file mode 100644 index 00000000..7fe6f39d --- /dev/null +++ b/packages/mcp/src/core/registry.models.ts @@ -0,0 +1,8 @@ +export interface RegistryClient { + /** Returns live in-memory content for a file open in the editor, or null. */ + getDocument(fsPath: string): Promise +} + +export const nullClient: RegistryClient = { + getDocument: async () => null, +} diff --git a/packages/mcp/src/core/registry.utils.ts b/packages/mcp/src/core/registry.utils.ts new file mode 100644 index 00000000..195de4e3 --- /dev/null +++ b/packages/mcp/src/core/registry.utils.ts @@ -0,0 +1,5 @@ +import { createHash } from 'node:crypto' + +export function workspaceHash(root: string): string { + return createHash('md5').update(root).digest('hex').slice(0, 8) +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index cb0ff5c3..c8687f82 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1 +1,52 @@ -export {}; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio' +import { createWireframeFileService } from './commons/wireframe-file.service' +import { createRegistryClient } from './core' +import { captureWireframe } from './tools/capture-wireframe' +import { getWireframeAssets } from './tools/get-wireframe-assets' +import { getWireframeJson } from './tools/get-wireframe-json' +import { getWireframePages } from './tools/get-wireframe-pages' +import { listWireframes } from './tools/list-wireframes' + +const registry = createRegistryClient() +const service = createWireframeFileService(registry) + +const server = new McpServer({ name: 'quickmock', version: '0.1.0' }) + +server.registerTool(listWireframes.name, { description: listWireframes.description }, () => + listWireframes.execute(), +) + +server.registerTool( + getWireframeJson.name, + { description: getWireframeJson.description, inputSchema: getWireframeJson.schema }, + (args) => getWireframeJson.execute(args, service), +) + +server.registerTool( + getWireframePages.name, + { description: getWireframePages.description, inputSchema: getWireframePages.schema }, + (args) => getWireframePages.execute(args, service), +) + +server.registerTool( + captureWireframe.name, + { description: captureWireframe.description, inputSchema: captureWireframe.schema }, + (args) => captureWireframe.execute(args, service), +) + +server.registerTool( + getWireframeAssets.name, + { description: getWireframeAssets.description, inputSchema: getWireframeAssets.schema }, + (args) => getWireframeAssets.execute(args, service), +) + +async function main() { + const transport = new StdioServerTransport() + await server.connect(transport) +} + +main().catch((err) => { + console.error('[quickmock-mcp] fatal error:', err) + process.exit(1) +}) diff --git a/packages/mcp/src/renderer/bridge.server.ts b/packages/mcp/src/renderer/bridge.server.ts new file mode 100644 index 00000000..c818230c --- /dev/null +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -0,0 +1,70 @@ +import { createServer, type Server } from 'node:http' +import { AddressInfo } from 'node:net' +import { QUICKMOCK_URL } from './renderer.consts' + +export interface BridgeServer { + server: Server + port: number +} + +/** HTTP server that serves the Puppeteer ↔ QuickMock iframe bridge page. */ +export function startBridgeServer(): Promise { + return new Promise((resolve, reject) => { + const server = createServer((_req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) + res.end(buildBridgeHtml()) + }) + + server.on('error', reject) + + server.listen(0, '127.0.0.1', () => { + const { port } = server.address() as AddressInfo + resolve({ server, port }) + }) + }) +} + +function buildBridgeHtml(): string { + return /* html */ ` + + + + + + + + + +` +} diff --git a/packages/mcp/src/renderer/headless.renderer.ts b/packages/mcp/src/renderer/headless.renderer.ts new file mode 100644 index 00000000..bf4a0bd0 --- /dev/null +++ b/packages/mcp/src/renderer/headless.renderer.ts @@ -0,0 +1,48 @@ +import type { Browser } from 'puppeteer' +import puppeteer from 'puppeteer' +import { startBridgeServer } from './bridge.server' +import { + screenshotIframe, + sendFileToApp, + waitForAppReady, + waitForRenderComplete, + watchNetworkFailures, +} from './page.session' + +const BROWSER_LAUNCH_ARGS = [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-web-security', + '--disable-features=IsolateOrigins,site-per-process', +] + +/** Renders a .qm file in a headless Chromium instance and returns a PNG buffer. */ +export async function renderWireframe(content: string, fileName: string): Promise { + const { server, port } = await startBridgeServer() + + try { + return await withBrowser(async (browser) => { + const page = await browser.newPage() + await page.setViewport({ width: 1440, height: 900 }) + await page.goto(`http://127.0.0.1:${port}`, { waitUntil: 'domcontentloaded' }) + + const networkFailure = watchNetworkFailures(page) + await waitForAppReady(page, networkFailure) + await sendFileToApp(page, content, fileName) + const bbox = await waitForRenderComplete(page) + + return screenshotIframe(page, bbox) + }) + } finally { + server.close() + } +} + +async function withBrowser(fn: (browser: Browser) => Promise): Promise { + const browser = await puppeteer.launch({ headless: true, args: BROWSER_LAUNCH_ARGS }) + try { + return await fn(browser) + } finally { + await browser.close() + } +} diff --git a/packages/mcp/src/renderer/index.ts b/packages/mcp/src/renderer/index.ts new file mode 100644 index 00000000..57e43185 --- /dev/null +++ b/packages/mcp/src/renderer/index.ts @@ -0,0 +1 @@ +export * from './headless.renderer' diff --git a/packages/mcp/src/renderer/page.session.ts b/packages/mcp/src/renderer/page.session.ts new file mode 100644 index 00000000..c7530d2c --- /dev/null +++ b/packages/mcp/src/renderer/page.session.ts @@ -0,0 +1,96 @@ +import type { Page } from 'puppeteer' +import { + LOCAL_INSTANCE_HINT, + QM_APP_ORIGIN, + QUICKMOCK_URL, + READY_TIMEOUT_MS, + RENDER_TIMEOUT_MS, +} from './renderer.consts' + +export interface ContentBbox { + x: number + y: number + width: number + height: number +} + +/** Rejects early on network failure — avoids waiting the full READY_TIMEOUT_MS. */ +export function watchNetworkFailures(page: Page): Promise { + return new Promise((_, reject) => { + page.on('requestfailed', (request) => { + if (request.url().startsWith(QM_APP_ORIGIN)) { + const reason = request.failure()?.errorText ?? 'network error' + reject( + new Error( + `Cannot reach QuickMock app at "${QUICKMOCK_URL}": ${reason}.\n${LOCAL_INSTANCE_HINT}`, + ), + ) + } + }) + }) +} + +/** Waits for `qm:ready`, racing against `networkFailure` for fast error reporting. */ +export async function waitForAppReady(page: Page, networkFailure: Promise): Promise { + try { + await Promise.race([ + page.waitForFunction(() => (window as Window & { __qmReady?: boolean }).__qmReady === true, { + timeout: READY_TIMEOUT_MS, + }), + networkFailure, + ]) + } catch (err) { + if (err instanceof Error && err.message.startsWith('Cannot reach')) throw err + + const iframeLoaded = await page + .evaluate(() => (window as Window & { __iframeLoaded?: boolean }).__iframeLoaded === true) + .catch(() => false) + + if (iframeLoaded) { + throw new Error( + `QuickMock app loaded but did not emit qm:ready within ${READY_TIMEOUT_MS}ms — ` + + 'the app may have changed its postMessage protocol.', + ) + } + + throw new Error( + `Cannot reach QuickMock app at "${QUICKMOCK_URL}" — ` + + `the iframe did not load within ${READY_TIMEOUT_MS}ms.\n${LOCAL_INSTANCE_HINT}`, + ) + } +} + +/** Sends the file content to the QuickMock app via postMessage → iframe. */ +export async function sendFileToApp(page: Page, content: string, fileName: string): Promise { + await page.evaluate( + ({ content, fileName }) => { + const iframe = document.querySelector('iframe') as HTMLIFrameElement + iframe.contentWindow?.postMessage( + { type: 'LOAD_FILE', payload: { data: JSON.parse(content), fileName } }, + '*', + ) + }, + { content, fileName }, + ) +} + +/** Waits for the app to emit `qm:render-complete` and returns the content bbox. */ +export async function waitForRenderComplete(page: Page): Promise { + await page.waitForFunction( + () => (window as Window & { __renderComplete?: boolean }).__renderComplete === true, + { timeout: RENDER_TIMEOUT_MS }, + ) + + return page.evaluate( + () => (window as Window & { __renderBbox?: ContentBbox }).__renderBbox ?? undefined, + ) +} + +/** Screenshots the iframe, cropped to `bbox` when provided. */ +export async function screenshotIframe(page: Page, bbox: ContentBbox | undefined): Promise { + const iframe = await page.$('iframe') + if (!iframe) throw new Error('iframe element not found in renderer page') + + const screenshot = await iframe.screenshot({ type: 'png', clip: bbox }) + return Buffer.from(screenshot) +} diff --git a/packages/mcp/src/renderer/renderer.consts.ts b/packages/mcp/src/renderer/renderer.consts.ts new file mode 100644 index 00000000..c73acf1d --- /dev/null +++ b/packages/mcp/src/renderer/renderer.consts.ts @@ -0,0 +1,16 @@ +export const QUICKMOCK_URL = + process.env.QM_APP_URL ?? 'http://localhost:5173/editor.html?env=vscode&headless=1' + +export const READY_TIMEOUT_MS = 15_000 +export const RENDER_TIMEOUT_MS = 20_000 + +export const QM_APP_ORIGIN = (() => { + try { + return new URL(QUICKMOCK_URL).origin + } catch { + return QUICKMOCK_URL + } +})() + +export const LOCAL_INSTANCE_HINT = + 'Set QM_APP_URL=http://localhost:5173/editor.html?env=vscode&headless=1 to use a local instance of QuickMock.' diff --git a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts new file mode 100644 index 00000000..ac9cf713 --- /dev/null +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts @@ -0,0 +1,37 @@ +import { basename } from 'node:path' +import { renderWireframe } from '../../renderer/headless.renderer' +import { toolError, toolImage } from '../../commons/tool-response.helpers' +import type { WireframeFileService } from '../../commons/wireframe-file.service' + +export async function captureWireframeHandler( + args: { path: string; pageIndex?: number }, + service: WireframeFileService, +) { + const { path, pageIndex = 0 } = args + + try { + const { absPath, content, parsed } = await service.readFile(path) + const fileName = basename(absPath) + const pageCount = parsed.pages.length + + if (pageIndex < 0 || pageIndex >= pageCount) { + return toolError( + `Page index ${pageIndex} is out of range. ` + + `"${fileName}" has ${pageCount} page${pageCount === 1 ? '' : 's'} (indices 0–${pageCount - 1}).`, + ) + } + + const targetContent = + pageIndex === 0 + ? content + : JSON.stringify({ + ...parsed, + pages: [parsed.pages[pageIndex], ...parsed.pages.filter((_, i) => i !== pageIndex)], + }) + + const png = await renderWireframe(targetContent, fileName) + return toolImage(png.toString('base64'), 'image/png') + } catch (err) { + return toolError(`Error capturing wireframe at "${path}": ${String(err)}`) + } +} diff --git a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts new file mode 100644 index 00000000..08a361d8 --- /dev/null +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +export const captureWireframeSchema = { + path: z.string().describe('Relative or absolute path to the .qm file'), + pageIndex: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Zero-based index of the page to capture (default: 0). Use get_wireframe_pages to see all available pages.', + ), +} diff --git a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts new file mode 100644 index 00000000..a74ad1a5 --- /dev/null +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts @@ -0,0 +1,11 @@ +import { captureWireframeHandler } from './capture-wireframe.handler' +import { captureWireframeSchema } from './capture-wireframe.schema' + +export const captureWireframe = { + name: 'capture_wireframe' as const, + description: + 'Returns a PNG screenshot of a fully-rendered .qm wireframe file. ' + + 'Use get_wireframe_pages first to discover available pages and their indices. ', + schema: captureWireframeSchema, + execute: captureWireframeHandler, +} diff --git a/packages/mcp/src/tools/capture-wireframe/index.ts b/packages/mcp/src/tools/capture-wireframe/index.ts new file mode 100644 index 00000000..316ca844 --- /dev/null +++ b/packages/mcp/src/tools/capture-wireframe/index.ts @@ -0,0 +1 @@ +export * from './capture-wireframe.tool' diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts new file mode 100644 index 00000000..0be539d0 --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts @@ -0,0 +1,105 @@ +import { createHash } from 'node:crypto' +import { mkdir, writeFile } from 'node:fs/promises' +import { basename, extname, join, resolve } from 'node:path' +import { toolError, toolText } from '../../commons/tool-response.helpers' +import type { WireframeFileService } from '../../commons/wireframe-file.service' + +interface ParsedDataUrl { + mimeType: string + base64: string +} + +function parseDataUrl(src: string): ParsedDataUrl | null { + const match = src.match(/^data:([^;]+);base64,(.+)$/) + if (!match) { + return null + } + return { mimeType: match[1], base64: match[2] } +} + +function mimeTypeToExtension(mimeType: string): string { + const map: Record = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/gif': 'gif', + 'image/webp': 'webp', + 'image/svg+xml': 'svg', + } + return map[mimeType] ?? 'bin' +} + +function sanitizeName(name: string): string { + return name.replace(/[^a-z0-9_-]/gi, '_').toLowerCase() +} + +interface SavedAsset { + pageIndex: number + pageName: string + shapeId: string + filePath: string + mimeType: string +} + +export async function getWireframeAssetsHandler( + args: { path: string; outputDir?: string }, + service: WireframeFileService, +) { + try { + const { absPath, parsed } = await service.readFile(args.path) + const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd() + const wireframeName = sanitizeName(basename(absPath, extname(absPath))) + + const targetDir = args.outputDir + ? resolve(workspaceRoot, args.outputDir) + : join(workspaceRoot, 'images', wireframeName) + + await mkdir(targetDir, { recursive: true }) + + const seenHashes = new Set() + const saved: SavedAsset[] = [] + + for (const [pageIndex, page] of parsed.pages.entries()) { + for (const shape of page.shapes) { + if (shape.type !== 'image') { + continue + } + const src = shape.otherProps?.imageSrc + if (!src) { + continue + } + + const dataUrl = parseDataUrl(src) + if (!dataUrl) { + continue + } + + const { mimeType, base64 } = dataUrl + const hash = createHash('sha1').update(base64).digest('hex') + if (seenHashes.has(hash)) { + continue + } + seenHashes.add(hash) + + const ext = mimeTypeToExtension(mimeType) + const fileName = `${sanitizeName(page.name)}-${shape.id}.${ext}` + const filePath = join(targetDir, fileName) + + await writeFile(filePath, Buffer.from(base64, 'base64')) + saved.push({ pageIndex, pageName: page.name, shapeId: shape.id, filePath, mimeType }) + } + } + + if (saved.length === 0) { + return toolText('No image assets found in this wireframe.') + } + + const summary = saved + .map((a) => `[${a.pageIndex}] "${a.pageName}" · ${a.shapeId} (${a.mimeType}) → ${a.filePath}`) + .join('\n') + + return toolText(`Saved ${saved.length} asset(s) to "${targetDir}":\n\n${summary}`) + } catch (err) { + return toolError(`Error extracting assets from "${args.path}": ${String(err)}`) + } +} diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts new file mode 100644 index 00000000..a0e38902 --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts @@ -0,0 +1,6 @@ +export interface WireframeAsset { + shapeId: string + pageIndex: number + pageName: string + filePath: string +} diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts new file mode 100644 index 00000000..cf95faad --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +export const getWireframeAssetsSchema = { + path: z.string().describe('Relative or absolute path to the .qm file'), + outputDir: z + .string() + .optional() + .describe( + 'Directory where PNG files will be saved. ' + + 'Relative paths are resolved from the workspace root. ' + + 'Defaults to "images/" inside the workspace root.', + ), +} diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts new file mode 100644 index 00000000..8aa8c16a --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts @@ -0,0 +1,13 @@ +import { getWireframeAssetsHandler } from './get-wireframe-assets.handler' +import { getWireframeAssetsSchema } from './get-wireframe-assets.schema' + +export const getWireframeAssets = { + name: 'get_wireframe_assets' as const, + description: + 'Extracts all image assets (logos, content images, etc.) from a .qm wireframe file. ' + + 'Finds every shape of type "image" that has an imageSrc, saves each one as a PNG/JPEG/etc. ' + + 'to "images//" inside the workspace root (or outputDir if provided), ' + + 'and returns the images as inline content so they can be viewed directly.', + schema: getWireframeAssetsSchema, + execute: getWireframeAssetsHandler, +} diff --git a/packages/mcp/src/tools/get-wireframe-assets/index.ts b/packages/mcp/src/tools/get-wireframe-assets/index.ts new file mode 100644 index 00000000..28ff0e64 --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-assets/index.ts @@ -0,0 +1 @@ +export * from './get-wireframe-assets.tool' diff --git a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts new file mode 100644 index 00000000..915827bd --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts @@ -0,0 +1,14 @@ +import { toolError, toolText } from '../../commons/tool-response.helpers' +import type { WireframeFileService } from '../../commons/wireframe-file.service' + +export async function getWireframeJsonHandler( + args: { path: string }, + service: WireframeFileService, +) { + try { + const { content } = await service.readFile(args.path) + return toolText(content) + } catch (err) { + return toolError(`Error reading wireframe at "${args.path}": ${String(err)}`) + } +} diff --git a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts new file mode 100644 index 00000000..ad71091d --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const getWireframeJsonSchema = { + path: z.string().describe('Relative or absolute path to the .qm file'), +} diff --git a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts new file mode 100644 index 00000000..83489dca --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts @@ -0,0 +1,10 @@ +import { getWireframeJsonHandler } from './get-wireframe-json.handler' +import { getWireframeJsonSchema } from './get-wireframe-json.schema' + +export const getWireframeJson = { + name: 'get_wireframe_json' as const, + description: + 'Returns the JSON content of a .qm wireframe file. When the file is open in the editor with unsaved changes, returns the latest in-memory state instead of the saved file.', + schema: getWireframeJsonSchema, + execute: getWireframeJsonHandler, +} diff --git a/packages/mcp/src/tools/get-wireframe-json/index.ts b/packages/mcp/src/tools/get-wireframe-json/index.ts new file mode 100644 index 00000000..221d63bd --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-json/index.ts @@ -0,0 +1 @@ +export * from './get-wireframe-json.tool' diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts new file mode 100644 index 00000000..8f9da688 --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts @@ -0,0 +1,23 @@ +import { toolError, toolText } from '../../commons/tool-response.helpers' +import type { WireframeFileService } from '../../commons/wireframe-file.service' +import type { WireframePage } from './get-wireframe-pages.models' + +export async function getWireframePagesHandler( + args: { path: string }, + service: WireframeFileService, +) { + try { + const { parsed } = await service.readFile(args.path) + + const pages: WireframePage[] = parsed.pages.map((page, index) => ({ + index, + id: page.id, + name: page.name, + shapeCount: page.shapes.length, + })) + + return toolText(JSON.stringify(pages, null, 2)) + } catch (err) { + return toolError(`Error reading pages from "${args.path}": ${String(err)}`) + } +} diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts new file mode 100644 index 00000000..e784ca9f --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts @@ -0,0 +1,6 @@ +export interface WireframePage { + index: number + id: string + name: string + shapeCount: number +} diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts new file mode 100644 index 00000000..24fa6378 --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const getWireframePagesSchema = { + path: z.string().describe('Relative or absolute path to the .qm file'), +} diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts new file mode 100644 index 00000000..9aa20939 --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts @@ -0,0 +1,11 @@ +import { getWireframePagesHandler } from './get-wireframe-pages.handler' +import { getWireframePagesSchema } from './get-wireframe-pages.schema' + +export const getWireframePages = { + name: 'get_wireframe_pages' as const, + description: + 'Returns the list of pages in a .qm wireframe file with their index, id, name, and shape count. ' + + 'Use the index values with capture_wireframe to screenshot a specific page.', + schema: getWireframePagesSchema, + execute: getWireframePagesHandler, +} diff --git a/packages/mcp/src/tools/get-wireframe-pages/index.ts b/packages/mcp/src/tools/get-wireframe-pages/index.ts new file mode 100644 index 00000000..09c29ae6 --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-pages/index.ts @@ -0,0 +1 @@ +export * from './get-wireframe-pages.tool' diff --git a/packages/mcp/src/tools/list-wireframes/index.ts b/packages/mcp/src/tools/list-wireframes/index.ts new file mode 100644 index 00000000..1ad3a3ce --- /dev/null +++ b/packages/mcp/src/tools/list-wireframes/index.ts @@ -0,0 +1 @@ +export * from './list-wireframes.tool' diff --git a/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts b/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts new file mode 100644 index 00000000..26ddaf00 --- /dev/null +++ b/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts @@ -0,0 +1,44 @@ +import { readdir } from 'node:fs/promises' +import { join, relative } from 'node:path' +import { toolError, toolText } from '../../commons/tool-response.helpers' + +const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'out', '.vscode']) + +export async function listWireframesHandler() { + const root = process.env.QM_WORKSPACE_ROOT ?? process.cwd() + + try { + const files = await collectQmFiles(root, root) + return toolText(JSON.stringify(files, null, 2)) + } catch (err) { + return toolError(`Error scanning workspace: ${String(err)}`) + } +} + +async function collectQmFiles(dir: string, root: string): Promise { + let entries: import('node:fs').Dirent[] + try { + entries = (await readdir(dir, { withFileTypes: true })) as unknown as import('node:fs').Dirent[] + } catch { + return [] + } + + const results: string[] = [] + + for (const entry of entries) { + if (IGNORED_DIRS.has(entry.name)) { + continue + } + + const fullPath = join(dir, entry.name) + + if (entry.isDirectory()) { + const nested = await collectQmFiles(fullPath, root) + results.push(...nested) + } else if (entry.isFile() && entry.name.endsWith('.qm')) { + results.push(relative(root, fullPath)) + } + } + + return results +} diff --git a/packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts b/packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts new file mode 100644 index 00000000..162c825f --- /dev/null +++ b/packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts @@ -0,0 +1,8 @@ +import { listWireframesHandler } from './list-wireframes.handler' + +export const listWireframes = { + name: 'list_wireframes' as const, + description: + 'Lists all .qm wireframe files in the current workspace. Returns paths relative to the workspace root.', + execute: listWireframesHandler, +} diff --git a/packages/mcp/tsdown.config.ts b/packages/mcp/tsdown.config.ts index 33d5ceda..bce13549 100644 --- a/packages/mcp/tsdown.config.ts +++ b/packages/mcp/tsdown.config.ts @@ -3,4 +3,8 @@ import { baseTsdownConfig } from '@lemoncode/tsdown-config/base'; export default { ...baseTsdownConfig, entry: ['src/index.ts'], + deps: { + neverBundle: ['puppeteer', 'puppeteer-core'], + alwaysBundle: /.*/, + }, }; diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 63b796f1..3961634e 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -47,7 +47,7 @@ "directory": "packages/vscode-extension" }, "engines": { - "vscode": "^1.116.0" + "vscode": "^1.115.0" }, "icon": "./assets/app-icon.webp", "galleryBanner": { @@ -55,12 +55,41 @@ "theme": "dark" }, "qna": false, - "activationEvents": [], + "activationEvents": [ + "onStartupFinished" + ], "contributes": { + "customEditors": [ + { + "viewType": "quickmock.editor", + "displayName": "QuickMock Wireframe Editor", + "selector": [ + { + "filenamePattern": "*.qm" + } + ], + "priority": "default" + } + ], "commands": [ { - "command": "quickmock.helloWorld", - "title": "Quickmock: Hello World" + "command": "quickmock-vscode-ai.helloWorld", + "title": "Hello World" + }, + { + "command": "quickmock.newWireframe", + "title": "QuickMock: New Wireframe" + }, + { + "command": "quickmock.connectMcp", + "title": "QuickMock: Connect MCP Server", + "category": "QuickMock" + } + ], + "mcpServerDefinitionProviders": [ + { + "id": "quickmock", + "label": "QuickMock Wireframe Tools" } ] } diff --git a/packages/vscode-extension/tsconfig.json b/packages/vscode-extension/tsconfig.json index 38115089..8dd7ade5 100644 --- a/packages/vscode-extension/tsconfig.json +++ b/packages/vscode-extension/tsconfig.json @@ -3,6 +3,7 @@ "include": ["src"], "compilerOptions": { "rootDir": "src", + "lib": ["DOM"], "paths": { "#*": ["./src/*"] } diff --git a/tooling/typescript/node.json b/tooling/typescript/node.json index 14b2f8e8..9f21fc22 100644 --- a/tooling/typescript/node.json +++ b/tooling/typescript/node.json @@ -3,6 +3,7 @@ "compilerOptions": { "target": "ES2024", "lib": ["ES2024"], + "types": ["node"], "noEmit": true } } From 22c7cc994d3cf7ac4b65dc5f609c6199aac785bb Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 10:15:09 +0200 Subject: [PATCH 02/23] feat(vscode-extension): initial migration and integration --- package-lock.json | 305 +++++++++++++++++- packages/vscode-extension/package.json | 12 +- .../vscode-extension/src/core/constants.ts | 3 + .../src/core/document-registry.ts | 17 + .../vscode-extension/src/core/protocol.ts | 37 +++ .../vscode-extension/src/editor/document.ts | 29 ++ .../vscode-extension/src/editor/handlers.ts | 45 +++ packages/vscode-extension/src/editor/panel.ts | 28 ++ .../vscode-extension/src/editor/provider.ts | 127 ++++++++ packages/vscode-extension/src/index.ts | 30 +- .../vscode-extension/src/mcp/mcp-command.ts | 76 +++++ .../src/mcp/mcp-registration.ts | 149 +++++++++ .../src/mcp/registry-server.ts | 82 +++++ .../src/mcp/server-definition-provider.ts | 57 ++++ .../vscode-extension/src/webview/bridge.ts | 31 ++ packages/vscode-extension/src/webview/main.ts | 10 + packages/vscode-extension/tsconfig.json | 2 +- packages/vscode-extension/tsdown.config.ts | 26 +- 18 files changed, 1045 insertions(+), 21 deletions(-) create mode 100644 packages/vscode-extension/src/core/constants.ts create mode 100644 packages/vscode-extension/src/core/document-registry.ts create mode 100644 packages/vscode-extension/src/core/protocol.ts create mode 100644 packages/vscode-extension/src/editor/document.ts create mode 100644 packages/vscode-extension/src/editor/handlers.ts create mode 100644 packages/vscode-extension/src/editor/panel.ts create mode 100644 packages/vscode-extension/src/editor/provider.ts create mode 100644 packages/vscode-extension/src/mcp/mcp-command.ts create mode 100644 packages/vscode-extension/src/mcp/mcp-registration.ts create mode 100644 packages/vscode-extension/src/mcp/registry-server.ts create mode 100644 packages/vscode-extension/src/mcp/server-definition-provider.ts create mode 100644 packages/vscode-extension/src/webview/bridge.ts create mode 100644 packages/vscode-extension/src/webview/main.ts diff --git a/package-lock.json b/package-lock.json index 7dc27a20..1f3b0c41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4420,6 +4420,23 @@ "node": ">=6.6.0" } }, + "node_modules/copy-file": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.1.0.tgz", + "integrity": "sha512-X8XDzyvYaA6msMyAM575CUoygY5b44QzLcGRKsK3MFmXcOvQa518dNPLsKYwkYsn72g3EiW+LE0ytd/FlqWmyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.11", + "p-event": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -4481,6 +4498,217 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cpy": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/cpy/-/cpy-13.2.1.tgz", + "integrity": "sha512-/H2B3WW9gccZJKjKoDZsIrDU3MkkHlxgheT82hUbInC5fEdi4+54zyYpFueZT9pLfr5ObrtgN4MsYYrmTmHzeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-file": "^11.1.0", + "globby": "^16.1.0", + "junk": "^4.0.1", + "micromatch": "^4.0.8", + "p-filter": "^4.1.0", + "p-map": "^7.0.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy-cli": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cpy-cli/-/cpy-cli-7.0.0.tgz", + "integrity": "sha512-uGCdhIxGfZcPXidCuT8w1jBknVXFx0un7NLjzqBZcdnkIWtLUnWMPk5TC37ceoVjwASLSNsRtTXXNTuFIyE2ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "cpy": "^13.2.0", + "globby": "^16.1.0", + "meow": "^14.0.0" + }, + "bin": { + "cpy": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy-cli/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy-cli/node_modules/globby": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", + "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy-cli/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/cpy-cli/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy-cli/node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy/node_modules/globby": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", + "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/cpy/node_modules/p-filter": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", + "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-map": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy/node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpy/node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6284,6 +6512,19 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -6532,6 +6773,19 @@ "npm": ">=6" } }, + "node_modules/junk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", + "integrity": "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -7185,6 +7439,19 @@ "node": ">= 0.8" } }, + "node_modules/meow": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -7695,6 +7962,22 @@ } } }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -7747,6 +8030,19 @@ "node": ">=6" } }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11200,16 +11496,19 @@ "name": "quickmock", "version": "0.0.1", "license": "MIT", + "dependencies": { + "@lemoncode/quickmock-mcp": "*" + }, "devDependencies": { "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", - "@types/node": "^24.12.2", "@types/vscode": "1.116.0", - "@vscode/vsce": "3.9.0" + "@vscode/vsce": "3.9.0", + "cpy-cli": "^7.0.0" }, "engines": { - "vscode": "^1.116.0" + "vscode": "^1.115.0" } }, "tooling/dev-cli": { diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 3961634e..72fa9fa0 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -8,18 +8,22 @@ }, "scripts": { "dev": "node --run build -- --watch --sourcemap", - "build": "tsdown", + "build": "tsdown && cpy ../mcp/dist/index.mjs dist --rename=mcp-server.mjs", "check-types": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage" }, + "dependencies": { + "@lemoncode/quickmock-mcp": "*" + }, "devDependencies": { "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", "@types/vscode": "1.116.0", - "@vscode/vsce": "3.9.0" + "@vscode/vsce": "3.9.0", + "cpy-cli": "^7.0.0" }, "publisher": "Lemoncoders", "displayName": "Quickmock", @@ -72,10 +76,6 @@ } ], "commands": [ - { - "command": "quickmock-vscode-ai.helloWorld", - "title": "Hello World" - }, { "command": "quickmock.newWireframe", "title": "QuickMock: New Wireframe" diff --git a/packages/vscode-extension/src/core/constants.ts b/packages/vscode-extension/src/core/constants.ts new file mode 100644 index 00000000..bd0b8980 --- /dev/null +++ b/packages/vscode-extension/src/core/constants.ts @@ -0,0 +1,3 @@ +export const QUICKMOCK_APP_URL = 'http://localhost:5173/editor.html?env=vscode'; // TODO: This should be an environment variable + +export const QUICKMOCK_APP_ORIGIN = new URL(QUICKMOCK_APP_URL).origin; diff --git a/packages/vscode-extension/src/core/document-registry.ts b/packages/vscode-extension/src/core/document-registry.ts new file mode 100644 index 00000000..87caee02 --- /dev/null +++ b/packages/vscode-extension/src/core/document-registry.ts @@ -0,0 +1,17 @@ +export class DocumentRegistry { + private readonly map = new Map(); + + set(fsPath: string, content: string): void { + this.map.set(fsPath, content); + } + + get(fsPath: string): string | undefined { + return this.map.get(fsPath); + } + + delete(fsPath: string): void { + this.map.delete(fsPath); + } +} + +export const documentRegistry = new DocumentRegistry(); diff --git a/packages/vscode-extension/src/core/protocol.ts b/packages/vscode-extension/src/core/protocol.ts new file mode 100644 index 00000000..75cdaf9a --- /dev/null +++ b/packages/vscode-extension/src/core/protocol.ts @@ -0,0 +1,37 @@ +export const HOST_MESSAGE_TYPE = { + LOAD: 'qm:load', + SAVED: 'qm:saved', + LOAD_FILE: 'LOAD_FILE', +} as const; + +export const APP_MESSAGE_TYPE = { + READY: 'qm:ready', + SAVE: 'qm:save', + RENDER_COMPLETE: 'qm:render-complete', + WEBVIEW_READY: 'WEBVIEW_READY', +} as const; + +export interface LoadFilePayload { + data: unknown; + fileName: string; +} + +export type HostMessage = + | { + type: typeof HOST_MESSAGE_TYPE.LOAD; + payload: { content: string; fileName: string }; + } + | { type: typeof HOST_MESSAGE_TYPE.SAVED } + | { type: typeof HOST_MESSAGE_TYPE.LOAD_FILE; payload: LoadFilePayload }; + +export type AppMessage = + | { type: typeof APP_MESSAGE_TYPE.READY } + | { type: typeof APP_MESSAGE_TYPE.SAVE; payload: { content: string } } + | { type: typeof APP_MESSAGE_TYPE.RENDER_COMPLETE } + | { type: typeof APP_MESSAGE_TYPE.WEBVIEW_READY }; + +export interface BridgeMessage { + type: string; + payload?: T; + requestId?: string; +} diff --git a/packages/vscode-extension/src/editor/document.ts b/packages/vscode-extension/src/editor/document.ts new file mode 100644 index 00000000..3a89f1d0 --- /dev/null +++ b/packages/vscode-extension/src/editor/document.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode'; + +export type QuickMockDocument = vscode.CustomDocument & { + readonly uri: vscode.Uri; + content: string; +}; + +export const openDocument = async ( + uri: vscode.Uri, + openContext: vscode.CustomDocumentOpenContext +): Promise => { + const source = openContext.backupId + ? vscode.Uri.parse(openContext.backupId) + : uri; + const content = await readFile(source); + return { uri, content, dispose: () => {} }; +}; + +export const readFile = async (uri: vscode.Uri): Promise => { + const bytes = await vscode.workspace.fs.readFile(uri); + return new TextDecoder().decode(bytes); +}; + +export const writeFile = async ( + uri: vscode.Uri, + content: string +): Promise => { + await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(content)); +}; diff --git a/packages/vscode-extension/src/editor/handlers.ts b/packages/vscode-extension/src/editor/handlers.ts new file mode 100644 index 00000000..18e69450 --- /dev/null +++ b/packages/vscode-extension/src/editor/handlers.ts @@ -0,0 +1,45 @@ +import { basename } from 'node:path'; +import { + APP_MESSAGE_TYPE, + type AppMessage, + HOST_MESSAGE_TYPE, + type HostMessage, +} from '#core/protocol'; +import { type QuickMockDocument, writeFile } from './document'; + +type PostMessageFn = (msg: HostMessage) => void; + +export const handleWebviewMessage = async ( + msg: AppMessage, + doc: QuickMockDocument, + postMessage: PostMessageFn +): Promise => { + switch (msg.type) { + case APP_MESSAGE_TYPE.READY: + postMessage({ + type: HOST_MESSAGE_TYPE.LOAD, + payload: { content: doc.content, fileName: basename(doc.uri.fsPath) }, + }); + break; + + case APP_MESSAGE_TYPE.WEBVIEW_READY: { + let data: unknown; + try { + data = JSON.parse(doc.content); + } catch { + data = doc.content; + } + postMessage({ + type: HOST_MESSAGE_TYPE.LOAD_FILE, + payload: { data, fileName: basename(doc.uri.fsPath) }, + }); + break; + } + + case APP_MESSAGE_TYPE.SAVE: + doc.content = msg.payload.content; + await writeFile(doc.uri, doc.content); + postMessage({ type: HOST_MESSAGE_TYPE.SAVED }); + break; + } +}; diff --git a/packages/vscode-extension/src/editor/panel.ts b/packages/vscode-extension/src/editor/panel.ts new file mode 100644 index 00000000..82c6799c --- /dev/null +++ b/packages/vscode-extension/src/editor/panel.ts @@ -0,0 +1,28 @@ +import * as vscode from 'vscode'; + +export const getHtml = ( + webview: vscode.Webview, + extensionUri: vscode.Uri +): string => { + const scriptUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'dist', 'webview.js') + ); + + // TODO: Parametrize the URL of the for CSP and the iframe using an environment variable + return /* html */ ` + + + + + + + + + + +`; +}; diff --git a/packages/vscode-extension/src/editor/provider.ts b/packages/vscode-extension/src/editor/provider.ts new file mode 100644 index 00000000..69c2230a --- /dev/null +++ b/packages/vscode-extension/src/editor/provider.ts @@ -0,0 +1,127 @@ +import { basename } from 'node:path'; +import * as vscode from 'vscode'; +import { documentRegistry } from '#core/document-registry'; +import { + type AppMessage, + HOST_MESSAGE_TYPE, + type HostMessage, +} from '#core/protocol'; +import { + openDocument, + type QuickMockDocument, + readFile, + writeFile, +} from './document'; +import { handleWebviewMessage } from './handlers'; +import { getHtml } from './panel'; + +export class QuickMockEditorProvider + implements vscode.CustomEditorProvider +{ + static register(context: vscode.ExtensionContext): vscode.Disposable { + return vscode.window.registerCustomEditorProvider( + 'quickmock.editor', + new QuickMockEditorProvider(context.extensionUri), + { + supportsMultipleEditorsPerDocument: false, + webviewOptions: { retainContextWhenHidden: true }, + } + ); + } + + constructor(private readonly extensionUri: vscode.Uri) {} + + private readonly _onDidChangeCustomDocument = new vscode.EventEmitter< + vscode.CustomDocumentContentChangeEvent + >(); + readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event; + + private readonly panels = new Map(); + + async openCustomDocument( + uri: vscode.Uri, + openContext: vscode.CustomDocumentOpenContext + ): Promise { + const doc = await openDocument(uri, openContext); + documentRegistry.set(doc.uri.fsPath, doc.content); + return doc; + } + + async saveCustomDocument( + doc: QuickMockDocument, + _cancel: vscode.CancellationToken + ): Promise { + await writeFile(doc.uri, doc.content); + } + + async saveCustomDocumentAs( + doc: QuickMockDocument, + dest: vscode.Uri, + _cancel: vscode.CancellationToken + ): Promise { + await writeFile(dest, doc.content); + } + + async revertCustomDocument( + doc: QuickMockDocument, + _cancel: vscode.CancellationToken + ): Promise { + doc.content = await readFile(doc.uri); + this.broadcast(doc, { + type: HOST_MESSAGE_TYPE.LOAD, + payload: { content: doc.content, fileName: basename(doc.uri.fsPath) }, + }); + } + + async backupCustomDocument( + doc: QuickMockDocument, + context: vscode.CustomDocumentBackupContext, + _cancel: vscode.CancellationToken + ): Promise { + await writeFile(context.destination, doc.content); + return { + id: context.destination.toString(), + delete: () => { + vscode.workspace.fs.delete(context.destination).then( + undefined, + () => {} + ); + }, + }; + } + + resolveCustomEditor( + doc: QuickMockDocument, + panel: vscode.WebviewPanel, + _token: vscode.CancellationToken + ): void { + const key = doc.uri.toString(); + this.panels.set(key, [...(this.panels.get(key) ?? []), panel]); + panel.onDidDispose(() => { + const remaining = (this.panels.get(key) ?? []).filter((p) => p !== panel); + this.panels.set(key, remaining); + if (remaining.length === 0) { + documentRegistry.delete(doc.uri.fsPath); + } + }); + + panel.webview.options = { + enableScripts: true, + localResourceRoots: [this.extensionUri], + }; + panel.webview.html = getHtml(panel.webview, this.extensionUri); + + panel.webview.onDidReceiveMessage(async (msg: AppMessage) => { + await handleWebviewMessage(msg, doc, (reply) => + panel.webview.postMessage(reply satisfies HostMessage) + ); + documentRegistry.set(doc.uri.fsPath, doc.content); + }); + } + + private broadcast(doc: QuickMockDocument, msg: HostMessage): void { + for (const panel of this.panels.get(doc.uri.toString()) ?? []) { + panel.webview.postMessage(msg); + } + } +} diff --git a/packages/vscode-extension/src/index.ts b/packages/vscode-extension/src/index.ts index c0a7ab1d..5fcde0b5 100644 --- a/packages/vscode-extension/src/index.ts +++ b/packages/vscode-extension/src/index.ts @@ -1,14 +1,32 @@ import * as vscode from 'vscode'; +import { QuickMockEditorProvider } from '#editor/provider'; +import { registerConnectMcpCommand } from '#mcp/mcp-command'; +import { registerMcpServer } from '#mcp/mcp-registration'; +import { RegistryServer } from '#mcp/registry-server'; +import { registerQuickMockMcpServerProvider } from '#mcp/server-definition-provider'; export const activate = (context: vscode.ExtensionContext) => { - const disposable = vscode.commands.registerCommand( - 'quickmock.helloWorld', - () => { - vscode.window.showInformationMessage('Quickmock extension is running!'); - } + context.subscriptions.push(QuickMockEditorProvider.register(context)); + + const registryServer = new RegistryServer(); + registryServer + .start(context) + .catch((err) => + console.error('[QuickMock] Failed to start MCP registry server:', err) + ); + + context.subscriptions.push(registerQuickMockMcpServerProvider(context)); + context.subscriptions.push(registerConnectMcpCommand(context)); + + registerMcpServer(context).catch((err) => + console.error('[QuickMock] Failed to register MCP server:', err) ); - context.subscriptions.push(disposable); + context.subscriptions.push( + vscode.commands.registerCommand('quickmock.newWireframe', () => { + vscode.window.showInformationMessage('New wireframe coming soon'); + }) + ); }; export const deactivate = () => {}; diff --git a/packages/vscode-extension/src/mcp/mcp-command.ts b/packages/vscode-extension/src/mcp/mcp-command.ts new file mode 100644 index 00000000..178e4424 --- /dev/null +++ b/packages/vscode-extension/src/mcp/mcp-command.ts @@ -0,0 +1,76 @@ +import * as vscode from 'vscode'; +import { registerMcpServer } from './mcp-registration'; + +const TOOLS = [ + { + name: 'list_wireframes', + description: 'List all .qm files in the workspace', + }, + { + name: 'get_wireframe_json', + description: 'Read the JSON content of a .qm file', + }, + { + name: 'capture_wireframe', + description: 'Render a .qm file and return a PNG screenshot', + }, +]; + +const STATUS_ICON: Record = { + registered: '$(check)', + skipped: '$(circle-slash)', + error: '$(error)', +}; + +export const registerConnectMcpCommand = ( + context: vscode.ExtensionContext +): vscode.Disposable => + vscode.commands.registerCommand('quickmock.connectMcp', async () => { + const serverPath = vscode.Uri.joinPath( + context.extensionUri, + 'dist', + 'mcp-server.mjs' + ).fsPath; + + const results = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'QuickMock: Connecting MCP server…', + cancellable: false, + }, + () => registerMcpServer(context) + ); + + const items: vscode.QuickPickItem[] = [ + { label: 'Providers', kind: vscode.QuickPickItemKind.Separator }, + ...results.map((r) => ({ + label: `${STATUS_ICON[r.status]} ${r.label}`, + description: r.status === 'registered' ? 'registered' : r.status, + detail: r.detail, + })), + { label: 'Available tools', kind: vscode.QuickPickItemKind.Separator }, + ...TOOLS.map((t) => ({ + label: `$(tools) ${t.name}`, + description: t.description, + })), + { label: 'Server', kind: vscode.QuickPickItemKind.Separator }, + { + label: '$(file-code) Server path', + description: serverPath, + detail: 'Click to copy', + }, + ]; + + const selected = await vscode.window.showQuickPick(items, { + title: 'QuickMock MCP Server', + placeHolder: 'Registration complete — select an item to copy its value', + matchOnDescription: true, + }); + + if (selected?.description === serverPath) { + await vscode.env.clipboard.writeText(serverPath); + vscode.window.showInformationMessage( + 'Server path copied to clipboard.' + ); + } + }); diff --git a/packages/vscode-extension/src/mcp/mcp-registration.ts b/packages/vscode-extension/src/mcp/mcp-registration.ts new file mode 100644 index 00000000..9adcfb5c --- /dev/null +++ b/packages/vscode-extension/src/mcp/mcp-registration.ts @@ -0,0 +1,149 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir, platform } from 'node:os'; +import { dirname, join } from 'node:path'; +import * as vscode from 'vscode'; + +const MCP_SERVER_KEY = 'quickmock'; + +export type RegistrationStatus = 'registered' | 'skipped' | 'error'; + +export interface RegistrationResult { + label: string; + status: RegistrationStatus; + detail?: string; +} + +interface McpFileConfig { + mcpServers?: Record; + [key: string]: unknown; +} + +interface FileTarget { + label: string; + path: string; +} + +const getFileTargets = (): FileTarget[] => { + const home = homedir(); + const os = platform(); + + const targets: FileTarget[] = [ + { label: 'Claude Code', path: join(home, '.claude.json') }, + { label: 'Cursor', path: join(home, '.cursor', 'mcp.json') }, + { + label: 'Windsurf', + path: join(home, '.codeium', 'windsurf', 'mcp_config.json'), + }, + ]; + + if (os === 'darwin') { + targets.push({ + label: 'Claude Desktop', + path: join( + home, + 'Library', + 'Application Support', + 'Claude', + 'claude_desktop_config.json' + ), + }); + } else if (os === 'win32') { + const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming'); + targets.push({ + label: 'Claude Desktop', + path: join(appData, 'Claude', 'claude_desktop_config.json'), + }); + } else { + targets.push({ + label: 'Claude Desktop', + path: join(home, '.config', 'Claude', 'claude_desktop_config.json'), + }); + } + + return targets; +}; + +const readFileConfig = (filePath: string): McpFileConfig => { + try { + return JSON.parse(readFileSync(filePath, 'utf-8')) as McpFileConfig; + } catch { + return {}; + } +}; + +const writeFileConfig = (filePath: string, data: McpFileConfig): void => { + const dir = dirname(filePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); +}; + +const registerInVSCode = async ( + entry: unknown +): Promise => { + try { + const config = vscode.workspace.getConfiguration('mcp'); + const servers = config.get>('servers') ?? {}; + servers[MCP_SERVER_KEY] = entry; + await config.update('servers', servers, vscode.ConfigurationTarget.Global); + return { label: 'VS Code / GitHub Copilot', status: 'registered' }; + } catch (err) { + return { + label: 'VS Code / GitHub Copilot', + status: 'error', + detail: String(err), + }; + } +}; + +const registerInFileTarget = ( + target: FileTarget, + entry: unknown +): RegistrationResult => { + const dir = dirname(target.path); + if (!existsSync(dir) && !existsSync(target.path)) { + return { label: target.label, status: 'skipped', detail: 'Not installed' }; + } + + try { + const config = readFileConfig(target.path); + if (!config.mcpServers) { + config.mcpServers = {}; + } + config.mcpServers[MCP_SERVER_KEY] = entry; + writeFileConfig(target.path, config); + return { label: target.label, status: 'registered' }; + } catch (err) { + return { label: target.label, status: 'error', detail: String(err) }; + } +}; + +export const registerMcpServer = async ( + context: vscode.ExtensionContext +): Promise => { + const serverPath = vscode.Uri.joinPath( + context.extensionUri, + 'dist', + 'mcp-server.mjs' + ).fsPath; + + const entry = { type: 'stdio', command: 'node', args: [serverPath], env: {} }; + + const results: RegistrationResult[] = [ + await registerInVSCode(entry), + ...getFileTargets().map((t) => registerInFileTarget(t, entry)), + ]; + + for (const r of results) { + if (r.status === 'registered') { + console.info(`[QuickMock] MCP registered — ${r.label}`); + } else if (r.status === 'error') { + console.error( + `[QuickMock] MCP registration failed — ${r.label}: ${r.detail}` + ); + } + } + + return results; +}; diff --git a/packages/vscode-extension/src/mcp/registry-server.ts b/packages/vscode-extension/src/mcp/registry-server.ts new file mode 100644 index 00000000..3b190f03 --- /dev/null +++ b/packages/vscode-extension/src/mcp/registry-server.ts @@ -0,0 +1,82 @@ +import { createHash } from 'node:crypto'; +import { unlinkSync, writeFileSync } from 'node:fs'; +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from 'node:http'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import * as vscode from 'vscode'; +import { documentRegistry } from '#core/document-registry'; + +export class RegistryServer { + private portFile: string | null = null; + + async start(context: vscode.ExtensionContext): Promise { + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspaceRoot) { + return; + } + + const hash = workspaceHash(workspaceRoot); + this.portFile = join(tmpdir(), `quickmock-${hash}.port`); + + const server = createServer((req, res) => this.handleRequest(req, res)); + + await new Promise((resolve, reject) => { + server.on('error', reject); + server.listen(0, '127.0.0.1', () => { + const addr = server.address() as { port: number }; + try { + writeFileSync(this.portFile!, String(addr.port), 'utf-8'); + } catch (err) { + reject(err); + return; + } + resolve(); + }); + }); + + context.subscriptions.push({ + dispose: () => { + server.close(); + if (this.portFile) { + try { + unlinkSync(this.portFile); + } catch {} + } + }, + }); + } + + private handleRequest(req: IncomingMessage, res: ServerResponse): void { + const url = new URL(req.url ?? '/', 'http://localhost'); + + if (url.pathname !== '/document') { + res.writeHead(404); + res.end(); + return; + } + + const path = url.searchParams.get('path'); + if (!path) { + res.writeHead(400); + res.end('Missing path parameter'); + return; + } + + const content = documentRegistry.get(path); + if (content === undefined) { + res.writeHead(404); + res.end('Document not open in editor'); + return; + } + + res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end(content); + } +} + +const workspaceHash = (root: string): string => + createHash('md5').update(root).digest('hex').slice(0, 8); diff --git a/packages/vscode-extension/src/mcp/server-definition-provider.ts b/packages/vscode-extension/src/mcp/server-definition-provider.ts new file mode 100644 index 00000000..66ab8cf6 --- /dev/null +++ b/packages/vscode-extension/src/mcp/server-definition-provider.ts @@ -0,0 +1,57 @@ +import * as vscode from 'vscode'; +import { QUICKMOCK_APP_URL } from '#core/constants'; + +const PROVIDER_ID = 'quickmock'; +const SERVER_LABEL = 'QuickMock Wireframe Tools'; +const SERVER_VERSION = '0.0.1'; + +const QUICKMOCK_HEADLESS_URL = `${QUICKMOCK_APP_URL}&headless=1`; + +export const registerQuickMockMcpServerProvider = ( + context: vscode.ExtensionContext +): vscode.Disposable => { + const didChangeDefinitions = new vscode.EventEmitter(); + console.info('[QuickMock] Registering MCP server definition provider'); + + context.subscriptions.push( + didChangeDefinitions, + vscode.workspace.onDidChangeWorkspaceFolders(() => + didChangeDefinitions.fire() + ) + ); + + return vscode.lm.registerMcpServerDefinitionProvider(PROVIDER_ID, { + onDidChangeMcpServerDefinitions: didChangeDefinitions.event, + provideMcpServerDefinitions: async (_token) => { + console.info('[QuickMock] Providing MCP server definitions'); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + console.info('[QuickMock] No workspace folder available for MCP server'); + return []; + } + + return [ + new vscode.McpStdioServerDefinition( + SERVER_LABEL, + 'node', + [ + vscode.Uri.joinPath( + context.extensionUri, + 'dist', + 'mcp-server.mjs' + ).fsPath, + ], + { + QM_WORKSPACE_ROOT: workspaceFolder.uri.fsPath, + QM_APP_URL: QUICKMOCK_HEADLESS_URL, + }, + SERVER_VERSION + ), + ]; + }, + resolveMcpServerDefinition: async (server, _token) => { + console.info('[QuickMock] Resolving MCP server definition'); + return server; + }, + }); +}; diff --git a/packages/vscode-extension/src/webview/bridge.ts b/packages/vscode-extension/src/webview/bridge.ts new file mode 100644 index 00000000..ea0ef317 --- /dev/null +++ b/packages/vscode-extension/src/webview/bridge.ts @@ -0,0 +1,31 @@ +import { QUICKMOCK_APP_ORIGIN } from '#core/constants'; +import { + type AppMessage, + HOST_MESSAGE_TYPE, + type HostMessage, +} from '#core/protocol'; + +declare function acquireVsCodeApi(): { postMessage(msg: AppMessage): void }; + +const vscode = acquireVsCodeApi(); + +const QUICKMOCK_ORIGIN = QUICKMOCK_APP_ORIGIN; + +const FORWARDED_TO_IFRAME: ReadonlySet = new Set([ + HOST_MESSAGE_TYPE.LOAD, + HOST_MESSAGE_TYPE.SAVED, + HOST_MESSAGE_TYPE.LOAD_FILE, +]); + +export const setupBridge = (iframe: HTMLIFrameElement): void => { + window.addEventListener('message', (event: MessageEvent) => { + if (event.origin === QUICKMOCK_ORIGIN) { + vscode.postMessage(event.data as AppMessage); + } else { + const msg = event.data as HostMessage; + if (FORWARDED_TO_IFRAME.has(msg.type)) { + iframe.contentWindow?.postMessage(msg, QUICKMOCK_ORIGIN); + } + } + }); +}; diff --git a/packages/vscode-extension/src/webview/main.ts b/packages/vscode-extension/src/webview/main.ts new file mode 100644 index 00000000..6b931021 --- /dev/null +++ b/packages/vscode-extension/src/webview/main.ts @@ -0,0 +1,10 @@ +import { QUICKMOCK_APP_URL } from '#core/constants'; +import { setupBridge } from './bridge'; + +const iframe = document.createElement('iframe'); +iframe.src = QUICKMOCK_APP_URL; +iframe.allow = 'clipboard-read; clipboard-write'; +iframe.title = 'QuickMock Application'; +document.body.appendChild(iframe); + +setupBridge(iframe); diff --git a/packages/vscode-extension/tsconfig.json b/packages/vscode-extension/tsconfig.json index 8dd7ade5..e9acf234 100644 --- a/packages/vscode-extension/tsconfig.json +++ b/packages/vscode-extension/tsconfig.json @@ -3,7 +3,7 @@ "include": ["src"], "compilerOptions": { "rootDir": "src", - "lib": ["DOM"], + "lib": ["ES2024", "DOM"], "paths": { "#*": ["./src/*"] } diff --git a/packages/vscode-extension/tsdown.config.ts b/packages/vscode-extension/tsdown.config.ts index 6462135d..7e50bd88 100644 --- a/packages/vscode-extension/tsdown.config.ts +++ b/packages/vscode-extension/tsdown.config.ts @@ -1,7 +1,23 @@ import { baseTsdownConfig } from '@lemoncode/tsdown-config/base'; +import { defineConfig } from 'tsdown'; -export default { - ...baseTsdownConfig, - entry: ['src/index.ts'], - external: ['vscode'], -}; +export default defineConfig([ + { + ...baseTsdownConfig, + entry: ['src/index.ts'], + deps: { neverBundle: ['vscode'] }, + }, + { + entry: { webview: 'src/webview/main.ts' }, + format: 'iife', + platform: 'browser', + outDir: 'dist', + target: 'es2022', + sourcemap: true, + clean: false, + dts: false, + outputOptions: { + entryFileNames: '[name].js', + }, + }, +]); From 46569615dcb0b57f2519a760fa3ebdb36766615a Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 10:31:32 +0200 Subject: [PATCH 03/23] feat(vscode-extension): implement script to copy MCP build output --- package-lock.json | 299 +----------------- packages/mcp/package.json | 3 +- packages/vscode-extension/package.json | 5 +- .../vscode-extension/scripts/copy-mcp.mjs | 16 + 4 files changed, 21 insertions(+), 302 deletions(-) create mode 100644 packages/vscode-extension/scripts/copy-mcp.mjs diff --git a/package-lock.json b/package-lock.json index 1f3b0c41..466f7d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4420,23 +4420,6 @@ "node": ">=6.6.0" } }, - "node_modules/copy-file": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.1.0.tgz", - "integrity": "sha512-X8XDzyvYaA6msMyAM575CUoygY5b44QzLcGRKsK3MFmXcOvQa518dNPLsKYwkYsn72g3EiW+LE0ytd/FlqWmyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.11", - "p-event": "^6.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -4498,217 +4481,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cpy": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/cpy/-/cpy-13.2.1.tgz", - "integrity": "sha512-/H2B3WW9gccZJKjKoDZsIrDU3MkkHlxgheT82hUbInC5fEdi4+54zyYpFueZT9pLfr5ObrtgN4MsYYrmTmHzeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "copy-file": "^11.1.0", - "globby": "^16.1.0", - "junk": "^4.0.1", - "micromatch": "^4.0.8", - "p-filter": "^4.1.0", - "p-map": "^7.0.4" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy-cli": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cpy-cli/-/cpy-cli-7.0.0.tgz", - "integrity": "sha512-uGCdhIxGfZcPXidCuT8w1jBknVXFx0un7NLjzqBZcdnkIWtLUnWMPk5TC37ceoVjwASLSNsRtTXXNTuFIyE2ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "cpy": "^13.2.0", - "globby": "^16.1.0", - "meow": "^14.0.0" - }, - "bin": { - "cpy": "cli.js" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy-cli/node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy-cli/node_modules/globby": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", - "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.5", - "is-path-inside": "^4.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy-cli/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/cpy-cli/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy-cli/node_modules/unicorn-magic": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy/node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy/node_modules/globby": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", - "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.5", - "is-path-inside": "^4.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/cpy/node_modules/p-filter": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", - "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-map": "^7.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy/node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cpy/node_modules/unicorn-magic": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6512,19 +6284,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -6773,19 +6532,6 @@ "npm": ">=6" } }, - "node_modules/junk": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", - "integrity": "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -7439,19 +7185,6 @@ "node": ">= 0.8" } }, - "node_modules/meow": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", - "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -7962,22 +7695,6 @@ } } }, - "node_modules/p-event": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", - "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-timeout": "^6.1.2" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -8030,19 +7747,6 @@ "node": ">=6" } }, - "node_modules/p-timeout": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", - "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11504,8 +11208,7 @@ "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", "@types/vscode": "1.116.0", - "@vscode/vsce": "3.9.0", - "cpy-cli": "^7.0.0" + "@vscode/vsce": "3.9.0" }, "engines": { "vscode": "^1.115.0" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 86494abc..58be10a1 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -6,7 +6,8 @@ ".": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" - } + }, + "./package.json": "./package.json" }, "imports": { "#*": "./src/*" diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 72fa9fa0..ac03fffe 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -8,7 +8,7 @@ }, "scripts": { "dev": "node --run build -- --watch --sourcemap", - "build": "tsdown && cpy ../mcp/dist/index.mjs dist --rename=mcp-server.mjs", + "build": "tsdown && node scripts/copy-mcp.mjs", "check-types": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", @@ -22,8 +22,7 @@ "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", "@types/vscode": "1.116.0", - "@vscode/vsce": "3.9.0", - "cpy-cli": "^7.0.0" + "@vscode/vsce": "3.9.0" }, "publisher": "Lemoncoders", "displayName": "Quickmock", diff --git a/packages/vscode-extension/scripts/copy-mcp.mjs b/packages/vscode-extension/scripts/copy-mcp.mjs new file mode 100644 index 00000000..d7d6d104 --- /dev/null +++ b/packages/vscode-extension/scripts/copy-mcp.mjs @@ -0,0 +1,16 @@ +import { copyFile, mkdir } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const here = dirname(fileURLToPath(import.meta.url)); + +const mcpDir = dirname(require.resolve('@lemoncode/quickmock-mcp/package.json')); +const source = join(mcpDir, 'dist', 'index.mjs'); + +const distDir = join(here, '..', 'dist'); +const target = join(distDir, 'mcp-server.mjs'); + +await mkdir(distDir, { recursive: true }); +await copyFile(source, target); From 45f329f601c90107cb472733be4fbf31bc4f9cd8 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 10:45:59 +0200 Subject: [PATCH 04/23] feat(vscode-extension): refactor app URL handling to use environment variable --- .env.example | 4 ++++ packages/vscode-extension/src/core/constants.ts | 5 ++--- packages/vscode-extension/src/editor/panel.ts | 13 +++++++++---- packages/vscode-extension/src/editor/provider.ts | 7 ++++++- packages/vscode-extension/src/webview/bridge.ts | 12 ++++++------ packages/vscode-extension/src/webview/main.ts | 12 +++++++++--- 6 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..a2f0fa59 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# URL of the QuickMock web app loaded in the VS Code webview and by the MCP renderer. +# Default (local dev): http://localhost:5173/editor.html?env=vscode +# Production example: https://quickmock.net/editor.html?env=vscode +QM_APP_URL=http://localhost:5173/editor.html?env=vscode diff --git a/packages/vscode-extension/src/core/constants.ts b/packages/vscode-extension/src/core/constants.ts index bd0b8980..05ab2132 100644 --- a/packages/vscode-extension/src/core/constants.ts +++ b/packages/vscode-extension/src/core/constants.ts @@ -1,3 +1,2 @@ -export const QUICKMOCK_APP_URL = 'http://localhost:5173/editor.html?env=vscode'; // TODO: This should be an environment variable - -export const QUICKMOCK_APP_ORIGIN = new URL(QUICKMOCK_APP_URL).origin; +export const QUICKMOCK_APP_URL = + process.env.QM_APP_URL ?? 'http://localhost:5173/editor.html?env=vscode'; diff --git a/packages/vscode-extension/src/editor/panel.ts b/packages/vscode-extension/src/editor/panel.ts index 82c6799c..f943115f 100644 --- a/packages/vscode-extension/src/editor/panel.ts +++ b/packages/vscode-extension/src/editor/panel.ts @@ -1,19 +1,24 @@ import * as vscode from 'vscode'; +const escapeAttr = (value: string): string => + value.replace(/&/g, '&').replace(/"/g, '"'); + export const getHtml = ( webview: vscode.Webview, - extensionUri: vscode.Uri + extensionUri: vscode.Uri, + appUrl: string ): string => { const scriptUri = webview.asWebviewUri( vscode.Uri.joinPath(extensionUri, 'dist', 'webview.js') ); + const appOrigin = new URL(appUrl).origin; + const wsOrigin = appOrigin.replace(/^http/, 'ws'); - // TODO: Parametrize the URL of the for CSP and the iframe using an environment variable return /* html */ ` - + - + `; diff --git a/packages/vscode-extension/src/editor/provider.ts b/packages/vscode-extension/src/editor/provider.ts index 69c2230a..cd012339 100644 --- a/packages/vscode-extension/src/editor/provider.ts +++ b/packages/vscode-extension/src/editor/provider.ts @@ -1,5 +1,6 @@ import { basename } from 'node:path'; import * as vscode from 'vscode'; +import { QUICKMOCK_APP_URL } from '#core/constants'; import { documentRegistry } from '#core/document-registry'; import { type AppMessage, @@ -109,7 +110,11 @@ export class QuickMockEditorProvider enableScripts: true, localResourceRoots: [this.extensionUri], }; - panel.webview.html = getHtml(panel.webview, this.extensionUri); + panel.webview.html = getHtml( + panel.webview, + this.extensionUri, + QUICKMOCK_APP_URL + ); panel.webview.onDidReceiveMessage(async (msg: AppMessage) => { await handleWebviewMessage(msg, doc, (reply) => diff --git a/packages/vscode-extension/src/webview/bridge.ts b/packages/vscode-extension/src/webview/bridge.ts index ea0ef317..7ef52506 100644 --- a/packages/vscode-extension/src/webview/bridge.ts +++ b/packages/vscode-extension/src/webview/bridge.ts @@ -1,4 +1,3 @@ -import { QUICKMOCK_APP_ORIGIN } from '#core/constants'; import { type AppMessage, HOST_MESSAGE_TYPE, @@ -9,22 +8,23 @@ declare function acquireVsCodeApi(): { postMessage(msg: AppMessage): void }; const vscode = acquireVsCodeApi(); -const QUICKMOCK_ORIGIN = QUICKMOCK_APP_ORIGIN; - const FORWARDED_TO_IFRAME: ReadonlySet = new Set([ HOST_MESSAGE_TYPE.LOAD, HOST_MESSAGE_TYPE.SAVED, HOST_MESSAGE_TYPE.LOAD_FILE, ]); -export const setupBridge = (iframe: HTMLIFrameElement): void => { +export const setupBridge = ( + iframe: HTMLIFrameElement, + appOrigin: string +): void => { window.addEventListener('message', (event: MessageEvent) => { - if (event.origin === QUICKMOCK_ORIGIN) { + if (event.origin === appOrigin) { vscode.postMessage(event.data as AppMessage); } else { const msg = event.data as HostMessage; if (FORWARDED_TO_IFRAME.has(msg.type)) { - iframe.contentWindow?.postMessage(msg, QUICKMOCK_ORIGIN); + iframe.contentWindow?.postMessage(msg, appOrigin); } } }); diff --git a/packages/vscode-extension/src/webview/main.ts b/packages/vscode-extension/src/webview/main.ts index 6b931021..eaf70d3b 100644 --- a/packages/vscode-extension/src/webview/main.ts +++ b/packages/vscode-extension/src/webview/main.ts @@ -1,10 +1,16 @@ -import { QUICKMOCK_APP_URL } from '#core/constants'; import { setupBridge } from './bridge'; +const appUrl = document.body.dataset.appUrl; +if (!appUrl) { + throw new Error('[QuickMock] Missing data-app-url attribute on '); +} + +const appOrigin = new URL(appUrl).origin; + const iframe = document.createElement('iframe'); -iframe.src = QUICKMOCK_APP_URL; +iframe.src = appUrl; iframe.allow = 'clipboard-read; clipboard-write'; iframe.title = 'QuickMock Application'; document.body.appendChild(iframe); -setupBridge(iframe); +setupBridge(iframe, appOrigin); From d6d3f5c45125615c1e1fcd0ef7794a0cc650bc05 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 10:51:06 +0200 Subject: [PATCH 05/23] feat(mcp): update type imports and enhance data URL parsing logic --- packages/mcp/src/renderer/bridge.server.ts | 2 +- .../tools/get-wireframe-assets/get-wireframe-assets.handler.ts | 2 +- packages/mcp/tsconfig.json | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mcp/src/renderer/bridge.server.ts b/packages/mcp/src/renderer/bridge.server.ts index c818230c..848d882f 100644 --- a/packages/mcp/src/renderer/bridge.server.ts +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -1,5 +1,5 @@ import { createServer, type Server } from 'node:http' -import { AddressInfo } from 'node:net' +import type { AddressInfo } from 'node:net' import { QUICKMOCK_URL } from './renderer.consts' export interface BridgeServer { diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts index 0be539d0..9677542b 100644 --- a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts @@ -11,7 +11,7 @@ interface ParsedDataUrl { function parseDataUrl(src: string): ParsedDataUrl | null { const match = src.match(/^data:([^;]+);base64,(.+)$/) - if (!match) { + if (!match?.[1] || !match[2]) { return null } return { mimeType: match[1], base64: match[2] } diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json index 38115089..e9acf234 100644 --- a/packages/mcp/tsconfig.json +++ b/packages/mcp/tsconfig.json @@ -3,6 +3,7 @@ "include": ["src"], "compilerOptions": { "rootDir": "src", + "lib": ["ES2024", "DOM"], "paths": { "#*": ["./src/*"] } From 17c754d708f3ce42771494b22b54d1b035965d0e Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 11:32:41 +0200 Subject: [PATCH 06/23] feat: implement quickmock bridge protocol and integrate VSCode extension features --- apps/web/package.json | 1 + apps/web/src/App.tsx | 3 ++ .../utils/compute-content-bbox.utils.ts | 29 +++++++++++ apps/web/src/common/utils/env.utils.ts | 7 +++ .../src/common/utils/vscode-bridge.utils.ts | 52 +++++++++++++++++++ .../use-headless-render-complete.hook.ts | 34 ++++++++++++ .../core/vscode/use-vscode-auto-save.hook.ts | 49 +++++++++++++++++ .../core/vscode/use-vscode-file-load.hook.ts | 49 +++++++++++++++++ .../src/core/vscode/use-vscode-sync.hook.ts | 13 +++++ apps/web/src/core/vscode/vscode-sync.utils.ts | 17 ++++++ apps/web/src/scenes/main.scene.tsx | 21 +++++--- package-lock.json | 14 +++++ packages/bridge-protocol/package.json | 15 ++++++ .../src/index.ts} | 33 +++++++----- packages/bridge-protocol/tsconfig.json | 10 ++++ packages/mcp/package.json | 1 + packages/mcp/src/renderer/bridge.server.ts | 8 ++- packages/vscode-extension/package.json | 1 + .../vscode-extension/src/editor/handlers.ts | 2 +- .../vscode-extension/src/editor/provider.ts | 2 +- .../vscode-extension/src/webview/bridge.ts | 2 +- packages/vscode-extension/tsdown.config.ts | 1 + 22 files changed, 341 insertions(+), 23 deletions(-) create mode 100644 apps/web/src/common/utils/compute-content-bbox.utils.ts create mode 100644 apps/web/src/common/utils/env.utils.ts create mode 100644 apps/web/src/common/utils/vscode-bridge.utils.ts create mode 100644 apps/web/src/core/vscode/use-headless-render-complete.hook.ts create mode 100644 apps/web/src/core/vscode/use-vscode-auto-save.hook.ts create mode 100644 apps/web/src/core/vscode/use-vscode-file-load.hook.ts create mode 100644 apps/web/src/core/vscode/use-vscode-sync.hook.ts create mode 100644 apps/web/src/core/vscode/vscode-sync.utils.ts create mode 100644 packages/bridge-protocol/package.json rename packages/{vscode-extension/src/core/protocol.ts => bridge-protocol/src/index.ts} (65%) create mode 100644 packages/bridge-protocol/tsconfig.json diff --git a/apps/web/package.json b/apps/web/package.json index fefcefe2..ef2f2b64 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.7.10", + "@lemoncode/quickmock-bridge-protocol": "*", "@fontsource-variable/montserrat": "5.0.20", "@fontsource/balsamiq-sans": "5.0.21", "@uiw/react-color-chrome": "2.10.1", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index d2d93317..977a45d8 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,7 +1,10 @@ import { ModalDialogComponent } from './common/components/modal-dialog'; +import { useVSCodeSync } from '#core/vscode/use-vscode-sync.hook'; import { MainScene } from './scenes/main.scene'; function App() { + useVSCodeSync(); + return ( <> diff --git a/apps/web/src/common/utils/compute-content-bbox.utils.ts b/apps/web/src/common/utils/compute-content-bbox.utils.ts new file mode 100644 index 00000000..f6a51709 --- /dev/null +++ b/apps/web/src/common/utils/compute-content-bbox.utils.ts @@ -0,0 +1,29 @@ +import type { ContentBbox } from '@lemoncode/quickmock-bridge-protocol'; +import type { useCanvasContext } from '#core/providers'; + +const CONTENT_PADDING = 16; + +export function computeContentBbox( + shapes: ReturnType['shapes'], + stageRef: ReturnType['stageRef'] +): ContentBbox | undefined { + const stage = stageRef.current; + if (!stage || shapes.length === 0) return undefined; + + const scale = stage.scaleX(); + const stageX = stage.x(); + const stageY = stage.y(); + const container = stage.container().getBoundingClientRect(); + + const minX = Math.min(...shapes.map(s => s.x)); + const minY = Math.min(...shapes.map(s => s.y)); + const maxX = Math.max(...shapes.map(s => s.x + s.width)); + const maxY = Math.max(...shapes.map(s => s.y + s.height)); + + return { + x: Math.max(0, container.left + stageX + minX * scale - CONTENT_PADDING), + y: Math.max(0, container.top + stageY + minY * scale - CONTENT_PADDING), + width: (maxX - minX) * scale + CONTENT_PADDING * 2, + height: (maxY - minY) * scale + CONTENT_PADDING * 2, + }; +} diff --git a/apps/web/src/common/utils/env.utils.ts b/apps/web/src/common/utils/env.utils.ts new file mode 100644 index 00000000..f0ce1e96 --- /dev/null +++ b/apps/web/src/common/utils/env.utils.ts @@ -0,0 +1,7 @@ +export const isVSCodeEnv = (): boolean => { + return new URLSearchParams(window.location.search).get('env') === 'vscode'; +}; + +export const isHeadlessEnv = (): boolean => { + return new URLSearchParams(window.location.search).get('headless') === '1'; +}; diff --git a/apps/web/src/common/utils/vscode-bridge.utils.ts b/apps/web/src/common/utils/vscode-bridge.utils.ts new file mode 100644 index 00000000..b6b41376 --- /dev/null +++ b/apps/web/src/common/utils/vscode-bridge.utils.ts @@ -0,0 +1,52 @@ +import type { + AppMessage, + HostMessage, + PayloadOf, +} from '@lemoncode/quickmock-bridge-protocol'; +import { isVSCodeEnv } from './env.utils'; + +type HandlerFor = ( + payload: PayloadOf +) => void; + +type AnyHandler = (payload: unknown) => void; + +const handlers = new Map>(); + +export const sendToExtension = (msg: AppMessage): void => { + if (!isVSCodeEnv()) return; + window.parent.postMessage(msg, '*'); +}; + +export const onMessage = ( + type: T, + handler: HandlerFor +): (() => void) => { + if (!isVSCodeEnv()) return () => {}; + + const existing = handlers.get(type) ?? new Set(); + existing.add(handler as AnyHandler); + handlers.set(type, existing); + + return () => { + const set = handlers.get(type); + if (!set) return; + set.delete(handler as AnyHandler); + if (set.size === 0) handlers.delete(type); + }; +}; + +if (isVSCodeEnv()) { + window.addEventListener('message', (event: MessageEvent) => { + if (event.source !== window.parent) return; + + const msg = event.data as Partial | undefined; + if (!msg?.type) return; + + const set = handlers.get(msg.type); + if (!set) return; + + const payload = (msg as { payload?: unknown }).payload; + for (const handler of set) handler(payload); + }); +} diff --git a/apps/web/src/core/vscode/use-headless-render-complete.hook.ts b/apps/web/src/core/vscode/use-headless-render-complete.hook.ts new file mode 100644 index 00000000..c271ce6f --- /dev/null +++ b/apps/web/src/core/vscode/use-headless-render-complete.hook.ts @@ -0,0 +1,34 @@ +import { computeContentBbox } from '#common/utils/compute-content-bbox.utils.ts'; +import { isHeadlessEnv } from '#common/utils/env.utils.ts'; +import { sendToExtension } from '#common/utils/vscode-bridge.utils.ts'; +import { useCanvasContext } from '#core/providers'; +import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol'; +import { useEffect } from 'react'; + +export function useHeadlessRenderComplete(hasReceivedFileRef: { + current: boolean; +}): void { + const { howManyLoadedDocuments, shapes, stageRef } = useCanvasContext(); + + useEffect(() => { + if (!isHeadlessEnv() || !hasReceivedFileRef.current) return; + + let innerRafId = 0; + // Double rAF: the first frame runs after React commits; the second waits + // for Konva to paint the updated canvas, so Puppeteer's screenshot reflects it. + // There was a previous issue when the canvas was blank because the screenshot ran before Konva painted. + const outerRafId = requestAnimationFrame(() => { + innerRafId = requestAnimationFrame(() => { + sendToExtension({ + type: APP_MESSAGE_TYPE.RENDER_COMPLETE, + payload: computeContentBbox(shapes, stageRef), + }); + }); + }); + + return () => { + cancelAnimationFrame(outerRafId); + cancelAnimationFrame(innerRafId); + }; + }, [howManyLoadedDocuments]); +} diff --git a/apps/web/src/core/vscode/use-vscode-auto-save.hook.ts b/apps/web/src/core/vscode/use-vscode-auto-save.hook.ts new file mode 100644 index 00000000..47a9437d --- /dev/null +++ b/apps/web/src/core/vscode/use-vscode-auto-save.hook.ts @@ -0,0 +1,49 @@ +import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts'; +import { sendToExtension } from '#common/utils/vscode-bridge.utils.ts'; +import { useCanvasContext } from '#core/providers'; +import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol'; +import { useEffect, useRef } from 'react'; +import { serializeDocument } from './vscode-sync.utils'; + +const AUTO_SAVE_DEBOUNCE_MS = 500; + +export function useVSCodeAutoSave(hasReceivedFileRef: { + current: boolean; +}): void { + const { fullDocument, howManyLoadedDocuments } = useCanvasContext(); + + const prevLoadCountRef = useRef(howManyLoadedDocuments); + const lastSavedContentRef = useRef(''); + const debounceTimerRef = useRef | null>(null); + + useEffect(() => { + if (!isVSCodeEnv() || isHeadlessEnv() || !hasReceivedFileRef.current) + return; + + if (prevLoadCountRef.current !== howManyLoadedDocuments) { + prevLoadCountRef.current = howManyLoadedDocuments; + lastSavedContentRef.current = serializeDocument(fullDocument); + return; + } + + const content = serializeDocument(fullDocument); + + if (content === lastSavedContentRef.current) return; + + debounceTimerRef.current = setTimeout(() => { + sendToExtension({ + type: APP_MESSAGE_TYPE.SAVE, + payload: { content }, + }); + lastSavedContentRef.current = content; + debounceTimerRef.current = null; + }, AUTO_SAVE_DEBOUNCE_MS); + + return () => { + if (debounceTimerRef.current !== null) { + clearTimeout(debounceTimerRef.current); + debounceTimerRef.current = null; + } + }; + }, [fullDocument, howManyLoadedDocuments]); +} diff --git a/apps/web/src/core/vscode/use-vscode-file-load.hook.ts b/apps/web/src/core/vscode/use-vscode-file-load.hook.ts new file mode 100644 index 00000000..e0aa85db --- /dev/null +++ b/apps/web/src/core/vscode/use-vscode-file-load.hook.ts @@ -0,0 +1,49 @@ +import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts'; +import { onMessage, sendToExtension } from '#common/utils/vscode-bridge.utils.ts'; +import { QuickMockFileContract } from '#core/local-disk/local-disk.model'; +import { useCanvasContext } from '#core/providers'; +import { + APP_MESSAGE_TYPE, + HOST_MESSAGE_TYPE, + type LoadFilePayload, +} from '@lemoncode/quickmock-bridge-protocol'; +import { useEffect, useRef } from 'react'; +import { deserializeDocument } from './vscode-sync.utils'; + +export function useVSCodeFileLoad(): { current: boolean } { + const { loadDocument, setFileName } = useCanvasContext(); + + const loadDocumentRef = useRef(loadDocument); + const setFileNameRef = useRef(setFileName); + useEffect(() => { + loadDocumentRef.current = loadDocument; + setFileNameRef.current = setFileName; + }); + + const hasReceivedFileRef = useRef(false); + + useEffect(() => { + if (!isVSCodeEnv()) return; + + const unsubscribe = onMessage( + HOST_MESSAGE_TYPE.LOAD_FILE, + (payload: LoadFilePayload) => { + hasReceivedFileRef.current = true; + setFileNameRef.current(payload.fileName); + loadDocumentRef.current( + deserializeDocument(payload.data as QuickMockFileContract) + ); + } + ); + + sendToExtension({ + type: isHeadlessEnv() + ? APP_MESSAGE_TYPE.READY + : APP_MESSAGE_TYPE.WEBVIEW_READY, + }); + + return unsubscribe; + }, []); + + return hasReceivedFileRef; +} diff --git a/apps/web/src/core/vscode/use-vscode-sync.hook.ts b/apps/web/src/core/vscode/use-vscode-sync.hook.ts new file mode 100644 index 00000000..ce07fd9e --- /dev/null +++ b/apps/web/src/core/vscode/use-vscode-sync.hook.ts @@ -0,0 +1,13 @@ +import { useHeadlessRenderComplete } from './use-headless-render-complete.hook'; +import { useVSCodeAutoSave } from './use-vscode-auto-save.hook'; +import { useVSCodeFileLoad } from './use-vscode-file-load.hook'; + +/** + * Wires the full VS Code webview bridge. Each inner hook no-ops when not + * running inside a webview, so this can be called unconditionally. + */ +export function useVSCodeSync(): void { + const hasReceivedFileRef = useVSCodeFileLoad(); + useVSCodeAutoSave(hasReceivedFileRef); + useHeadlessRenderComplete(hasReceivedFileRef); +} diff --git a/apps/web/src/core/vscode/vscode-sync.utils.ts b/apps/web/src/core/vscode/vscode-sync.utils.ts new file mode 100644 index 00000000..9a41fb24 --- /dev/null +++ b/apps/web/src/core/vscode/vscode-sync.utils.ts @@ -0,0 +1,17 @@ +import { QuickMockFileContract } from '#core/local-disk/local-disk.model'; +import { + mapFromQuickMockFileDocumentToApplicationDocument, + mapFromQuickMockFileDocumentToApplicationDocumentV0_1, + mapFromShapesArrayToQuickMockFileDocument, +} from '#core/local-disk/shapes-to-document.mapper'; +import { DocumentModel } from '#core/providers/canvas/canvas.model'; + +export function deserializeDocument(data: QuickMockFileContract) { + return data.version === '0.1' + ? mapFromQuickMockFileDocumentToApplicationDocumentV0_1(data) + : mapFromQuickMockFileDocumentToApplicationDocument(data); +} + +export function serializeDocument(document: DocumentModel): string { + return JSON.stringify(mapFromShapesArrayToQuickMockFileDocument(document)); +} diff --git a/apps/web/src/scenes/main.scene.tsx b/apps/web/src/scenes/main.scene.tsx index aa2eef41..8cb9fc2c 100644 --- a/apps/web/src/scenes/main.scene.tsx +++ b/apps/web/src/scenes/main.scene.tsx @@ -1,27 +1,36 @@ import { MainLayout } from '#layout/main.layout'; import classes from './main.module.css'; +import { isHeadlessEnv } from '#common/utils/env.utils.ts'; +import { useInteractionModeContext } from '#core/providers'; import { + BasicShapesGalleryPod, CanvasPod, - ToolbarPod, - ContainerGalleryPod, ComponentGalleryPod, - BasicShapesGalleryPod, + ContainerGalleryPod, + LowWireframeGalleryPod, RichComponentsGalleryPod, TextComponetGalleryPod, - LowWireframeGalleryPod, + ToolbarPod, } from '#pods'; -import { PropertiesPod } from '#pods/properties'; import { FooterPod } from '#pods/footer/footer.pod'; +import { PropertiesPod } from '#pods/properties'; import { ThumbPagesPod } from '#pods/thumb-pages'; import { useAccordionSectionVisibility } from './accordion-section-visibility.hook'; -import { useInteractionModeContext } from '#core/providers'; export const MainScene = () => { const { isThumbPagesPodOpen, thumbPagesPodRef } = useAccordionSectionVisibility(); const { interactionMode } = useInteractionModeContext(); + if (isHeadlessEnv()) { + return ( +
+ +
+ ); + } + return ( {interactionMode === 'view' && ( diff --git a/package-lock.json b/package-lock.json index 466f7d31..0f09f42c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@atlaskit/pragmatic-drag-and-drop": "1.7.10", "@fontsource-variable/montserrat": "5.0.20", "@fontsource/balsamiq-sans": "5.0.21", + "@lemoncode/quickmock-bridge-protocol": "*", "@uiw/react-color-chrome": "2.10.1", "html2canvas": "1.4.1", "immer": "10.1.1", @@ -1271,6 +1272,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lemoncode/quickmock-bridge-protocol": { + "resolved": "packages/bridge-protocol", + "link": true + }, "node_modules/@lemoncode/quickmock-mcp": { "resolved": "packages/mcp", "link": true @@ -11164,10 +11169,18 @@ "zod": "^3.25.28 || ^4" } }, + "packages/bridge-protocol": { + "name": "@lemoncode/quickmock-bridge-protocol", + "version": "0.0.0", + "devDependencies": { + "@lemoncode/typescript-config": "*" + } + }, "packages/mcp": { "name": "@lemoncode/quickmock-mcp", "version": "0.0.1", "dependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@modelcontextprotocol/sdk": "^1.12.0", "puppeteer": "^24.0.0", "zod": "^4.0.0" @@ -11201,6 +11214,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/quickmock-mcp": "*" }, "devDependencies": { diff --git a/packages/bridge-protocol/package.json b/packages/bridge-protocol/package.json new file mode 100644 index 00000000..52ddea32 --- /dev/null +++ b/packages/bridge-protocol/package.json @@ -0,0 +1,15 @@ +{ + "name": "@lemoncode/quickmock-bridge-protocol", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@lemoncode/typescript-config": "*" + } +} diff --git a/packages/vscode-extension/src/core/protocol.ts b/packages/bridge-protocol/src/index.ts similarity index 65% rename from packages/vscode-extension/src/core/protocol.ts rename to packages/bridge-protocol/src/index.ts index 75cdaf9a..1e2d2f29 100644 --- a/packages/vscode-extension/src/core/protocol.ts +++ b/packages/bridge-protocol/src/index.ts @@ -1,3 +1,15 @@ +export interface ContentBbox { + x: number; + y: number; + width: number; + height: number; +} + +export interface LoadFilePayload { + data: unknown; + fileName: string; +} + export const HOST_MESSAGE_TYPE = { LOAD: 'qm:load', SAVED: 'qm:saved', @@ -11,11 +23,6 @@ export const APP_MESSAGE_TYPE = { WEBVIEW_READY: 'WEBVIEW_READY', } as const; -export interface LoadFilePayload { - data: unknown; - fileName: string; -} - export type HostMessage = | { type: typeof HOST_MESSAGE_TYPE.LOAD; @@ -26,12 +33,14 @@ export type HostMessage = export type AppMessage = | { type: typeof APP_MESSAGE_TYPE.READY } + | { type: typeof APP_MESSAGE_TYPE.WEBVIEW_READY } | { type: typeof APP_MESSAGE_TYPE.SAVE; payload: { content: string } } - | { type: typeof APP_MESSAGE_TYPE.RENDER_COMPLETE } - | { type: typeof APP_MESSAGE_TYPE.WEBVIEW_READY }; + | { + type: typeof APP_MESSAGE_TYPE.RENDER_COMPLETE; + payload?: ContentBbox; + }; -export interface BridgeMessage { - type: string; - payload?: T; - requestId?: string; -} +export type PayloadOf< + U extends { type: string }, + T extends U['type'], +> = Extract extends { payload: infer P } ? P : undefined; diff --git a/packages/bridge-protocol/tsconfig.json b/packages/bridge-protocol/tsconfig.json new file mode 100644 index 00000000..82855979 --- /dev/null +++ b/packages/bridge-protocol/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@lemoncode/typescript-config/base", + "include": ["src"], + "compilerOptions": { + "target": "ES2024", + "lib": ["ES2024"], + "noEmit": true, + "rootDir": "src" + } +} diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 58be10a1..b8c96e88 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -24,6 +24,7 @@ "inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.mjs" }, "dependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@modelcontextprotocol/sdk": "^1.12.0", "puppeteer": "^24.0.0", "zod": "^4.0.0" diff --git a/packages/mcp/src/renderer/bridge.server.ts b/packages/mcp/src/renderer/bridge.server.ts index 848d882f..84c21fc3 100644 --- a/packages/mcp/src/renderer/bridge.server.ts +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -1,5 +1,6 @@ import { createServer, type Server } from 'node:http' import type { AddressInfo } from 'node:net' +import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol' import { QUICKMOCK_URL } from './renderer.consts' export interface BridgeServer { @@ -25,6 +26,9 @@ export function startBridgeServer(): Promise { } function buildBridgeHtml(): string { + const READY = JSON.stringify(APP_MESSAGE_TYPE.READY) + const RENDER_COMPLETE = JSON.stringify(APP_MESSAGE_TYPE.RENDER_COMPLETE) + return /* html */ ` @@ -51,8 +55,8 @@ function buildBridgeHtml(): string { // Messages coming UP from the QuickMock iframe if (event.source === iframe.contentWindow) { - if (type === 'qm:ready') window.__qmReady = true - if (type === 'qm:render-complete') { + if (type === ${READY}) window.__qmReady = true + if (type === ${RENDER_COMPLETE}) { window.__renderComplete = true window.__renderBbox = event.data.payload ?? null } diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index ac03fffe..138d37ad 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -15,6 +15,7 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/quickmock-mcp": "*" }, "devDependencies": { diff --git a/packages/vscode-extension/src/editor/handlers.ts b/packages/vscode-extension/src/editor/handlers.ts index 18e69450..e6795936 100644 --- a/packages/vscode-extension/src/editor/handlers.ts +++ b/packages/vscode-extension/src/editor/handlers.ts @@ -4,7 +4,7 @@ import { type AppMessage, HOST_MESSAGE_TYPE, type HostMessage, -} from '#core/protocol'; +} from '@lemoncode/quickmock-bridge-protocol'; import { type QuickMockDocument, writeFile } from './document'; type PostMessageFn = (msg: HostMessage) => void; diff --git a/packages/vscode-extension/src/editor/provider.ts b/packages/vscode-extension/src/editor/provider.ts index cd012339..9c667005 100644 --- a/packages/vscode-extension/src/editor/provider.ts +++ b/packages/vscode-extension/src/editor/provider.ts @@ -6,7 +6,7 @@ import { type AppMessage, HOST_MESSAGE_TYPE, type HostMessage, -} from '#core/protocol'; +} from '@lemoncode/quickmock-bridge-protocol'; import { openDocument, type QuickMockDocument, diff --git a/packages/vscode-extension/src/webview/bridge.ts b/packages/vscode-extension/src/webview/bridge.ts index 7ef52506..0b19e3a6 100644 --- a/packages/vscode-extension/src/webview/bridge.ts +++ b/packages/vscode-extension/src/webview/bridge.ts @@ -2,7 +2,7 @@ import { type AppMessage, HOST_MESSAGE_TYPE, type HostMessage, -} from '#core/protocol'; +} from '@lemoncode/quickmock-bridge-protocol'; declare function acquireVsCodeApi(): { postMessage(msg: AppMessage): void }; diff --git a/packages/vscode-extension/tsdown.config.ts b/packages/vscode-extension/tsdown.config.ts index 7e50bd88..4a2f2f28 100644 --- a/packages/vscode-extension/tsdown.config.ts +++ b/packages/vscode-extension/tsdown.config.ts @@ -16,6 +16,7 @@ export default defineConfig([ sourcemap: true, clean: false, dts: false, + deps: { alwaysBundle: /.*/ }, outputOptions: { entryFileNames: '[name].js', }, From 274eed8c5a3a82086edeac3c6b9021b61cc1e0c6 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 11:57:22 +0200 Subject: [PATCH 07/23] feat(vscode-extension): update VSCode URL and adjust type dependencies --- package-lock.json | 20 +++++++++---------- packages/mcp/package.json | 2 +- packages/mcp/src/renderer/renderer.consts.ts | 2 +- packages/vscode-extension/package.json | 4 ++-- .../vscode-extension/src/core/constants.ts | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f09f42c..f18994d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2742,13 +2742,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/vscode": { - "version": "1.116.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.116.0.tgz", - "integrity": "sha512-sYHp4MO6BqJ2PD7Hjt0hlIS3tMaYsVPJrd0RUjDJ8HtOYnyJIEej0bLSccM8rE77WrC+Xox/kdBwEFDO8MsxNA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/wicg-file-system-access": { "version": "2023.10.7", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.7.tgz", @@ -11180,12 +11173,12 @@ "name": "@lemoncode/quickmock-mcp", "version": "0.0.1", "dependencies": { - "@lemoncode/quickmock-bridge-protocol": "*", "@modelcontextprotocol/sdk": "^1.12.0", "puppeteer": "^24.0.0", "zod": "^4.0.0" }, "devDependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", @@ -11214,20 +11207,27 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/quickmock-mcp": "*" }, "devDependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", - "@types/vscode": "1.116.0", + "@types/vscode": "1.115.0", "@vscode/vsce": "3.9.0" }, "engines": { "vscode": "^1.115.0" } }, + "packages/vscode-extension/node_modules/@types/vscode": { + "version": "1.115.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.115.0.tgz", + "integrity": "sha512-/M8cdznOlqtMqduHKKlIF00v4eum4ZWKgn8YoPRKcN6PDdvoWeeqDaQSnw63ipDbq1Uzz78Wndk/d0uSPwORfA==", + "dev": true, + "license": "MIT" + }, "tooling/dev-cli": { "dependencies": { "@clack/prompts": "1.2.0" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index b8c96e88..599ad7eb 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -24,12 +24,12 @@ "inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.mjs" }, "dependencies": { - "@lemoncode/quickmock-bridge-protocol": "*", "@modelcontextprotocol/sdk": "^1.12.0", "puppeteer": "^24.0.0", "zod": "^4.0.0" }, "devDependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/typescript-config": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/vitest-config": "*", diff --git a/packages/mcp/src/renderer/renderer.consts.ts b/packages/mcp/src/renderer/renderer.consts.ts index c73acf1d..0c2661a8 100644 --- a/packages/mcp/src/renderer/renderer.consts.ts +++ b/packages/mcp/src/renderer/renderer.consts.ts @@ -1,5 +1,5 @@ export const QUICKMOCK_URL = - process.env.QM_APP_URL ?? 'http://localhost:5173/editor.html?env=vscode&headless=1' + process.env.QM_APP_URL ?? 'https://quickmock.net/editor.html?env=vscode&headless=1' export const READY_TIMEOUT_MS = 15_000 export const RENDER_TIMEOUT_MS = 20_000 diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 138d37ad..4a91a1d4 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -15,14 +15,14 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { - "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/quickmock-mcp": "*" }, "devDependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", - "@types/vscode": "1.116.0", + "@types/vscode": "1.115.0", "@vscode/vsce": "3.9.0" }, "publisher": "Lemoncoders", diff --git a/packages/vscode-extension/src/core/constants.ts b/packages/vscode-extension/src/core/constants.ts index 05ab2132..59bf42a4 100644 --- a/packages/vscode-extension/src/core/constants.ts +++ b/packages/vscode-extension/src/core/constants.ts @@ -1,2 +1,2 @@ export const QUICKMOCK_APP_URL = - process.env.QM_APP_URL ?? 'http://localhost:5173/editor.html?env=vscode'; + process.env.QM_APP_URL ?? 'https://quickmock.net/editor.html?env=vscode'; From 927a78209d1169a18d2b674cfccde572dfc17354 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 17 Apr 2026 12:29:55 +0200 Subject: [PATCH 08/23] feat(vscode-extension): add environment variable for QM_APP_URL in launch configuration --- .vscode/launch.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7f4f8135..c3608c15 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,9 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-extension" ], + "env": { + "QM_APP_URL": "http://localhost:5173/editor.html?env=vscode" + }, "outFiles": ["${workspaceFolder}/packages/vscode-extension/dist/**/*.mjs"] } ] From dc43f2bff98f31c2fe63044e37c84756cebff307 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 10:26:41 +0200 Subject: [PATCH 09/23] feat: bundle Chromium, share app URL via ~/.quickmock, and reorganize modules --- .env.example | 4 - .vscode/launch.json | 3 - .vscode/settings.json | 3 +- package-lock.json | 158 +--------------- packages/mcp/package.json | 3 +- packages/mcp/src/core/mcp.logger.ts | 9 + packages/mcp/src/index.ts | 3 +- packages/mcp/src/renderer/app-url.consts.ts | 28 +++ packages/mcp/src/renderer/bridge.server.ts | 4 +- .../mcp/src/renderer/chromium.resolver.ts | 64 +++++++ .../mcp/src/renderer/headless.renderer.ts | 12 +- packages/mcp/src/renderer/page.session.ts | 5 +- packages/mcp/src/renderer/renderer.consts.ts | 13 +- .../capture-wireframe.handler.ts | 11 +- .../get-wireframe-assets.handler.ts | 4 +- .../get-wireframe-json.handler.ts | 4 +- .../get-wireframe-pages.handler.ts | 4 +- .../list-wireframes.handler.ts | 2 +- packages/mcp/tsdown.config.ts | 1 - packages/vscode-extension/.vscodeignore | 3 + packages/vscode-extension/package.json | 15 +- .../vscode-extension/scripts/copy-mcp.mjs | 9 +- packages/vscode-extension/src/core/config.ts | 46 +++++ .../vscode-extension/src/core/constants.ts | 2 - packages/vscode-extension/src/core/logger.ts | 9 + packages/vscode-extension/src/core/paths.ts | 15 ++ .../vscode-extension/src/editor/provider.ts | 20 +- packages/vscode-extension/src/index.ts | 20 +- .../src/mcp/mcp-client-targets.ts | 62 +++++++ .../vscode-extension/src/mcp/mcp-command.ts | 7 +- .../src/mcp/mcp-config-file.ts | 26 +++ .../src/mcp/mcp-registration.ts | 173 ++++++++---------- .../src/mcp/server-definition-provider.ts | 116 +++++++----- packages/vscode-extension/tsdown.config.ts | 2 + 34 files changed, 504 insertions(+), 356 deletions(-) delete mode 100644 .env.example create mode 100644 packages/mcp/src/core/mcp.logger.ts create mode 100644 packages/mcp/src/renderer/app-url.consts.ts create mode 100644 packages/mcp/src/renderer/chromium.resolver.ts create mode 100644 packages/vscode-extension/src/core/config.ts delete mode 100644 packages/vscode-extension/src/core/constants.ts create mode 100644 packages/vscode-extension/src/core/logger.ts create mode 100644 packages/vscode-extension/src/core/paths.ts create mode 100644 packages/vscode-extension/src/mcp/mcp-client-targets.ts create mode 100644 packages/vscode-extension/src/mcp/mcp-config-file.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index a2f0fa59..00000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# URL of the QuickMock web app loaded in the VS Code webview and by the MCP renderer. -# Default (local dev): http://localhost:5173/editor.html?env=vscode -# Production example: https://quickmock.net/editor.html?env=vscode -QM_APP_URL=http://localhost:5173/editor.html?env=vscode diff --git a/.vscode/launch.json b/.vscode/launch.json index c3608c15..7f4f8135 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,9 +8,6 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-extension" ], - "env": { - "QM_APP_URL": "http://localhost:5173/editor.html?env=vscode" - }, "outFiles": ["${workspaceFolder}/packages/vscode-extension/dist/**/*.mjs"] } ] diff --git a/.vscode/settings.json b/.vscode/settings.json index 08f7bec1..8fc4865d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "editor.codeActionsOnSave": { "source.organizeImports": "always", "source.removeUnusedImports": "always" - } + }, + "quickmock.appUrl": "http://localhost:5173/editor.html" } diff --git a/package-lock.json b/package-lock.json index f18994d2..4ab198fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -279,6 +279,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -293,6 +294,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -377,6 +379,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3549,6 +3552,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -4065,15 +4069,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -4435,50 +4430,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4949,15 +4900,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -4971,15 +4913,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -6099,31 +6032,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/import-without-cache": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.3.3.tgz", @@ -6192,12 +6100,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -6438,6 +6340,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6459,12 +6362,6 @@ "node": ">=6" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -6858,12 +6755,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -7804,18 +7695,6 @@ "quansync": "^0.2.7" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-json": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", @@ -7999,6 +7878,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8254,27 +8134,6 @@ "node": ">=6" } }, - "node_modules/puppeteer": { - "version": "24.41.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.41.0.tgz", - "integrity": "sha512-W6Fk0J3TPjjtwjXOyR/qf+YaL0H/Uq8HIgHcXG4mNM/IgbKMCH/HPyK0Fi2qbTU/QpSl9bCte2yBpGHKejTpIw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.13.0", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1595872", - "puppeteer-core": "24.41.0", - "typed-query-selector": "^2.12.1" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/puppeteer-core": { "version": "24.41.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.41.0.tgz", @@ -10122,7 +9981,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -11174,7 +11033,8 @@ "version": "0.0.1", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", - "puppeteer": "^24.0.0", + "@puppeteer/browsers": "^2.13.0", + "puppeteer-core": "^24.0.0", "zod": "^4.0.0" }, "devDependencies": { diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 599ad7eb..87272b9b 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -25,7 +25,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", - "puppeteer": "^24.0.0", + "@puppeteer/browsers": "^2.13.0", + "puppeteer-core": "^24.0.0", "zod": "^4.0.0" }, "devDependencies": { diff --git a/packages/mcp/src/core/mcp.logger.ts b/packages/mcp/src/core/mcp.logger.ts new file mode 100644 index 00000000..53e98387 --- /dev/null +++ b/packages/mcp/src/core/mcp.logger.ts @@ -0,0 +1,9 @@ +const PREFIX = '[quickmock-mcp]' + +export const logInfo = (message: string, ...rest: unknown[]): void => { + console.error(`${PREFIX} ${message}`, ...rest) +} + +export const logError = (message: string, ...rest: unknown[]): void => { + console.error(`${PREFIX} ${message}`, ...rest) +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index c8687f82..0852ba87 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio' import { createWireframeFileService } from './commons/wireframe-file.service' import { createRegistryClient } from './core' +import { logError } from './core/mcp.logger' import { captureWireframe } from './tools/capture-wireframe' import { getWireframeAssets } from './tools/get-wireframe-assets' import { getWireframeJson } from './tools/get-wireframe-json' @@ -47,6 +48,6 @@ async function main() { } main().catch((err) => { - console.error('[quickmock-mcp] fatal error:', err) + logError('fatal error:', err) process.exit(1) }) diff --git a/packages/mcp/src/renderer/app-url.consts.ts b/packages/mcp/src/renderer/app-url.consts.ts new file mode 100644 index 00000000..bcd6a8de --- /dev/null +++ b/packages/mcp/src/renderer/app-url.consts.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { join } from 'node:path' + +const DEFAULT_APP_URL = + 'https://quickmock.net/editor.html?env=vscode&headless=1' + +const APP_URL_FILE = join(homedir(), '.quickmock', 'app-url') + +const readAppUrl = (): string => { + try { + const value = readFileSync(APP_URL_FILE, 'utf-8').trim() + if (value) return value + } catch { + // fallback to default + } + return DEFAULT_APP_URL +} + +export const QUICKMOCK_URL = readAppUrl() + +export const QM_APP_ORIGIN = (() => { + try { + return new URL(QUICKMOCK_URL).origin + } catch { + return QUICKMOCK_URL + } +})() diff --git a/packages/mcp/src/renderer/bridge.server.ts b/packages/mcp/src/renderer/bridge.server.ts index 84c21fc3..13e62c29 100644 --- a/packages/mcp/src/renderer/bridge.server.ts +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -1,7 +1,7 @@ +import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol' import { createServer, type Server } from 'node:http' import type { AddressInfo } from 'node:net' -import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol' -import { QUICKMOCK_URL } from './renderer.consts' +import { QUICKMOCK_URL } from './app-url.consts' export interface BridgeServer { server: Server diff --git a/packages/mcp/src/renderer/chromium.resolver.ts b/packages/mcp/src/renderer/chromium.resolver.ts new file mode 100644 index 00000000..476c4f48 --- /dev/null +++ b/packages/mcp/src/renderer/chromium.resolver.ts @@ -0,0 +1,64 @@ +import { + Browser, + computeExecutablePath, + detectBrowserPlatform, + install, + resolveBuildId, +} from '@puppeteer/browsers' +import { existsSync } from 'node:fs' +import { homedir, tmpdir } from 'node:os' +import { join } from 'node:path' +import { logError, logInfo } from '../core/mcp.logger' + +const CHROMIUM_CHANNEL = 'stable' +const PROGRESS_LOG_STEP_PERCENT = 10 +const FALLBACK_CACHE_DIR = join(tmpdir(), 'quickmock-browsers') +const USER_CACHE_DIR_SEGMENTS = ['.quickmock', 'browsers'] as const + +let cachedPath: Promise | undefined + +export function resolveChromiumExecutable(): Promise { + cachedPath ??= doResolve() + return cachedPath +} + +async function doResolve(): Promise { + const cacheDir = getCacheDir() + const platform = detectBrowserPlatform() + if (!platform) throw new Error('Unsupported platform for Chromium download.') + + const buildId = await resolveBuildId(Browser.CHROME, platform, CHROMIUM_CHANNEL) + const executablePath = computeExecutablePath({ browser: Browser.CHROME, buildId, cacheDir }) + + if (existsSync(executablePath)) return executablePath + + logInfo( + `Chromium not found in "${cacheDir}". Downloading ${Browser.CHROME}@${buildId} for ${platform}…`, + ) + + let lastLoggedPercent = -1 + await install({ + browser: Browser.CHROME, + buildId, + cacheDir, + downloadProgressCallback: (downloaded, total) => { + if (!total) return + const percent = Math.floor((downloaded / total) * 100) + if (percent === lastLoggedPercent || percent % PROGRESS_LOG_STEP_PERCENT !== 0) return + lastLoggedPercent = percent + logInfo(`Downloading Chromium: ${percent}%`) + }, + }).catch((err) => { + logError('Chromium download failed:', err) + throw err + }) + + logInfo(`Chromium ready at ${executablePath}`) + return executablePath +} + +function getCacheDir(): string { + const home = homedir() + if (home) return join(home, ...USER_CACHE_DIR_SEGMENTS) + return FALLBACK_CACHE_DIR +} diff --git a/packages/mcp/src/renderer/headless.renderer.ts b/packages/mcp/src/renderer/headless.renderer.ts index bf4a0bd0..4282418a 100644 --- a/packages/mcp/src/renderer/headless.renderer.ts +++ b/packages/mcp/src/renderer/headless.renderer.ts @@ -1,6 +1,7 @@ -import type { Browser } from 'puppeteer' -import puppeteer from 'puppeteer' +import type { Browser } from 'puppeteer-core' +import puppeteer from 'puppeteer-core' import { startBridgeServer } from './bridge.server' +import { resolveChromiumExecutable } from './chromium.resolver' import { screenshotIframe, sendFileToApp, @@ -39,7 +40,12 @@ export async function renderWireframe(content: string, fileName: string): Promis } async function withBrowser(fn: (browser: Browser) => Promise): Promise { - const browser = await puppeteer.launch({ headless: true, args: BROWSER_LAUNCH_ARGS }) + const executablePath = await resolveChromiumExecutable() + const browser = await puppeteer.launch({ + headless: true, + executablePath, + args: BROWSER_LAUNCH_ARGS, + }) try { return await fn(browser) } finally { diff --git a/packages/mcp/src/renderer/page.session.ts b/packages/mcp/src/renderer/page.session.ts index c7530d2c..4b8c5d37 100644 --- a/packages/mcp/src/renderer/page.session.ts +++ b/packages/mcp/src/renderer/page.session.ts @@ -1,8 +1,7 @@ -import type { Page } from 'puppeteer' +import type { Page } from 'puppeteer-core' +import { QM_APP_ORIGIN, QUICKMOCK_URL } from './app-url.consts' import { LOCAL_INSTANCE_HINT, - QM_APP_ORIGIN, - QUICKMOCK_URL, READY_TIMEOUT_MS, RENDER_TIMEOUT_MS, } from './renderer.consts' diff --git a/packages/mcp/src/renderer/renderer.consts.ts b/packages/mcp/src/renderer/renderer.consts.ts index 0c2661a8..fc690df8 100644 --- a/packages/mcp/src/renderer/renderer.consts.ts +++ b/packages/mcp/src/renderer/renderer.consts.ts @@ -1,16 +1,5 @@ -export const QUICKMOCK_URL = - process.env.QM_APP_URL ?? 'https://quickmock.net/editor.html?env=vscode&headless=1' - export const READY_TIMEOUT_MS = 15_000 export const RENDER_TIMEOUT_MS = 20_000 -export const QM_APP_ORIGIN = (() => { - try { - return new URL(QUICKMOCK_URL).origin - } catch { - return QUICKMOCK_URL - } -})() - export const LOCAL_INSTANCE_HINT = - 'Set QM_APP_URL=http://localhost:5173/editor.html?env=vscode&headless=1 to use a local instance of QuickMock.' + 'Set quickmock.appUrl in VS Code settings (or edit ~/.quickmock/app-url) to point at a local QuickMock instance, e.g. http://localhost:5173/editor.html.' diff --git a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts index ac9cf713..358879d5 100644 --- a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts @@ -1,7 +1,8 @@ -import { basename } from 'node:path' -import { renderWireframe } from '../../renderer/headless.renderer' -import { toolError, toolImage } from '../../commons/tool-response.helpers' -import type { WireframeFileService } from '../../commons/wireframe-file.service' +import { toolError, toolImage } from '#/commons/tool-response.helpers'; +import type { WireframeFileService } from '#/commons/wireframe-file.service'; +import { renderWireframe } from '#/renderer/headless.renderer'; +import { QUICKMOCK_URL } from '#renderer/app-url.consts.js'; +import { basename } from 'node:path'; export async function captureWireframeHandler( args: { path: string; pageIndex?: number }, @@ -32,6 +33,6 @@ export async function captureWireframeHandler( const png = await renderWireframe(targetContent, fileName) return toolImage(png.toString('base64'), 'image/png') } catch (err) { - return toolError(`Error capturing wireframe at "${path}": ${String(err)}`) + return toolError(`Error capturing wireframe at "${path} with ${QUICKMOCK_URL}": ${String(err)}`) } } diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts index 9677542b..d3c35a34 100644 --- a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts @@ -1,8 +1,8 @@ +import { toolError, toolText } from '#/commons/tool-response.helpers' +import type { WireframeFileService } from '#/commons/wireframe-file.service' import { createHash } from 'node:crypto' import { mkdir, writeFile } from 'node:fs/promises' import { basename, extname, join, resolve } from 'node:path' -import { toolError, toolText } from '../../commons/tool-response.helpers' -import type { WireframeFileService } from '../../commons/wireframe-file.service' interface ParsedDataUrl { mimeType: string diff --git a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts index 915827bd..99d6386e 100644 --- a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts @@ -1,5 +1,5 @@ -import { toolError, toolText } from '../../commons/tool-response.helpers' -import type { WireframeFileService } from '../../commons/wireframe-file.service' +import { toolError, toolText } from '#/commons/tool-response.helpers' +import type { WireframeFileService } from '#/commons/wireframe-file.service' export async function getWireframeJsonHandler( args: { path: string }, diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts index 8f9da688..cb8354a1 100644 --- a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts @@ -1,5 +1,5 @@ -import { toolError, toolText } from '../../commons/tool-response.helpers' -import type { WireframeFileService } from '../../commons/wireframe-file.service' +import { toolError, toolText } from '#/commons/tool-response.helpers' +import type { WireframeFileService } from '#/commons/wireframe-file.service' import type { WireframePage } from './get-wireframe-pages.models' export async function getWireframePagesHandler( diff --git a/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts b/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts index 26ddaf00..97453ba9 100644 --- a/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts +++ b/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts @@ -1,6 +1,6 @@ +import { toolError, toolText } from '#/commons/tool-response.helpers' import { readdir } from 'node:fs/promises' import { join, relative } from 'node:path' -import { toolError, toolText } from '../../commons/tool-response.helpers' const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'out', '.vscode']) diff --git a/packages/mcp/tsdown.config.ts b/packages/mcp/tsdown.config.ts index bce13549..9928ca2e 100644 --- a/packages/mcp/tsdown.config.ts +++ b/packages/mcp/tsdown.config.ts @@ -4,7 +4,6 @@ export default { ...baseTsdownConfig, entry: ['src/index.ts'], deps: { - neverBundle: ['puppeteer', 'puppeteer-core'], alwaysBundle: /.*/, }, }; diff --git a/packages/vscode-extension/.vscodeignore b/packages/vscode-extension/.vscodeignore index 7dada2d1..a4774897 100644 --- a/packages/vscode-extension/.vscodeignore +++ b/packages/vscode-extension/.vscodeignore @@ -1,8 +1,11 @@ src/** node_modules/** +scripts/** *.ts tsconfig.json tsdown.config.ts vitest.config.ts .turbo/** +.env* coverage/** +*.vsix diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 4a91a1d4..cc7ba954 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "quickmock", "version": "0.0.1", "type": "module", - "main": "./dist/index.mjs", + "main": "./dist/index.cjs", "imports": { "#*": "./src/*" }, @@ -91,6 +91,17 @@ "id": "quickmock", "label": "QuickMock Wireframe Tools" } - ] + ], + "configuration": { + "title": "QuickMock", + "properties": { + "quickmock.appUrl": { + "type": "string", + "default": "https://quickmock.net/editor.html", + "format": "uri", + "markdownDescription": "Base URL of the QuickMock web app used by the custom editor and the MCP renderer. The extension automatically appends `?env=vscode` for the webview and `&headless=1` for the headless screenshot MCP. Changing this refreshes open editors and respawns the MCP server." + } + } + } } } diff --git a/packages/vscode-extension/scripts/copy-mcp.mjs b/packages/vscode-extension/scripts/copy-mcp.mjs index d7d6d104..8c4266f5 100644 --- a/packages/vscode-extension/scripts/copy-mcp.mjs +++ b/packages/vscode-extension/scripts/copy-mcp.mjs @@ -1,4 +1,4 @@ -import { copyFile, mkdir } from 'node:fs/promises'; +import { cp, mkdir, rm } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -7,10 +7,11 @@ const require = createRequire(import.meta.url); const here = dirname(fileURLToPath(import.meta.url)); const mcpDir = dirname(require.resolve('@lemoncode/quickmock-mcp/package.json')); -const source = join(mcpDir, 'dist', 'index.mjs'); +const source = join(mcpDir, 'dist'); const distDir = join(here, '..', 'dist'); -const target = join(distDir, 'mcp-server.mjs'); +const target = join(distDir, 'mcp'); await mkdir(distDir, { recursive: true }); -await copyFile(source, target); +await rm(target, { recursive: true, force: true }); +await cp(source, target, { recursive: true }); diff --git a/packages/vscode-extension/src/core/config.ts b/packages/vscode-extension/src/core/config.ts new file mode 100644 index 00000000..c68c4c9b --- /dev/null +++ b/packages/vscode-extension/src/core/config.ts @@ -0,0 +1,46 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; +import * as vscode from 'vscode'; +import { logError } from './logger'; +import { APP_URL_FILE } from './paths'; + +const SECTION = 'quickmock'; +const APP_URL_KEY = 'appUrl'; +const FULL_KEY = `${SECTION}.${APP_URL_KEY}`; +const DEFAULT_APP_URL = 'https://quickmock.net/editor.html'; + +const EDITOR_PARAMS = { env: 'vscode' } as const; +const HEADLESS_PARAMS = { env: 'vscode', headless: '1' } as const; + +const readRawAppUrl = (): string => { + const value = vscode.workspace + .getConfiguration(SECTION) + .get(APP_URL_KEY); + return value?.trim() || DEFAULT_APP_URL; +}; + +const withParams = (url: string, params: Record): string => { + const parsed = new URL(url); + for (const [k, v] of Object.entries(params)) parsed.searchParams.set(k, v); + return parsed.toString(); +}; + +export const getEditorAppUrl = (): string => + withParams(readRawAppUrl(), EDITOR_PARAMS); + +export const getHeadlessAppUrl = (): string => + withParams(readRawAppUrl(), HEADLESS_PARAMS); + +export const syncAppUrlFile = (): void => { + try { + mkdirSync(dirname(APP_URL_FILE), { recursive: true }); + writeFileSync(APP_URL_FILE, getHeadlessAppUrl(), 'utf-8'); + } catch (err) { + logError('Failed to write app URL file:', err); + } +}; + +export const onAppUrlChange = (listener: () => void): vscode.Disposable => + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(FULL_KEY)) listener(); + }); diff --git a/packages/vscode-extension/src/core/constants.ts b/packages/vscode-extension/src/core/constants.ts deleted file mode 100644 index 59bf42a4..00000000 --- a/packages/vscode-extension/src/core/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const QUICKMOCK_APP_URL = - process.env.QM_APP_URL ?? 'https://quickmock.net/editor.html?env=vscode'; diff --git a/packages/vscode-extension/src/core/logger.ts b/packages/vscode-extension/src/core/logger.ts new file mode 100644 index 00000000..a1098647 --- /dev/null +++ b/packages/vscode-extension/src/core/logger.ts @@ -0,0 +1,9 @@ +const PREFIX = '[QuickMock]'; + +export const logInfo = (message: string, ...rest: unknown[]): void => { + console.info(`${PREFIX} ${message}`, ...rest); +}; + +export const logError = (message: string, ...rest: unknown[]): void => { + console.error(`${PREFIX} ${message}`, ...rest); +}; diff --git a/packages/vscode-extension/src/core/paths.ts b/packages/vscode-extension/src/core/paths.ts new file mode 100644 index 00000000..39a0879c --- /dev/null +++ b/packages/vscode-extension/src/core/paths.ts @@ -0,0 +1,15 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import * as vscode from 'vscode'; + +export const MCP_SERVER_ID = 'quickmock'; + +export const QUICKMOCK_HOME = join(homedir(), '.quickmock'); +export const APP_URL_FILE = join(QUICKMOCK_HOME, 'app-url'); + +const MCP_DIST_SEGMENTS = ['dist', 'mcp', 'index.mjs'] as const; + +export const getMcpEntrypointPath = ( + context: vscode.ExtensionContext +): string => + vscode.Uri.joinPath(context.extensionUri, ...MCP_DIST_SEGMENTS).fsPath; diff --git a/packages/vscode-extension/src/editor/provider.ts b/packages/vscode-extension/src/editor/provider.ts index 9c667005..dc5e1e2c 100644 --- a/packages/vscode-extension/src/editor/provider.ts +++ b/packages/vscode-extension/src/editor/provider.ts @@ -1,6 +1,6 @@ import { basename } from 'node:path'; import * as vscode from 'vscode'; -import { QUICKMOCK_APP_URL } from '#core/constants'; +import { getEditorAppUrl, onAppUrlChange } from '#core/config'; import { documentRegistry } from '#core/document-registry'; import { type AppMessage, @@ -20,14 +20,17 @@ export class QuickMockEditorProvider implements vscode.CustomEditorProvider { static register(context: vscode.ExtensionContext): vscode.Disposable { - return vscode.window.registerCustomEditorProvider( + const provider = new QuickMockEditorProvider(context.extensionUri); + const editorRegistration = vscode.window.registerCustomEditorProvider( 'quickmock.editor', - new QuickMockEditorProvider(context.extensionUri), + provider, { supportsMultipleEditorsPerDocument: false, webviewOptions: { retainContextWhenHidden: true }, } ); + const configListener = onAppUrlChange(() => provider.refreshAllPanels()); + return vscode.Disposable.from(editorRegistration, configListener); } constructor(private readonly extensionUri: vscode.Uri) {} @@ -113,7 +116,7 @@ export class QuickMockEditorProvider panel.webview.html = getHtml( panel.webview, this.extensionUri, - QUICKMOCK_APP_URL + getEditorAppUrl() ); panel.webview.onDidReceiveMessage(async (msg: AppMessage) => { @@ -129,4 +132,13 @@ export class QuickMockEditorProvider panel.webview.postMessage(msg); } } + + refreshAllPanels(): void { + const url = getEditorAppUrl(); + for (const panels of this.panels.values()) { + for (const panel of panels) { + panel.webview.html = getHtml(panel.webview, this.extensionUri, url); + } + } + } } diff --git a/packages/vscode-extension/src/index.ts b/packages/vscode-extension/src/index.ts index 5fcde0b5..724e52e0 100644 --- a/packages/vscode-extension/src/index.ts +++ b/packages/vscode-extension/src/index.ts @@ -1,26 +1,32 @@ import * as vscode from 'vscode'; +import { onAppUrlChange, syncAppUrlFile } from '#core/config'; +import { logError } from '#core/logger'; import { QuickMockEditorProvider } from '#editor/provider'; import { registerConnectMcpCommand } from '#mcp/mcp-command'; -import { registerMcpServer } from '#mcp/mcp-registration'; +import { + cleanupStaleMcpRegistration, + registerMcpServer, +} from '#mcp/mcp-registration'; import { RegistryServer } from '#mcp/registry-server'; import { registerQuickMockMcpServerProvider } from '#mcp/server-definition-provider'; export const activate = (context: vscode.ExtensionContext) => { + syncAppUrlFile(); + context.subscriptions.push(onAppUrlChange(syncAppUrlFile)); + context.subscriptions.push(QuickMockEditorProvider.register(context)); const registryServer = new RegistryServer(); registryServer .start(context) - .catch((err) => - console.error('[QuickMock] Failed to start MCP registry server:', err) - ); + .catch((err) => logError('Failed to start MCP registry server:', err)); context.subscriptions.push(registerQuickMockMcpServerProvider(context)); context.subscriptions.push(registerConnectMcpCommand(context)); - registerMcpServer(context).catch((err) => - console.error('[QuickMock] Failed to register MCP server:', err) - ); + cleanupStaleMcpRegistration() + .then(() => registerMcpServer(context)) + .catch((err) => logError('Failed to register MCP server:', err)); context.subscriptions.push( vscode.commands.registerCommand('quickmock.newWireframe', () => { diff --git a/packages/vscode-extension/src/mcp/mcp-client-targets.ts b/packages/vscode-extension/src/mcp/mcp-client-targets.ts new file mode 100644 index 00000000..00d08902 --- /dev/null +++ b/packages/vscode-extension/src/mcp/mcp-client-targets.ts @@ -0,0 +1,62 @@ +import { homedir, platform } from 'node:os'; +import { join } from 'node:path'; + +export interface McpClientTarget { + label: string; + path: string; +} + +const CLAUDE_CODE: McpClientTarget = { + label: 'Claude Code', + path: join(homedir(), '.claude.json'), +}; + +const CURSOR: McpClientTarget = { + label: 'Cursor', + path: join(homedir(), '.cursor', 'mcp.json'), +}; + +const WINDSURF: McpClientTarget = { + label: 'Windsurf', + path: join(homedir(), '.codeium', 'windsurf', 'mcp_config.json'), +}; + +const CLAUDE_DESKTOP_FILE = 'claude_desktop_config.json'; + +const getClaudeDesktopTarget = (): McpClientTarget => { + const home = homedir(); + const os = platform(); + + if (os === 'darwin') { + return { + label: 'Claude Desktop', + path: join( + home, + 'Library', + 'Application Support', + 'Claude', + CLAUDE_DESKTOP_FILE + ), + }; + } + + if (os === 'win32') { + const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming'); + return { + label: 'Claude Desktop', + path: join(appData, 'Claude', CLAUDE_DESKTOP_FILE), + }; + } + + return { + label: 'Claude Desktop', + path: join(home, '.config', 'Claude', CLAUDE_DESKTOP_FILE), + }; +}; + +export const getMcpClientTargets = (): McpClientTarget[] => [ + CLAUDE_CODE, + CURSOR, + WINDSURF, + getClaudeDesktopTarget(), +]; diff --git a/packages/vscode-extension/src/mcp/mcp-command.ts b/packages/vscode-extension/src/mcp/mcp-command.ts index 178e4424..bb4c021d 100644 --- a/packages/vscode-extension/src/mcp/mcp-command.ts +++ b/packages/vscode-extension/src/mcp/mcp-command.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { getMcpEntrypointPath } from '#core/paths'; import { registerMcpServer } from './mcp-registration'; const TOOLS = [ @@ -26,11 +27,7 @@ export const registerConnectMcpCommand = ( context: vscode.ExtensionContext ): vscode.Disposable => vscode.commands.registerCommand('quickmock.connectMcp', async () => { - const serverPath = vscode.Uri.joinPath( - context.extensionUri, - 'dist', - 'mcp-server.mjs' - ).fsPath; + const serverPath = getMcpEntrypointPath(context); const results = await vscode.window.withProgress( { diff --git a/packages/vscode-extension/src/mcp/mcp-config-file.ts b/packages/vscode-extension/src/mcp/mcp-config-file.ts new file mode 100644 index 00000000..1d7f0edf --- /dev/null +++ b/packages/vscode-extension/src/mcp/mcp-config-file.ts @@ -0,0 +1,26 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; + +export interface McpFileConfig { + mcpServers?: Record; + [key: string]: unknown; +} + +export const readMcpFileConfig = (filePath: string): McpFileConfig => { + try { + return JSON.parse(readFileSync(filePath, 'utf-8')) as McpFileConfig; + } catch { + return {}; + } +}; + +export const writeMcpFileConfig = ( + filePath: string, + data: McpFileConfig +): void => { + const dir = dirname(filePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); +}; diff --git a/packages/vscode-extension/src/mcp/mcp-registration.ts b/packages/vscode-extension/src/mcp/mcp-registration.ts index 9adcfb5c..c7d6de79 100644 --- a/packages/vscode-extension/src/mcp/mcp-registration.ts +++ b/packages/vscode-extension/src/mcp/mcp-registration.ts @@ -1,9 +1,17 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { homedir, platform } from 'node:os'; -import { dirname, join } from 'node:path'; +import { existsSync } from 'node:fs'; +import { dirname } from 'node:path'; import * as vscode from 'vscode'; - -const MCP_SERVER_KEY = 'quickmock'; +import { logError, logInfo } from '#core/logger'; +import { getMcpEntrypointPath, MCP_SERVER_ID } from '#core/paths'; +import { + getMcpClientTargets, + type McpClientTarget, +} from './mcp-client-targets'; +import { readMcpFileConfig, writeMcpFileConfig } from './mcp-config-file'; + +const VSCODE_CLIENT_LABEL = 'VS Code / GitHub Copilot'; +const MCP_CONFIG_SECTION = 'mcp'; +const MCP_SERVERS_KEY = 'servers'; export type RegistrationStatus = 'registered' | 'skipped' | 'error'; @@ -13,135 +21,104 @@ export interface RegistrationResult { detail?: string; } -interface McpFileConfig { - mcpServers?: Record; - [key: string]: unknown; -} - -interface FileTarget { - label: string; - path: string; +interface McpServerEntry { + type: 'stdio'; + command: string; + args: string[]; } -const getFileTargets = (): FileTarget[] => { - const home = homedir(); - const os = platform(); - - const targets: FileTarget[] = [ - { label: 'Claude Code', path: join(home, '.claude.json') }, - { label: 'Cursor', path: join(home, '.cursor', 'mcp.json') }, - { - label: 'Windsurf', - path: join(home, '.codeium', 'windsurf', 'mcp_config.json'), - }, - ]; - - if (os === 'darwin') { - targets.push({ - label: 'Claude Desktop', - path: join( - home, - 'Library', - 'Application Support', - 'Claude', - 'claude_desktop_config.json' - ), - }); - } else if (os === 'win32') { - const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming'); - targets.push({ - label: 'Claude Desktop', - path: join(appData, 'Claude', 'claude_desktop_config.json'), - }); - } else { - targets.push({ - label: 'Claude Desktop', - path: join(home, '.config', 'Claude', 'claude_desktop_config.json'), - }); - } - - return targets; -}; - -const readFileConfig = (filePath: string): McpFileConfig => { - try { - return JSON.parse(readFileSync(filePath, 'utf-8')) as McpFileConfig; - } catch { - return {}; - } -}; - -const writeFileConfig = (filePath: string, data: McpFileConfig): void => { - const dir = dirname(filePath); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); -}; +const buildMcpServerEntry = (serverPath: string): McpServerEntry => ({ + type: 'stdio', + command: 'node', + args: [serverPath], +}); const registerInVSCode = async ( - entry: unknown + entry: McpServerEntry ): Promise => { try { - const config = vscode.workspace.getConfiguration('mcp'); - const servers = config.get>('servers') ?? {}; - servers[MCP_SERVER_KEY] = entry; - await config.update('servers', servers, vscode.ConfigurationTarget.Global); - return { label: 'VS Code / GitHub Copilot', status: 'registered' }; + const config = vscode.workspace.getConfiguration(MCP_CONFIG_SECTION); + const servers = + config.get>(MCP_SERVERS_KEY) ?? {}; + servers[MCP_SERVER_ID] = entry; + await config.update( + MCP_SERVERS_KEY, + servers, + vscode.ConfigurationTarget.Global + ); + return { label: VSCODE_CLIENT_LABEL, status: 'registered' }; } catch (err) { return { - label: 'VS Code / GitHub Copilot', + label: VSCODE_CLIENT_LABEL, status: 'error', detail: String(err), }; } }; -const registerInFileTarget = ( - target: FileTarget, - entry: unknown +const registerInClientTarget = ( + target: McpClientTarget, + entry: McpServerEntry ): RegistrationResult => { - const dir = dirname(target.path); - if (!existsSync(dir) && !existsSync(target.path)) { + if (!existsSync(target.path) && !existsSync(dirname(target.path))) { return { label: target.label, status: 'skipped', detail: 'Not installed' }; } try { - const config = readFileConfig(target.path); - if (!config.mcpServers) { - config.mcpServers = {}; - } - config.mcpServers[MCP_SERVER_KEY] = entry; - writeFileConfig(target.path, config); + const config = readMcpFileConfig(target.path); + if (!config.mcpServers) config.mcpServers = {}; + config.mcpServers[MCP_SERVER_ID] = entry; + writeMcpFileConfig(target.path, config); return { label: target.label, status: 'registered' }; } catch (err) { return { label: target.label, status: 'error', detail: String(err) }; } }; +export const cleanupStaleMcpRegistration = async (): Promise => { + try { + const config = vscode.workspace.getConfiguration(MCP_CONFIG_SECTION); + const servers = { + ...(config.get>(MCP_SERVERS_KEY) ?? {}), + }; + const entry = servers[MCP_SERVER_ID] as { args?: unknown } | undefined; + if (!entry) return; + + const args = Array.isArray(entry.args) + ? entry.args.filter((a): a is string => typeof a === 'string') + : []; + const entrypoint = args[0]; + if (entrypoint && existsSync(entrypoint)) return; + + delete servers[MCP_SERVER_ID]; + await config.update( + MCP_SERVERS_KEY, + servers, + vscode.ConfigurationTarget.Global + ); + logInfo( + `Removed stale MCP registration (entrypoint "${entrypoint ?? ''}" missing)` + ); + } catch (err) { + logError('Failed to clean up stale MCP registration:', err); + } +}; + export const registerMcpServer = async ( context: vscode.ExtensionContext ): Promise => { - const serverPath = vscode.Uri.joinPath( - context.extensionUri, - 'dist', - 'mcp-server.mjs' - ).fsPath; - - const entry = { type: 'stdio', command: 'node', args: [serverPath], env: {} }; + const entry = buildMcpServerEntry(getMcpEntrypointPath(context)); const results: RegistrationResult[] = [ await registerInVSCode(entry), - ...getFileTargets().map((t) => registerInFileTarget(t, entry)), + ...getMcpClientTargets().map((t) => registerInClientTarget(t, entry)), ]; for (const r of results) { if (r.status === 'registered') { - console.info(`[QuickMock] MCP registered — ${r.label}`); + logInfo(`MCP registered — ${r.label}`); } else if (r.status === 'error') { - console.error( - `[QuickMock] MCP registration failed — ${r.label}: ${r.detail}` - ); + logError(`MCP registration failed — ${r.label}: ${r.detail}`); } } diff --git a/packages/vscode-extension/src/mcp/server-definition-provider.ts b/packages/vscode-extension/src/mcp/server-definition-provider.ts index 66ab8cf6..b8289240 100644 --- a/packages/vscode-extension/src/mcp/server-definition-provider.ts +++ b/packages/vscode-extension/src/mcp/server-definition-provider.ts @@ -1,57 +1,89 @@ +import { createHash } from 'node:crypto'; import * as vscode from 'vscode'; -import { QUICKMOCK_APP_URL } from '#core/constants'; +import { getHeadlessAppUrl, onAppUrlChange } from '#core/config'; +import { logInfo } from '#core/logger'; +import { getMcpEntrypointPath, MCP_SERVER_ID } from '#core/paths'; +import { version as EXTENSION_VERSION } from '../../package.json'; -const PROVIDER_ID = 'quickmock'; const SERVER_LABEL = 'QuickMock Wireframe Tools'; -const SERVER_VERSION = '0.0.1'; +const VERSION_HASH_ALGO = 'sha1'; +const VERSION_HASH_LENGTH = 8; -const QUICKMOCK_HEADLESS_URL = `${QUICKMOCK_APP_URL}&headless=1`; +const buildDefinition = ( + context: vscode.ExtensionContext, + workspaceFolder: vscode.WorkspaceFolder +): vscode.McpStdioServerDefinition => { + const versionSuffix = createHash(VERSION_HASH_ALGO) + .update(getHeadlessAppUrl()) + .digest('hex') + .slice(0, VERSION_HASH_LENGTH); + + return new vscode.McpStdioServerDefinition( + SERVER_LABEL, + 'node', + [getMcpEntrypointPath(context)], + { QM_WORKSPACE_ROOT: workspaceFolder.uri.fsPath }, + `${EXTENSION_VERSION}+${versionSuffix}` + ); +}; export const registerQuickMockMcpServerProvider = ( context: vscode.ExtensionContext ): vscode.Disposable => { const didChangeDefinitions = new vscode.EventEmitter(); - console.info('[QuickMock] Registering MCP server definition provider'); + logInfo('Registering MCP server definition provider'); + + let providerRegistration: vscode.Disposable | undefined; + + const register = () => { + providerRegistration?.dispose(); + providerRegistration = vscode.lm.registerMcpServerDefinitionProvider( + MCP_SERVER_ID, + { + onDidChangeMcpServerDefinitions: didChangeDefinitions.event, + provideMcpServerDefinitions: async (_token) => { + logInfo('Providing MCP server definitions'); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + logInfo('No workspace folder available for MCP server'); + return []; + } + return [buildDefinition(context, workspaceFolder)]; + }, + resolveMcpServerDefinition: async (server, _token) => { + logInfo('Resolving MCP server definition'); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if ( + !workspaceFolder || + !(server instanceof vscode.McpStdioServerDefinition) + ) { + return server; + } + const fresh = buildDefinition(context, workspaceFolder); + server.command = fresh.command; + server.args = fresh.args; + server.env = fresh.env; + server.version = fresh.version; + return server; + }, + } + ); + }; + + register(); - context.subscriptions.push( + const subscriptions: vscode.Disposable[] = [ didChangeDefinitions, vscode.workspace.onDidChangeWorkspaceFolders(() => didChangeDefinitions.fire() - ) - ); - - return vscode.lm.registerMcpServerDefinitionProvider(PROVIDER_ID, { - onDidChangeMcpServerDefinitions: didChangeDefinitions.event, - provideMcpServerDefinitions: async (_token) => { - console.info('[QuickMock] Providing MCP server definitions'); - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) { - console.info('[QuickMock] No workspace folder available for MCP server'); - return []; - } + ), + onAppUrlChange(() => { + logInfo('appUrl changed, re-registering MCP provider'); + register(); + didChangeDefinitions.fire(); + }), + { dispose: () => providerRegistration?.dispose() }, + ]; - return [ - new vscode.McpStdioServerDefinition( - SERVER_LABEL, - 'node', - [ - vscode.Uri.joinPath( - context.extensionUri, - 'dist', - 'mcp-server.mjs' - ).fsPath, - ], - { - QM_WORKSPACE_ROOT: workspaceFolder.uri.fsPath, - QM_APP_URL: QUICKMOCK_HEADLESS_URL, - }, - SERVER_VERSION - ), - ]; - }, - resolveMcpServerDefinition: async (server, _token) => { - console.info('[QuickMock] Resolving MCP server definition'); - return server; - }, - }); + return vscode.Disposable.from(...subscriptions); }; diff --git a/packages/vscode-extension/tsdown.config.ts b/packages/vscode-extension/tsdown.config.ts index 4a2f2f28..457f66bf 100644 --- a/packages/vscode-extension/tsdown.config.ts +++ b/packages/vscode-extension/tsdown.config.ts @@ -5,6 +5,8 @@ export default defineConfig([ { ...baseTsdownConfig, entry: ['src/index.ts'], + format: 'cjs', + dts: false, deps: { neverBundle: ['vscode'] }, }, { From d16ec6675ba0443eaa7ff17ad9ad84025ebbcb11 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 10:29:44 +0200 Subject: [PATCH 10/23] style: standardize code formatting --- .../core/vscode/use-vscode-file-load.hook.ts | 5 +- packages/bridge-protocol/src/index.ts | 6 +- packages/mcp/src/commons/qm-file.models.ts | 32 ++--- packages/mcp/src/commons/qm-file.utils.ts | 29 ++--- .../mcp/src/commons/tool-response.helpers.ts | 14 +-- .../mcp/src/commons/wireframe-file.service.ts | 14 ++- packages/mcp/src/core/index.ts | 6 +- packages/mcp/src/core/mcp.logger.ts | 10 +- packages/mcp/src/core/registry.client.ts | 36 +++--- packages/mcp/src/core/registry.models.ts | 4 +- packages/mcp/src/core/registry.utils.ts | 4 +- packages/mcp/src/index.ts | 88 ++++++++------ packages/mcp/src/renderer/app-url.consts.ts | 26 ++--- packages/mcp/src/renderer/bridge.server.ts | 34 +++--- .../mcp/src/renderer/chromium.resolver.ts | 82 +++++++------ .../mcp/src/renderer/headless.renderer.ts | 55 +++++---- packages/mcp/src/renderer/index.ts | 2 +- packages/mcp/src/renderer/page.session.ts | 110 +++++++++++------- packages/mcp/src/renderer/renderer.consts.ts | 6 +- .../capture-wireframe.handler.ts | 29 +++-- .../capture-wireframe.schema.ts | 6 +- .../capture-wireframe.tool.ts | 6 +- .../mcp/src/tools/capture-wireframe/index.ts | 2 +- .../get-wireframe-assets.handler.ts | 109 +++++++++-------- .../get-wireframe-assets.models.ts | 8 +- .../get-wireframe-assets.schema.ts | 6 +- .../get-wireframe-assets.tool.ts | 6 +- .../src/tools/get-wireframe-assets/index.ts | 2 +- .../get-wireframe-json.handler.ts | 14 ++- .../get-wireframe-json.schema.ts | 4 +- .../get-wireframe-json.tool.ts | 6 +- .../mcp/src/tools/get-wireframe-json/index.ts | 2 +- .../get-wireframe-pages.handler.ts | 16 +-- .../get-wireframe-pages.models.ts | 8 +- .../get-wireframe-pages.schema.ts | 4 +- .../get-wireframe-pages.tool.ts | 6 +- .../src/tools/get-wireframe-pages/index.ts | 2 +- .../mcp/src/tools/list-wireframes/index.ts | 2 +- .../list-wireframes.handler.ts | 46 +++++--- .../list-wireframes/list-wireframes.tool.ts | 4 +- .../vscode-extension/scripts/copy-mcp.mjs | 4 +- packages/vscode-extension/src/core/config.ts | 2 +- .../vscode-extension/src/editor/provider.ts | 15 +-- packages/vscode-extension/src/index.ts | 4 +- .../vscode-extension/src/mcp/mcp-command.ts | 8 +- .../src/mcp/mcp-registration.ts | 5 +- .../src/mcp/server-definition-provider.ts | 2 +- 47 files changed, 489 insertions(+), 402 deletions(-) diff --git a/apps/web/src/core/vscode/use-vscode-file-load.hook.ts b/apps/web/src/core/vscode/use-vscode-file-load.hook.ts index e0aa85db..dd4c0975 100644 --- a/apps/web/src/core/vscode/use-vscode-file-load.hook.ts +++ b/apps/web/src/core/vscode/use-vscode-file-load.hook.ts @@ -1,5 +1,8 @@ import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts'; -import { onMessage, sendToExtension } from '#common/utils/vscode-bridge.utils.ts'; +import { + onMessage, + sendToExtension, +} from '#common/utils/vscode-bridge.utils.ts'; import { QuickMockFileContract } from '#core/local-disk/local-disk.model'; import { useCanvasContext } from '#core/providers'; import { diff --git a/packages/bridge-protocol/src/index.ts b/packages/bridge-protocol/src/index.ts index 1e2d2f29..fd9e2ee4 100644 --- a/packages/bridge-protocol/src/index.ts +++ b/packages/bridge-protocol/src/index.ts @@ -40,7 +40,5 @@ export type AppMessage = payload?: ContentBbox; }; -export type PayloadOf< - U extends { type: string }, - T extends U['type'], -> = Extract extends { payload: infer P } ? P : undefined; +export type PayloadOf = + Extract extends { payload: infer P } ? P : undefined; diff --git a/packages/mcp/src/commons/qm-file.models.ts b/packages/mcp/src/commons/qm-file.models.ts index f71d7d64..8019f36d 100644 --- a/packages/mcp/src/commons/qm-file.models.ts +++ b/packages/mcp/src/commons/qm-file.models.ts @@ -1,28 +1,28 @@ export interface QmShape { - id: string - type: string + id: string; + type: string; otherProps?: { - imageSrc?: string - [key: string]: unknown - } - [key: string]: unknown + imageSrc?: string; + [key: string]: unknown; + }; + [key: string]: unknown; } export interface QmPage { - id: string - name: string - shapes: QmShape[] + id: string; + name: string; + shapes: QmShape[]; } export interface QmFileContract { - version: string - pages: QmPage[] - customColors: (string | null)[] - size: { width: number; height: number } + version: string; + pages: QmPage[]; + customColors: (string | null)[]; + size: { width: number; height: number }; } export interface QmFile { - absPath: string - content: string - parsed: QmFileContract + absPath: string; + content: string; + parsed: QmFileContract; } diff --git a/packages/mcp/src/commons/qm-file.utils.ts b/packages/mcp/src/commons/qm-file.utils.ts index c95bb058..9a6b0aa3 100644 --- a/packages/mcp/src/commons/qm-file.utils.ts +++ b/packages/mcp/src/commons/qm-file.utils.ts @@ -1,9 +1,9 @@ -import { readFile } from 'node:fs/promises' -import { resolve } from 'node:path' -import type { RegistryClient } from '../core/registry.models' -import type { QmFile, QmFileContract } from './qm-file.models' +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import type { RegistryClient } from '../core/registry.models'; +import type { QmFile, QmFileContract } from './qm-file.models'; -export type { QmFile, QmFileContract } +export type { QmFile, QmFileContract }; /** * Reads a .qm file (live registry first, disk fallback) and returns the raw @@ -11,18 +11,21 @@ export type { QmFile, QmFileContract } * * Throws if the file cannot be read or the JSON is invalid. */ -export async function readQmFile(path: string, registry: RegistryClient): Promise { - const root = process.env.QM_WORKSPACE_ROOT ?? process.cwd() - const absPath = resolve(root, path) +export async function readQmFile( + path: string, + registry: RegistryClient +): Promise { + const root = process.env.QM_WORKSPACE_ROOT ?? process.cwd(); + const absPath = resolve(root, path); - const live = await registry.getDocument(absPath) - const content = live ?? (await readFile(absPath, 'utf-8')) + const live = await registry.getDocument(absPath); + const content = live ?? (await readFile(absPath, 'utf-8')); - const parsed = JSON.parse(content) as QmFileContract + const parsed = JSON.parse(content) as QmFileContract; if (!Array.isArray(parsed.pages)) { - throw new Error(`"${path}" does not contain a valid pages array.`) + throw new Error(`"${path}" does not contain a valid pages array.`); } - return { absPath, content, parsed } + return { absPath, content, parsed }; } diff --git a/packages/mcp/src/commons/tool-response.helpers.ts b/packages/mcp/src/commons/tool-response.helpers.ts index 325c6ba3..88e89218 100644 --- a/packages/mcp/src/commons/tool-response.helpers.ts +++ b/packages/mcp/src/commons/tool-response.helpers.ts @@ -1,19 +1,19 @@ -type TextContent = { type: 'text'; text: string } -type ImageContent = { type: 'image'; data: string; mimeType: string } -type ToolContent = TextContent | ImageContent +type TextContent = { type: 'text'; text: string }; +type ImageContent = { type: 'image'; data: string; mimeType: string }; +type ToolContent = TextContent | ImageContent; export function toolText(text: string) { - return { content: [{ type: 'text' as const, text }] } + return { content: [{ type: 'text' as const, text }] }; } export function toolImage(data: string, mimeType: string) { - return { content: [{ type: 'image' as const, data, mimeType }] } + return { content: [{ type: 'image' as const, data, mimeType }] }; } export function toolMultiContent(items: ToolContent[]) { - return { content: items } + return { content: items }; } export function toolError(text: string) { - return { content: [{ type: 'text' as const, text }], isError: true as const } + return { content: [{ type: 'text' as const, text }], isError: true as const }; } diff --git a/packages/mcp/src/commons/wireframe-file.service.ts b/packages/mcp/src/commons/wireframe-file.service.ts index e02455ba..0531e134 100644 --- a/packages/mcp/src/commons/wireframe-file.service.ts +++ b/packages/mcp/src/commons/wireframe-file.service.ts @@ -1,13 +1,15 @@ -import type { RegistryClient } from '../core' -import type { QmFile } from './qm-file.models' -import { readQmFile } from './qm-file.utils' +import type { RegistryClient } from '../core'; +import type { QmFile } from './qm-file.models'; +import { readQmFile } from './qm-file.utils'; export interface WireframeFileService { - readFile(path: string): Promise + readFile(path: string): Promise; } -export function createWireframeFileService(registry: RegistryClient): WireframeFileService { +export function createWireframeFileService( + registry: RegistryClient +): WireframeFileService { return { readFile: (path: string) => readQmFile(path, registry), - } + }; } diff --git a/packages/mcp/src/core/index.ts b/packages/mcp/src/core/index.ts index af66b726..099305ae 100644 --- a/packages/mcp/src/core/index.ts +++ b/packages/mcp/src/core/index.ts @@ -1,3 +1,3 @@ -export * from './registry.client' -export * from './registry.models' -export * from './registry.utils' +export * from './registry.client'; +export * from './registry.models'; +export * from './registry.utils'; diff --git a/packages/mcp/src/core/mcp.logger.ts b/packages/mcp/src/core/mcp.logger.ts index 53e98387..d90055dd 100644 --- a/packages/mcp/src/core/mcp.logger.ts +++ b/packages/mcp/src/core/mcp.logger.ts @@ -1,9 +1,9 @@ -const PREFIX = '[quickmock-mcp]' +const PREFIX = '[quickmock-mcp]'; export const logInfo = (message: string, ...rest: unknown[]): void => { - console.error(`${PREFIX} ${message}`, ...rest) -} + console.error(`${PREFIX} ${message}`, ...rest); +}; export const logError = (message: string, ...rest: unknown[]): void => { - console.error(`${PREFIX} ${message}`, ...rest) -} + console.error(`${PREFIX} ${message}`, ...rest); +}; diff --git a/packages/mcp/src/core/registry.client.ts b/packages/mcp/src/core/registry.client.ts index 05aad41c..af3ef5fa 100644 --- a/packages/mcp/src/core/registry.client.ts +++ b/packages/mcp/src/core/registry.client.ts @@ -1,37 +1,37 @@ -import { readFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { nullClient, type RegistryClient } from './registry.models' -import { workspaceHash } from './registry.utils' +import { readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { nullClient, type RegistryClient } from './registry.models'; +import { workspaceHash } from './registry.utils'; /** HTTP client for the VSCode extension's registry server. Falls back to nullClient when the extension is not running. */ export function createRegistryClient(): RegistryClient { - const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd() + const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd(); - let port: number + let port: number; try { - const hash = workspaceHash(workspaceRoot) - const portFile = join(tmpdir(), `quickmock-${hash}.port`) - port = parseInt(readFileSync(portFile, 'utf-8').trim(), 10) + const hash = workspaceHash(workspaceRoot); + const portFile = join(tmpdir(), `quickmock-${hash}.port`); + port = parseInt(readFileSync(portFile, 'utf-8').trim(), 10); if (Number.isNaN(port)) { - return nullClient + return nullClient; } } catch { - return nullClient + return nullClient; } return { async getDocument(fsPath: string): Promise { try { - const url = `http://127.0.0.1:${port}/document?path=${encodeURIComponent(fsPath)}` - const res = await fetch(url, { signal: AbortSignal.timeout(2_000) }) + const url = `http://127.0.0.1:${port}/document?path=${encodeURIComponent(fsPath)}`; + const res = await fetch(url, { signal: AbortSignal.timeout(2_000) }); if (!res.ok) { - return null + return null; } - return await res.text() + return await res.text(); } catch { - return null + return null; } }, - } + }; } diff --git a/packages/mcp/src/core/registry.models.ts b/packages/mcp/src/core/registry.models.ts index 7fe6f39d..61b6e4e9 100644 --- a/packages/mcp/src/core/registry.models.ts +++ b/packages/mcp/src/core/registry.models.ts @@ -1,8 +1,8 @@ export interface RegistryClient { /** Returns live in-memory content for a file open in the editor, or null. */ - getDocument(fsPath: string): Promise + getDocument(fsPath: string): Promise; } export const nullClient: RegistryClient = { getDocument: async () => null, -} +}; diff --git a/packages/mcp/src/core/registry.utils.ts b/packages/mcp/src/core/registry.utils.ts index 195de4e3..abed4a92 100644 --- a/packages/mcp/src/core/registry.utils.ts +++ b/packages/mcp/src/core/registry.utils.ts @@ -1,5 +1,5 @@ -import { createHash } from 'node:crypto' +import { createHash } from 'node:crypto'; export function workspaceHash(root: string): string { - return createHash('md5').update(root).digest('hex').slice(0, 8) + return createHash('md5').update(root).digest('hex').slice(0, 8); } diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 0852ba87..9eed133b 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1,53 +1,67 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio' -import { createWireframeFileService } from './commons/wireframe-file.service' -import { createRegistryClient } from './core' -import { logError } from './core/mcp.logger' -import { captureWireframe } from './tools/capture-wireframe' -import { getWireframeAssets } from './tools/get-wireframe-assets' -import { getWireframeJson } from './tools/get-wireframe-json' -import { getWireframePages } from './tools/get-wireframe-pages' -import { listWireframes } from './tools/list-wireframes' - -const registry = createRegistryClient() -const service = createWireframeFileService(registry) - -const server = new McpServer({ name: 'quickmock', version: '0.1.0' }) - -server.registerTool(listWireframes.name, { description: listWireframes.description }, () => - listWireframes.execute(), -) +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio'; +import { createWireframeFileService } from './commons/wireframe-file.service'; +import { createRegistryClient } from './core'; +import { logError } from './core/mcp.logger'; +import { captureWireframe } from './tools/capture-wireframe'; +import { getWireframeAssets } from './tools/get-wireframe-assets'; +import { getWireframeJson } from './tools/get-wireframe-json'; +import { getWireframePages } from './tools/get-wireframe-pages'; +import { listWireframes } from './tools/list-wireframes'; + +const registry = createRegistryClient(); +const service = createWireframeFileService(registry); + +const server = new McpServer({ name: 'quickmock', version: '0.1.0' }); + +server.registerTool( + listWireframes.name, + { description: listWireframes.description }, + () => listWireframes.execute() +); server.registerTool( getWireframeJson.name, - { description: getWireframeJson.description, inputSchema: getWireframeJson.schema }, - (args) => getWireframeJson.execute(args, service), -) + { + description: getWireframeJson.description, + inputSchema: getWireframeJson.schema, + }, + args => getWireframeJson.execute(args, service) +); server.registerTool( getWireframePages.name, - { description: getWireframePages.description, inputSchema: getWireframePages.schema }, - (args) => getWireframePages.execute(args, service), -) + { + description: getWireframePages.description, + inputSchema: getWireframePages.schema, + }, + args => getWireframePages.execute(args, service) +); server.registerTool( captureWireframe.name, - { description: captureWireframe.description, inputSchema: captureWireframe.schema }, - (args) => captureWireframe.execute(args, service), -) + { + description: captureWireframe.description, + inputSchema: captureWireframe.schema, + }, + args => captureWireframe.execute(args, service) +); server.registerTool( getWireframeAssets.name, - { description: getWireframeAssets.description, inputSchema: getWireframeAssets.schema }, - (args) => getWireframeAssets.execute(args, service), -) + { + description: getWireframeAssets.description, + inputSchema: getWireframeAssets.schema, + }, + args => getWireframeAssets.execute(args, service) +); async function main() { - const transport = new StdioServerTransport() - await server.connect(transport) + const transport = new StdioServerTransport(); + await server.connect(transport); } -main().catch((err) => { - logError('fatal error:', err) - process.exit(1) -}) +main().catch(err => { + logError('fatal error:', err); + process.exit(1); +}); diff --git a/packages/mcp/src/renderer/app-url.consts.ts b/packages/mcp/src/renderer/app-url.consts.ts index bcd6a8de..cb66ad5c 100644 --- a/packages/mcp/src/renderer/app-url.consts.ts +++ b/packages/mcp/src/renderer/app-url.consts.ts @@ -1,28 +1,28 @@ -import { readFileSync } from 'node:fs' -import { homedir } from 'node:os' -import { join } from 'node:path' +import { readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; const DEFAULT_APP_URL = - 'https://quickmock.net/editor.html?env=vscode&headless=1' + 'https://quickmock.net/editor.html?env=vscode&headless=1'; -const APP_URL_FILE = join(homedir(), '.quickmock', 'app-url') +const APP_URL_FILE = join(homedir(), '.quickmock', 'app-url'); const readAppUrl = (): string => { try { - const value = readFileSync(APP_URL_FILE, 'utf-8').trim() - if (value) return value + const value = readFileSync(APP_URL_FILE, 'utf-8').trim(); + if (value) return value; } catch { // fallback to default } - return DEFAULT_APP_URL -} + return DEFAULT_APP_URL; +}; -export const QUICKMOCK_URL = readAppUrl() +export const QUICKMOCK_URL = readAppUrl(); export const QM_APP_ORIGIN = (() => { try { - return new URL(QUICKMOCK_URL).origin + return new URL(QUICKMOCK_URL).origin; } catch { - return QUICKMOCK_URL + return QUICKMOCK_URL; } -})() +})(); diff --git a/packages/mcp/src/renderer/bridge.server.ts b/packages/mcp/src/renderer/bridge.server.ts index 13e62c29..159d8f6f 100644 --- a/packages/mcp/src/renderer/bridge.server.ts +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -1,33 +1,33 @@ -import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol' -import { createServer, type Server } from 'node:http' -import type { AddressInfo } from 'node:net' -import { QUICKMOCK_URL } from './app-url.consts' +import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol'; +import { createServer, type Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { QUICKMOCK_URL } from './app-url.consts'; export interface BridgeServer { - server: Server - port: number + server: Server; + port: number; } /** HTTP server that serves the Puppeteer ↔ QuickMock iframe bridge page. */ export function startBridgeServer(): Promise { return new Promise((resolve, reject) => { const server = createServer((_req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) - res.end(buildBridgeHtml()) - }) + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(buildBridgeHtml()); + }); - server.on('error', reject) + server.on('error', reject); server.listen(0, '127.0.0.1', () => { - const { port } = server.address() as AddressInfo - resolve({ server, port }) - }) - }) + const { port } = server.address() as AddressInfo; + resolve({ server, port }); + }); + }); } function buildBridgeHtml(): string { - const READY = JSON.stringify(APP_MESSAGE_TYPE.READY) - const RENDER_COMPLETE = JSON.stringify(APP_MESSAGE_TYPE.RENDER_COMPLETE) + const READY = JSON.stringify(APP_MESSAGE_TYPE.READY); + const RENDER_COMPLETE = JSON.stringify(APP_MESSAGE_TYPE.RENDER_COMPLETE); return /* html */ ` @@ -70,5 +70,5 @@ function buildBridgeHtml(): string { }) -` +`; } diff --git a/packages/mcp/src/renderer/chromium.resolver.ts b/packages/mcp/src/renderer/chromium.resolver.ts index 476c4f48..900d9b47 100644 --- a/packages/mcp/src/renderer/chromium.resolver.ts +++ b/packages/mcp/src/renderer/chromium.resolver.ts @@ -4,61 +4,73 @@ import { detectBrowserPlatform, install, resolveBuildId, -} from '@puppeteer/browsers' -import { existsSync } from 'node:fs' -import { homedir, tmpdir } from 'node:os' -import { join } from 'node:path' -import { logError, logInfo } from '../core/mcp.logger' +} from '@puppeteer/browsers'; +import { existsSync } from 'node:fs'; +import { homedir, tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { logError, logInfo } from '../core/mcp.logger'; -const CHROMIUM_CHANNEL = 'stable' -const PROGRESS_LOG_STEP_PERCENT = 10 -const FALLBACK_CACHE_DIR = join(tmpdir(), 'quickmock-browsers') -const USER_CACHE_DIR_SEGMENTS = ['.quickmock', 'browsers'] as const +const CHROMIUM_CHANNEL = 'stable'; +const PROGRESS_LOG_STEP_PERCENT = 10; +const FALLBACK_CACHE_DIR = join(tmpdir(), 'quickmock-browsers'); +const USER_CACHE_DIR_SEGMENTS = ['.quickmock', 'browsers'] as const; -let cachedPath: Promise | undefined +let cachedPath: Promise | undefined; export function resolveChromiumExecutable(): Promise { - cachedPath ??= doResolve() - return cachedPath + cachedPath ??= doResolve(); + return cachedPath; } async function doResolve(): Promise { - const cacheDir = getCacheDir() - const platform = detectBrowserPlatform() - if (!platform) throw new Error('Unsupported platform for Chromium download.') + const cacheDir = getCacheDir(); + const platform = detectBrowserPlatform(); + if (!platform) throw new Error('Unsupported platform for Chromium download.'); - const buildId = await resolveBuildId(Browser.CHROME, platform, CHROMIUM_CHANNEL) - const executablePath = computeExecutablePath({ browser: Browser.CHROME, buildId, cacheDir }) + const buildId = await resolveBuildId( + Browser.CHROME, + platform, + CHROMIUM_CHANNEL + ); + const executablePath = computeExecutablePath({ + browser: Browser.CHROME, + buildId, + cacheDir, + }); - if (existsSync(executablePath)) return executablePath + if (existsSync(executablePath)) return executablePath; logInfo( - `Chromium not found in "${cacheDir}". Downloading ${Browser.CHROME}@${buildId} for ${platform}…`, - ) + `Chromium not found in "${cacheDir}". Downloading ${Browser.CHROME}@${buildId} for ${platform}…` + ); - let lastLoggedPercent = -1 + let lastLoggedPercent = -1; await install({ browser: Browser.CHROME, buildId, cacheDir, downloadProgressCallback: (downloaded, total) => { - if (!total) return - const percent = Math.floor((downloaded / total) * 100) - if (percent === lastLoggedPercent || percent % PROGRESS_LOG_STEP_PERCENT !== 0) return - lastLoggedPercent = percent - logInfo(`Downloading Chromium: ${percent}%`) + if (!total) return; + const percent = Math.floor((downloaded / total) * 100); + if ( + percent === lastLoggedPercent || + percent % PROGRESS_LOG_STEP_PERCENT !== 0 + ) + return; + lastLoggedPercent = percent; + logInfo(`Downloading Chromium: ${percent}%`); }, - }).catch((err) => { - logError('Chromium download failed:', err) - throw err - }) + }).catch(err => { + logError('Chromium download failed:', err); + throw err; + }); - logInfo(`Chromium ready at ${executablePath}`) - return executablePath + logInfo(`Chromium ready at ${executablePath}`); + return executablePath; } function getCacheDir(): string { - const home = homedir() - if (home) return join(home, ...USER_CACHE_DIR_SEGMENTS) - return FALLBACK_CACHE_DIR + const home = homedir(); + if (home) return join(home, ...USER_CACHE_DIR_SEGMENTS); + return FALLBACK_CACHE_DIR; } diff --git a/packages/mcp/src/renderer/headless.renderer.ts b/packages/mcp/src/renderer/headless.renderer.ts index 4282418a..e77960ef 100644 --- a/packages/mcp/src/renderer/headless.renderer.ts +++ b/packages/mcp/src/renderer/headless.renderer.ts @@ -1,54 +1,61 @@ -import type { Browser } from 'puppeteer-core' -import puppeteer from 'puppeteer-core' -import { startBridgeServer } from './bridge.server' -import { resolveChromiumExecutable } from './chromium.resolver' +import type { Browser } from 'puppeteer-core'; +import puppeteer from 'puppeteer-core'; +import { startBridgeServer } from './bridge.server'; +import { resolveChromiumExecutable } from './chromium.resolver'; import { screenshotIframe, sendFileToApp, waitForAppReady, waitForRenderComplete, watchNetworkFailures, -} from './page.session' +} from './page.session'; const BROWSER_LAUNCH_ARGS = [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', -] +]; /** Renders a .qm file in a headless Chromium instance and returns a PNG buffer. */ -export async function renderWireframe(content: string, fileName: string): Promise { - const { server, port } = await startBridgeServer() +export async function renderWireframe( + content: string, + fileName: string +): Promise { + const { server, port } = await startBridgeServer(); try { - return await withBrowser(async (browser) => { - const page = await browser.newPage() - await page.setViewport({ width: 1440, height: 900 }) - await page.goto(`http://127.0.0.1:${port}`, { waitUntil: 'domcontentloaded' }) + return await withBrowser(async browser => { + const page = await browser.newPage(); + await page.setViewport({ width: 1440, height: 900 }); + await page.goto(`http://127.0.0.1:${port}`, { + waitUntil: 'domcontentloaded', + }); - const networkFailure = watchNetworkFailures(page) - await waitForAppReady(page, networkFailure) - await sendFileToApp(page, content, fileName) - const bbox = await waitForRenderComplete(page) + const networkFailure = watchNetworkFailures(page); + await waitForAppReady(page, networkFailure); + await sendFileToApp(page, content, fileName); + const bbox = await waitForRenderComplete(page); - return screenshotIframe(page, bbox) - }) + return screenshotIframe(page, bbox); + }); } finally { - server.close() + server.close(); } } -async function withBrowser(fn: (browser: Browser) => Promise): Promise { - const executablePath = await resolveChromiumExecutable() +async function withBrowser( + fn: (browser: Browser) => Promise +): Promise { + const executablePath = await resolveChromiumExecutable(); const browser = await puppeteer.launch({ headless: true, executablePath, args: BROWSER_LAUNCH_ARGS, - }) + }); try { - return await fn(browser) + return await fn(browser); } finally { - await browser.close() + await browser.close(); } } diff --git a/packages/mcp/src/renderer/index.ts b/packages/mcp/src/renderer/index.ts index 57e43185..74363deb 100644 --- a/packages/mcp/src/renderer/index.ts +++ b/packages/mcp/src/renderer/index.ts @@ -1 +1 @@ -export * from './headless.renderer' +export * from './headless.renderer'; diff --git a/packages/mcp/src/renderer/page.session.ts b/packages/mcp/src/renderer/page.session.ts index 4b8c5d37..8a117842 100644 --- a/packages/mcp/src/renderer/page.session.ts +++ b/packages/mcp/src/renderer/page.session.ts @@ -1,95 +1,119 @@ -import type { Page } from 'puppeteer-core' -import { QM_APP_ORIGIN, QUICKMOCK_URL } from './app-url.consts' +import type { Page } from 'puppeteer-core'; +import { QM_APP_ORIGIN, QUICKMOCK_URL } from './app-url.consts'; import { LOCAL_INSTANCE_HINT, READY_TIMEOUT_MS, RENDER_TIMEOUT_MS, -} from './renderer.consts' +} from './renderer.consts'; export interface ContentBbox { - x: number - y: number - width: number - height: number + x: number; + y: number; + width: number; + height: number; } /** Rejects early on network failure — avoids waiting the full READY_TIMEOUT_MS. */ export function watchNetworkFailures(page: Page): Promise { return new Promise((_, reject) => { - page.on('requestfailed', (request) => { + page.on('requestfailed', request => { if (request.url().startsWith(QM_APP_ORIGIN)) { - const reason = request.failure()?.errorText ?? 'network error' + const reason = request.failure()?.errorText ?? 'network error'; reject( new Error( - `Cannot reach QuickMock app at "${QUICKMOCK_URL}": ${reason}.\n${LOCAL_INSTANCE_HINT}`, - ), - ) + `Cannot reach QuickMock app at "${QUICKMOCK_URL}": ${reason}.\n${LOCAL_INSTANCE_HINT}` + ) + ); } - }) - }) + }); + }); } /** Waits for `qm:ready`, racing against `networkFailure` for fast error reporting. */ -export async function waitForAppReady(page: Page, networkFailure: Promise): Promise { +export async function waitForAppReady( + page: Page, + networkFailure: Promise +): Promise { try { await Promise.race([ - page.waitForFunction(() => (window as Window & { __qmReady?: boolean }).__qmReady === true, { - timeout: READY_TIMEOUT_MS, - }), + page.waitForFunction( + () => (window as Window & { __qmReady?: boolean }).__qmReady === true, + { + timeout: READY_TIMEOUT_MS, + } + ), networkFailure, - ]) + ]); } catch (err) { - if (err instanceof Error && err.message.startsWith('Cannot reach')) throw err + if (err instanceof Error && err.message.startsWith('Cannot reach')) + throw err; const iframeLoaded = await page - .evaluate(() => (window as Window & { __iframeLoaded?: boolean }).__iframeLoaded === true) - .catch(() => false) + .evaluate( + () => + (window as Window & { __iframeLoaded?: boolean }).__iframeLoaded === + true + ) + .catch(() => false); if (iframeLoaded) { throw new Error( `QuickMock app loaded but did not emit qm:ready within ${READY_TIMEOUT_MS}ms — ` + - 'the app may have changed its postMessage protocol.', - ) + 'the app may have changed its postMessage protocol.' + ); } throw new Error( `Cannot reach QuickMock app at "${QUICKMOCK_URL}" — ` + - `the iframe did not load within ${READY_TIMEOUT_MS}ms.\n${LOCAL_INSTANCE_HINT}`, - ) + `the iframe did not load within ${READY_TIMEOUT_MS}ms.\n${LOCAL_INSTANCE_HINT}` + ); } } /** Sends the file content to the QuickMock app via postMessage → iframe. */ -export async function sendFileToApp(page: Page, content: string, fileName: string): Promise { +export async function sendFileToApp( + page: Page, + content: string, + fileName: string +): Promise { await page.evaluate( ({ content, fileName }) => { - const iframe = document.querySelector('iframe') as HTMLIFrameElement + const iframe = document.querySelector('iframe') as HTMLIFrameElement; iframe.contentWindow?.postMessage( { type: 'LOAD_FILE', payload: { data: JSON.parse(content), fileName } }, - '*', - ) + '*' + ); }, - { content, fileName }, - ) + { content, fileName } + ); } /** Waits for the app to emit `qm:render-complete` and returns the content bbox. */ -export async function waitForRenderComplete(page: Page): Promise { +export async function waitForRenderComplete( + page: Page +): Promise { await page.waitForFunction( - () => (window as Window & { __renderComplete?: boolean }).__renderComplete === true, - { timeout: RENDER_TIMEOUT_MS }, - ) + () => + (window as Window & { __renderComplete?: boolean }).__renderComplete === + true, + { timeout: RENDER_TIMEOUT_MS } + ); return page.evaluate( - () => (window as Window & { __renderBbox?: ContentBbox }).__renderBbox ?? undefined, - ) + () => + (window as Window & { __renderBbox?: ContentBbox }).__renderBbox ?? + undefined + ); } /** Screenshots the iframe, cropped to `bbox` when provided. */ -export async function screenshotIframe(page: Page, bbox: ContentBbox | undefined): Promise { - const iframe = await page.$('iframe') - if (!iframe) throw new Error('iframe element not found in renderer page') +export async function screenshotIframe( + page: Page, + bbox: ContentBbox | undefined +): Promise { + const iframe = await page.$('iframe'); + if (!iframe) throw new Error('iframe element not found in renderer page'); - const screenshot = await iframe.screenshot({ type: 'png', clip: bbox }) - return Buffer.from(screenshot) + const screenshot = await iframe.screenshot({ type: 'png', clip: bbox }); + return Buffer.from(screenshot); } diff --git a/packages/mcp/src/renderer/renderer.consts.ts b/packages/mcp/src/renderer/renderer.consts.ts index fc690df8..21a4ccb9 100644 --- a/packages/mcp/src/renderer/renderer.consts.ts +++ b/packages/mcp/src/renderer/renderer.consts.ts @@ -1,5 +1,5 @@ -export const READY_TIMEOUT_MS = 15_000 -export const RENDER_TIMEOUT_MS = 20_000 +export const READY_TIMEOUT_MS = 15_000; +export const RENDER_TIMEOUT_MS = 20_000; export const LOCAL_INSTANCE_HINT = - 'Set quickmock.appUrl in VS Code settings (or edit ~/.quickmock/app-url) to point at a local QuickMock instance, e.g. http://localhost:5173/editor.html.' + 'Set quickmock.appUrl in VS Code settings (or edit ~/.quickmock/app-url) to point at a local QuickMock instance, e.g. http://localhost:5173/editor.html.'; diff --git a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts index 358879d5..4e859003 100644 --- a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts @@ -6,20 +6,20 @@ import { basename } from 'node:path'; export async function captureWireframeHandler( args: { path: string; pageIndex?: number }, - service: WireframeFileService, + service: WireframeFileService ) { - const { path, pageIndex = 0 } = args + const { path, pageIndex = 0 } = args; try { - const { absPath, content, parsed } = await service.readFile(path) - const fileName = basename(absPath) - const pageCount = parsed.pages.length + const { absPath, content, parsed } = await service.readFile(path); + const fileName = basename(absPath); + const pageCount = parsed.pages.length; if (pageIndex < 0 || pageIndex >= pageCount) { return toolError( `Page index ${pageIndex} is out of range. ` + - `"${fileName}" has ${pageCount} page${pageCount === 1 ? '' : 's'} (indices 0–${pageCount - 1}).`, - ) + `"${fileName}" has ${pageCount} page${pageCount === 1 ? '' : 's'} (indices 0–${pageCount - 1}).` + ); } const targetContent = @@ -27,12 +27,17 @@ export async function captureWireframeHandler( ? content : JSON.stringify({ ...parsed, - pages: [parsed.pages[pageIndex], ...parsed.pages.filter((_, i) => i !== pageIndex)], - }) + pages: [ + parsed.pages[pageIndex], + ...parsed.pages.filter((_, i) => i !== pageIndex), + ], + }); - const png = await renderWireframe(targetContent, fileName) - return toolImage(png.toString('base64'), 'image/png') + const png = await renderWireframe(targetContent, fileName); + return toolImage(png.toString('base64'), 'image/png'); } catch (err) { - return toolError(`Error capturing wireframe at "${path} with ${QUICKMOCK_URL}": ${String(err)}`) + return toolError( + `Error capturing wireframe at "${path} with ${QUICKMOCK_URL}": ${String(err)}` + ); } } diff --git a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts index 08a361d8..bfc974ce 100644 --- a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.schema.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import { z } from 'zod'; export const captureWireframeSchema = { path: z.string().describe('Relative or absolute path to the .qm file'), @@ -8,6 +8,6 @@ export const captureWireframeSchema = { .min(0) .optional() .describe( - 'Zero-based index of the page to capture (default: 0). Use get_wireframe_pages to see all available pages.', + 'Zero-based index of the page to capture (default: 0). Use get_wireframe_pages to see all available pages.' ), -} +}; diff --git a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts index a74ad1a5..7e90be0e 100644 --- a/packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.tool.ts @@ -1,5 +1,5 @@ -import { captureWireframeHandler } from './capture-wireframe.handler' -import { captureWireframeSchema } from './capture-wireframe.schema' +import { captureWireframeHandler } from './capture-wireframe.handler'; +import { captureWireframeSchema } from './capture-wireframe.schema'; export const captureWireframe = { name: 'capture_wireframe' as const, @@ -8,4 +8,4 @@ export const captureWireframe = { 'Use get_wireframe_pages first to discover available pages and their indices. ', schema: captureWireframeSchema, execute: captureWireframeHandler, -} +}; diff --git a/packages/mcp/src/tools/capture-wireframe/index.ts b/packages/mcp/src/tools/capture-wireframe/index.ts index 316ca844..4a90e035 100644 --- a/packages/mcp/src/tools/capture-wireframe/index.ts +++ b/packages/mcp/src/tools/capture-wireframe/index.ts @@ -1 +1 @@ -export * from './capture-wireframe.tool' +export * from './capture-wireframe.tool'; diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts index d3c35a34..4185dfbc 100644 --- a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts @@ -1,20 +1,20 @@ -import { toolError, toolText } from '#/commons/tool-response.helpers' -import type { WireframeFileService } from '#/commons/wireframe-file.service' -import { createHash } from 'node:crypto' -import { mkdir, writeFile } from 'node:fs/promises' -import { basename, extname, join, resolve } from 'node:path' +import { toolError, toolText } from '#/commons/tool-response.helpers'; +import type { WireframeFileService } from '#/commons/wireframe-file.service'; +import { createHash } from 'node:crypto'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { basename, extname, join, resolve } from 'node:path'; interface ParsedDataUrl { - mimeType: string - base64: string + mimeType: string; + base64: string; } function parseDataUrl(src: string): ParsedDataUrl | null { - const match = src.match(/^data:([^;]+);base64,(.+)$/) + const match = src.match(/^data:([^;]+);base64,(.+)$/); if (!match?.[1] || !match[2]) { - return null + return null; } - return { mimeType: match[1], base64: match[2] } + return { mimeType: match[1], base64: match[2] }; } function mimeTypeToExtension(mimeType: string): string { @@ -25,81 +25,94 @@ function mimeTypeToExtension(mimeType: string): string { 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', - } - return map[mimeType] ?? 'bin' + }; + return map[mimeType] ?? 'bin'; } function sanitizeName(name: string): string { - return name.replace(/[^a-z0-9_-]/gi, '_').toLowerCase() + return name.replace(/[^a-z0-9_-]/gi, '_').toLowerCase(); } interface SavedAsset { - pageIndex: number - pageName: string - shapeId: string - filePath: string - mimeType: string + pageIndex: number; + pageName: string; + shapeId: string; + filePath: string; + mimeType: string; } export async function getWireframeAssetsHandler( args: { path: string; outputDir?: string }, - service: WireframeFileService, + service: WireframeFileService ) { try { - const { absPath, parsed } = await service.readFile(args.path) - const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd() - const wireframeName = sanitizeName(basename(absPath, extname(absPath))) + const { absPath, parsed } = await service.readFile(args.path); + const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd(); + const wireframeName = sanitizeName(basename(absPath, extname(absPath))); const targetDir = args.outputDir ? resolve(workspaceRoot, args.outputDir) - : join(workspaceRoot, 'images', wireframeName) + : join(workspaceRoot, 'images', wireframeName); - await mkdir(targetDir, { recursive: true }) + await mkdir(targetDir, { recursive: true }); - const seenHashes = new Set() - const saved: SavedAsset[] = [] + const seenHashes = new Set(); + const saved: SavedAsset[] = []; for (const [pageIndex, page] of parsed.pages.entries()) { for (const shape of page.shapes) { if (shape.type !== 'image') { - continue + continue; } - const src = shape.otherProps?.imageSrc + const src = shape.otherProps?.imageSrc; if (!src) { - continue + continue; } - const dataUrl = parseDataUrl(src) + const dataUrl = parseDataUrl(src); if (!dataUrl) { - continue + continue; } - const { mimeType, base64 } = dataUrl - const hash = createHash('sha1').update(base64).digest('hex') + const { mimeType, base64 } = dataUrl; + const hash = createHash('sha1').update(base64).digest('hex'); if (seenHashes.has(hash)) { - continue + continue; } - seenHashes.add(hash) - - const ext = mimeTypeToExtension(mimeType) - const fileName = `${sanitizeName(page.name)}-${shape.id}.${ext}` - const filePath = join(targetDir, fileName) - - await writeFile(filePath, Buffer.from(base64, 'base64')) - saved.push({ pageIndex, pageName: page.name, shapeId: shape.id, filePath, mimeType }) + seenHashes.add(hash); + + const ext = mimeTypeToExtension(mimeType); + const fileName = `${sanitizeName(page.name)}-${shape.id}.${ext}`; + const filePath = join(targetDir, fileName); + + await writeFile(filePath, Buffer.from(base64, 'base64')); + saved.push({ + pageIndex, + pageName: page.name, + shapeId: shape.id, + filePath, + mimeType, + }); } } if (saved.length === 0) { - return toolText('No image assets found in this wireframe.') + return toolText('No image assets found in this wireframe.'); } const summary = saved - .map((a) => `[${a.pageIndex}] "${a.pageName}" · ${a.shapeId} (${a.mimeType}) → ${a.filePath}`) - .join('\n') - - return toolText(`Saved ${saved.length} asset(s) to "${targetDir}":\n\n${summary}`) + .map( + a => + `[${a.pageIndex}] "${a.pageName}" · ${a.shapeId} (${a.mimeType}) → ${a.filePath}` + ) + .join('\n'); + + return toolText( + `Saved ${saved.length} asset(s) to "${targetDir}":\n\n${summary}` + ); } catch (err) { - return toolError(`Error extracting assets from "${args.path}": ${String(err)}`) + return toolError( + `Error extracting assets from "${args.path}": ${String(err)}` + ); } } diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts index a0e38902..0fc06689 100644 --- a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.models.ts @@ -1,6 +1,6 @@ export interface WireframeAsset { - shapeId: string - pageIndex: number - pageName: string - filePath: string + shapeId: string; + pageIndex: number; + pageName: string; + filePath: string; } diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts index cf95faad..cf1d203d 100644 --- a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.schema.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import { z } from 'zod'; export const getWireframeAssetsSchema = { path: z.string().describe('Relative or absolute path to the .qm file'), @@ -8,6 +8,6 @@ export const getWireframeAssetsSchema = { .describe( 'Directory where PNG files will be saved. ' + 'Relative paths are resolved from the workspace root. ' + - 'Defaults to "images/" inside the workspace root.', + 'Defaults to "images/" inside the workspace root.' ), -} +}; diff --git a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts index 8aa8c16a..a8a372fd 100644 --- a/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.tool.ts @@ -1,5 +1,5 @@ -import { getWireframeAssetsHandler } from './get-wireframe-assets.handler' -import { getWireframeAssetsSchema } from './get-wireframe-assets.schema' +import { getWireframeAssetsHandler } from './get-wireframe-assets.handler'; +import { getWireframeAssetsSchema } from './get-wireframe-assets.schema'; export const getWireframeAssets = { name: 'get_wireframe_assets' as const, @@ -10,4 +10,4 @@ export const getWireframeAssets = { 'and returns the images as inline content so they can be viewed directly.', schema: getWireframeAssetsSchema, execute: getWireframeAssetsHandler, -} +}; diff --git a/packages/mcp/src/tools/get-wireframe-assets/index.ts b/packages/mcp/src/tools/get-wireframe-assets/index.ts index 28ff0e64..0d67ef00 100644 --- a/packages/mcp/src/tools/get-wireframe-assets/index.ts +++ b/packages/mcp/src/tools/get-wireframe-assets/index.ts @@ -1 +1 @@ -export * from './get-wireframe-assets.tool' +export * from './get-wireframe-assets.tool'; diff --git a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts index 99d6386e..257c75ab 100644 --- a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts @@ -1,14 +1,16 @@ -import { toolError, toolText } from '#/commons/tool-response.helpers' -import type { WireframeFileService } from '#/commons/wireframe-file.service' +import { toolError, toolText } from '#/commons/tool-response.helpers'; +import type { WireframeFileService } from '#/commons/wireframe-file.service'; export async function getWireframeJsonHandler( args: { path: string }, - service: WireframeFileService, + service: WireframeFileService ) { try { - const { content } = await service.readFile(args.path) - return toolText(content) + const { content } = await service.readFile(args.path); + return toolText(content); } catch (err) { - return toolError(`Error reading wireframe at "${args.path}": ${String(err)}`) + return toolError( + `Error reading wireframe at "${args.path}": ${String(err)}` + ); } } diff --git a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts index ad71091d..5f3dec93 100644 --- a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.schema.ts @@ -1,5 +1,5 @@ -import { z } from 'zod' +import { z } from 'zod'; export const getWireframeJsonSchema = { path: z.string().describe('Relative or absolute path to the .qm file'), -} +}; diff --git a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts index 83489dca..ba237ba2 100644 --- a/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.tool.ts @@ -1,5 +1,5 @@ -import { getWireframeJsonHandler } from './get-wireframe-json.handler' -import { getWireframeJsonSchema } from './get-wireframe-json.schema' +import { getWireframeJsonHandler } from './get-wireframe-json.handler'; +import { getWireframeJsonSchema } from './get-wireframe-json.schema'; export const getWireframeJson = { name: 'get_wireframe_json' as const, @@ -7,4 +7,4 @@ export const getWireframeJson = { 'Returns the JSON content of a .qm wireframe file. When the file is open in the editor with unsaved changes, returns the latest in-memory state instead of the saved file.', schema: getWireframeJsonSchema, execute: getWireframeJsonHandler, -} +}; diff --git a/packages/mcp/src/tools/get-wireframe-json/index.ts b/packages/mcp/src/tools/get-wireframe-json/index.ts index 221d63bd..7e2ea9a1 100644 --- a/packages/mcp/src/tools/get-wireframe-json/index.ts +++ b/packages/mcp/src/tools/get-wireframe-json/index.ts @@ -1 +1 @@ -export * from './get-wireframe-json.tool' +export * from './get-wireframe-json.tool'; diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts index cb8354a1..ce5a473b 100644 --- a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.handler.ts @@ -1,23 +1,23 @@ -import { toolError, toolText } from '#/commons/tool-response.helpers' -import type { WireframeFileService } from '#/commons/wireframe-file.service' -import type { WireframePage } from './get-wireframe-pages.models' +import { toolError, toolText } from '#/commons/tool-response.helpers'; +import type { WireframeFileService } from '#/commons/wireframe-file.service'; +import type { WireframePage } from './get-wireframe-pages.models'; export async function getWireframePagesHandler( args: { path: string }, - service: WireframeFileService, + service: WireframeFileService ) { try { - const { parsed } = await service.readFile(args.path) + const { parsed } = await service.readFile(args.path); const pages: WireframePage[] = parsed.pages.map((page, index) => ({ index, id: page.id, name: page.name, shapeCount: page.shapes.length, - })) + })); - return toolText(JSON.stringify(pages, null, 2)) + return toolText(JSON.stringify(pages, null, 2)); } catch (err) { - return toolError(`Error reading pages from "${args.path}": ${String(err)}`) + return toolError(`Error reading pages from "${args.path}": ${String(err)}`); } } diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts index e784ca9f..b52537f7 100644 --- a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.models.ts @@ -1,6 +1,6 @@ export interface WireframePage { - index: number - id: string - name: string - shapeCount: number + index: number; + id: string; + name: string; + shapeCount: number; } diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts index 24fa6378..ef59c58e 100644 --- a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.schema.ts @@ -1,5 +1,5 @@ -import { z } from 'zod' +import { z } from 'zod'; export const getWireframePagesSchema = { path: z.string().describe('Relative or absolute path to the .qm file'), -} +}; diff --git a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts index 9aa20939..970a8163 100644 --- a/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts +++ b/packages/mcp/src/tools/get-wireframe-pages/get-wireframe-pages.tool.ts @@ -1,5 +1,5 @@ -import { getWireframePagesHandler } from './get-wireframe-pages.handler' -import { getWireframePagesSchema } from './get-wireframe-pages.schema' +import { getWireframePagesHandler } from './get-wireframe-pages.handler'; +import { getWireframePagesSchema } from './get-wireframe-pages.schema'; export const getWireframePages = { name: 'get_wireframe_pages' as const, @@ -8,4 +8,4 @@ export const getWireframePages = { 'Use the index values with capture_wireframe to screenshot a specific page.', schema: getWireframePagesSchema, execute: getWireframePagesHandler, -} +}; diff --git a/packages/mcp/src/tools/get-wireframe-pages/index.ts b/packages/mcp/src/tools/get-wireframe-pages/index.ts index 09c29ae6..9de8913e 100644 --- a/packages/mcp/src/tools/get-wireframe-pages/index.ts +++ b/packages/mcp/src/tools/get-wireframe-pages/index.ts @@ -1 +1 @@ -export * from './get-wireframe-pages.tool' +export * from './get-wireframe-pages.tool'; diff --git a/packages/mcp/src/tools/list-wireframes/index.ts b/packages/mcp/src/tools/list-wireframes/index.ts index 1ad3a3ce..5467112e 100644 --- a/packages/mcp/src/tools/list-wireframes/index.ts +++ b/packages/mcp/src/tools/list-wireframes/index.ts @@ -1 +1 @@ -export * from './list-wireframes.tool' +export * from './list-wireframes.tool'; diff --git a/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts b/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts index 97453ba9..15323d30 100644 --- a/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts +++ b/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts @@ -1,44 +1,52 @@ -import { toolError, toolText } from '#/commons/tool-response.helpers' -import { readdir } from 'node:fs/promises' -import { join, relative } from 'node:path' - -const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'out', '.vscode']) +import { toolError, toolText } from '#/commons/tool-response.helpers'; +import { readdir } from 'node:fs/promises'; +import { join, relative } from 'node:path'; + +const IGNORED_DIRS = new Set([ + 'node_modules', + '.git', + 'dist', + 'out', + '.vscode', +]); export async function listWireframesHandler() { - const root = process.env.QM_WORKSPACE_ROOT ?? process.cwd() + const root = process.env.QM_WORKSPACE_ROOT ?? process.cwd(); try { - const files = await collectQmFiles(root, root) - return toolText(JSON.stringify(files, null, 2)) + const files = await collectQmFiles(root, root); + return toolText(JSON.stringify(files, null, 2)); } catch (err) { - return toolError(`Error scanning workspace: ${String(err)}`) + return toolError(`Error scanning workspace: ${String(err)}`); } } async function collectQmFiles(dir: string, root: string): Promise { - let entries: import('node:fs').Dirent[] + let entries: import('node:fs').Dirent[]; try { - entries = (await readdir(dir, { withFileTypes: true })) as unknown as import('node:fs').Dirent[] + entries = (await readdir(dir, { + withFileTypes: true, + })) as unknown as import('node:fs').Dirent[]; } catch { - return [] + return []; } - const results: string[] = [] + const results: string[] = []; for (const entry of entries) { if (IGNORED_DIRS.has(entry.name)) { - continue + continue; } - const fullPath = join(dir, entry.name) + const fullPath = join(dir, entry.name); if (entry.isDirectory()) { - const nested = await collectQmFiles(fullPath, root) - results.push(...nested) + const nested = await collectQmFiles(fullPath, root); + results.push(...nested); } else if (entry.isFile() && entry.name.endsWith('.qm')) { - results.push(relative(root, fullPath)) + results.push(relative(root, fullPath)); } } - return results + return results; } diff --git a/packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts b/packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts index 162c825f..9b1473d1 100644 --- a/packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts +++ b/packages/mcp/src/tools/list-wireframes/list-wireframes.tool.ts @@ -1,8 +1,8 @@ -import { listWireframesHandler } from './list-wireframes.handler' +import { listWireframesHandler } from './list-wireframes.handler'; export const listWireframes = { name: 'list_wireframes' as const, description: 'Lists all .qm wireframe files in the current workspace. Returns paths relative to the workspace root.', execute: listWireframesHandler, -} +}; diff --git a/packages/vscode-extension/scripts/copy-mcp.mjs b/packages/vscode-extension/scripts/copy-mcp.mjs index 8c4266f5..14e221e7 100644 --- a/packages/vscode-extension/scripts/copy-mcp.mjs +++ b/packages/vscode-extension/scripts/copy-mcp.mjs @@ -6,7 +6,9 @@ import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const here = dirname(fileURLToPath(import.meta.url)); -const mcpDir = dirname(require.resolve('@lemoncode/quickmock-mcp/package.json')); +const mcpDir = dirname( + require.resolve('@lemoncode/quickmock-mcp/package.json') +); const source = join(mcpDir, 'dist'); const distDir = join(here, '..', 'dist'); diff --git a/packages/vscode-extension/src/core/config.ts b/packages/vscode-extension/src/core/config.ts index c68c4c9b..5eb6ff16 100644 --- a/packages/vscode-extension/src/core/config.ts +++ b/packages/vscode-extension/src/core/config.ts @@ -41,6 +41,6 @@ export const syncAppUrlFile = (): void => { }; export const onAppUrlChange = (listener: () => void): vscode.Disposable => - vscode.workspace.onDidChangeConfiguration((e) => { + vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(FULL_KEY)) listener(); }); diff --git a/packages/vscode-extension/src/editor/provider.ts b/packages/vscode-extension/src/editor/provider.ts index dc5e1e2c..a9b9fa4e 100644 --- a/packages/vscode-extension/src/editor/provider.ts +++ b/packages/vscode-extension/src/editor/provider.ts @@ -16,9 +16,7 @@ import { import { handleWebviewMessage } from './handlers'; import { getHtml } from './panel'; -export class QuickMockEditorProvider - implements vscode.CustomEditorProvider -{ +export class QuickMockEditorProvider implements vscode.CustomEditorProvider { static register(context: vscode.ExtensionContext): vscode.Disposable { const provider = new QuickMockEditorProvider(context.extensionUri); const editorRegistration = vscode.window.registerCustomEditorProvider( @@ -86,10 +84,9 @@ export class QuickMockEditorProvider return { id: context.destination.toString(), delete: () => { - vscode.workspace.fs.delete(context.destination).then( - undefined, - () => {} - ); + vscode.workspace.fs + .delete(context.destination) + .then(undefined, () => {}); }, }; } @@ -102,7 +99,7 @@ export class QuickMockEditorProvider const key = doc.uri.toString(); this.panels.set(key, [...(this.panels.get(key) ?? []), panel]); panel.onDidDispose(() => { - const remaining = (this.panels.get(key) ?? []).filter((p) => p !== panel); + const remaining = (this.panels.get(key) ?? []).filter(p => p !== panel); this.panels.set(key, remaining); if (remaining.length === 0) { documentRegistry.delete(doc.uri.fsPath); @@ -120,7 +117,7 @@ export class QuickMockEditorProvider ); panel.webview.onDidReceiveMessage(async (msg: AppMessage) => { - await handleWebviewMessage(msg, doc, (reply) => + await handleWebviewMessage(msg, doc, reply => panel.webview.postMessage(reply satisfies HostMessage) ); documentRegistry.set(doc.uri.fsPath, doc.content); diff --git a/packages/vscode-extension/src/index.ts b/packages/vscode-extension/src/index.ts index 724e52e0..6f623f0d 100644 --- a/packages/vscode-extension/src/index.ts +++ b/packages/vscode-extension/src/index.ts @@ -19,14 +19,14 @@ export const activate = (context: vscode.ExtensionContext) => { const registryServer = new RegistryServer(); registryServer .start(context) - .catch((err) => logError('Failed to start MCP registry server:', err)); + .catch(err => logError('Failed to start MCP registry server:', err)); context.subscriptions.push(registerQuickMockMcpServerProvider(context)); context.subscriptions.push(registerConnectMcpCommand(context)); cleanupStaleMcpRegistration() .then(() => registerMcpServer(context)) - .catch((err) => logError('Failed to register MCP server:', err)); + .catch(err => logError('Failed to register MCP server:', err)); context.subscriptions.push( vscode.commands.registerCommand('quickmock.newWireframe', () => { diff --git a/packages/vscode-extension/src/mcp/mcp-command.ts b/packages/vscode-extension/src/mcp/mcp-command.ts index bb4c021d..91c6d00b 100644 --- a/packages/vscode-extension/src/mcp/mcp-command.ts +++ b/packages/vscode-extension/src/mcp/mcp-command.ts @@ -40,13 +40,13 @@ export const registerConnectMcpCommand = ( const items: vscode.QuickPickItem[] = [ { label: 'Providers', kind: vscode.QuickPickItemKind.Separator }, - ...results.map((r) => ({ + ...results.map(r => ({ label: `${STATUS_ICON[r.status]} ${r.label}`, description: r.status === 'registered' ? 'registered' : r.status, detail: r.detail, })), { label: 'Available tools', kind: vscode.QuickPickItemKind.Separator }, - ...TOOLS.map((t) => ({ + ...TOOLS.map(t => ({ label: `$(tools) ${t.name}`, description: t.description, })), @@ -66,8 +66,6 @@ export const registerConnectMcpCommand = ( if (selected?.description === serverPath) { await vscode.env.clipboard.writeText(serverPath); - vscode.window.showInformationMessage( - 'Server path copied to clipboard.' - ); + vscode.window.showInformationMessage('Server path copied to clipboard.'); } }); diff --git a/packages/vscode-extension/src/mcp/mcp-registration.ts b/packages/vscode-extension/src/mcp/mcp-registration.ts index c7d6de79..b98a76aa 100644 --- a/packages/vscode-extension/src/mcp/mcp-registration.ts +++ b/packages/vscode-extension/src/mcp/mcp-registration.ts @@ -38,8 +38,7 @@ const registerInVSCode = async ( ): Promise => { try { const config = vscode.workspace.getConfiguration(MCP_CONFIG_SECTION); - const servers = - config.get>(MCP_SERVERS_KEY) ?? {}; + const servers = config.get>(MCP_SERVERS_KEY) ?? {}; servers[MCP_SERVER_ID] = entry; await config.update( MCP_SERVERS_KEY, @@ -111,7 +110,7 @@ export const registerMcpServer = async ( const results: RegistrationResult[] = [ await registerInVSCode(entry), - ...getMcpClientTargets().map((t) => registerInClientTarget(t, entry)), + ...getMcpClientTargets().map(t => registerInClientTarget(t, entry)), ]; for (const r of results) { diff --git a/packages/vscode-extension/src/mcp/server-definition-provider.ts b/packages/vscode-extension/src/mcp/server-definition-provider.ts index b8289240..dc74be6c 100644 --- a/packages/vscode-extension/src/mcp/server-definition-provider.ts +++ b/packages/vscode-extension/src/mcp/server-definition-provider.ts @@ -41,7 +41,7 @@ export const registerQuickMockMcpServerProvider = ( MCP_SERVER_ID, { onDidChangeMcpServerDefinitions: didChangeDefinitions.event, - provideMcpServerDefinitions: async (_token) => { + provideMcpServerDefinitions: async _token => { logInfo('Providing MCP server definitions'); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; if (!workspaceFolder) { From 228231672ef2dcd3ba790fc835360f4db11bff28 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 10:39:42 +0200 Subject: [PATCH 11/23] chore(changeset): initial 0.1.0 release --- .changeset/quick-meals-send.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .changeset/quick-meals-send.md diff --git a/.changeset/quick-meals-send.md b/.changeset/quick-meals-send.md new file mode 100644 index 00000000..4817fe46 --- /dev/null +++ b/.changeset/quick-meals-send.md @@ -0,0 +1,28 @@ +--- +'quickmock': minor +'@lemoncode/quickmock-mcp': minor +--- + +First public release of the QuickMock VS Code extension and its MCP server. + +**`quickmock` (VS Code extension)** + +- Custom editor for `.qm` files backed by the QuickMock web app, served inside a webview. + +- `quickmock.appUrl` setting (default `https://quickmock.net/editor.html`) to point the editor and the MCP renderer at any QuickMock instance. Changes refresh open editors and respawn the MCP server. + +- Automatic MCP server registration for VS Code / GitHub Copilot, Claude Code, Cursor, Windsurf and Claude Desktop, plus a dynamic `McpServerDefinitionProvider`. + +- `QuickMock: Connect MCP Server` command to re-run registration on demand and inspect the available tools. + +- Stale registrations pointing at a missing entrypoint are pruned automatically on activation. + +**`@lemoncode/quickmock-mcp` (MCP server)** + +- MCP tools to explore and render wireframes: `list_wireframes`, `get_wireframe_json`, `get_wireframe_pages`, `get_wireframe_assets` and `capture_wireframe`. + +- Headless screenshot pipeline via `puppeteer-core` against the QuickMock app, using a postMessage bridge. + +- On-demand Chromium download via `@puppeteer/browsers`, cached under `~/.quickmock/browsers`, so headless rendering works without relying on the user's local browser install. + +- Reads the target app URL from `~/.quickmock/app-url` (written by the extension) with a production fallback, so the MCP works out of the box regardless of how it is spawned. From 471bd68f8acbd2a5765014f54356db37c4ee2cf4 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 11:21:03 +0200 Subject: [PATCH 12/23] feat(vscode-extension): include QM_APP_ORIGIN in bridge server for iframe messaging --- packages/mcp/src/renderer/bridge.server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mcp/src/renderer/bridge.server.ts b/packages/mcp/src/renderer/bridge.server.ts index 159d8f6f..3c8b4a97 100644 --- a/packages/mcp/src/renderer/bridge.server.ts +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -1,7 +1,7 @@ import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol'; import { createServer, type Server } from 'node:http'; import type { AddressInfo } from 'node:net'; -import { QUICKMOCK_URL } from './app-url.consts'; +import { QM_APP_ORIGIN, QUICKMOCK_URL } from './app-url.consts'; export interface BridgeServer { server: Server; @@ -28,6 +28,7 @@ export function startBridgeServer(): Promise { function buildBridgeHtml(): string { const READY = JSON.stringify(APP_MESSAGE_TYPE.READY); const RENDER_COMPLETE = JSON.stringify(APP_MESSAGE_TYPE.RENDER_COMPLETE); + const QM_ORIGIN = JSON.stringify(QM_APP_ORIGIN); return /* html */ ` @@ -65,7 +66,7 @@ function buildBridgeHtml(): string { // Messages coming DOWN from Puppeteer (page.evaluate) → forward to iframe if (iframe.contentWindow) { - iframe.contentWindow.postMessage(event.data, '*') + iframe.contentWindow.postMessage(event.data, ${QM_ORIGIN}) } }) From 189d822970903f1689834035d339f655f1a32f2f Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 11:24:00 +0200 Subject: [PATCH 13/23] feat(mcp): include QM_APP_ORIGIN in postMessage for iframe file loading --- packages/mcp/src/renderer/page.session.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mcp/src/renderer/page.session.ts b/packages/mcp/src/renderer/page.session.ts index 8a117842..f732c9d3 100644 --- a/packages/mcp/src/renderer/page.session.ts +++ b/packages/mcp/src/renderer/page.session.ts @@ -77,14 +77,14 @@ export async function sendFileToApp( fileName: string ): Promise { await page.evaluate( - ({ content, fileName }) => { + ({ content, fileName, origin }) => { const iframe = document.querySelector('iframe') as HTMLIFrameElement; iframe.contentWindow?.postMessage( { type: 'LOAD_FILE', payload: { data: JSON.parse(content), fileName } }, - '*' + origin ); }, - { content, fileName } + { content, fileName, origin: QM_APP_ORIGIN } ); } From bb0a43282a944a6621c10cd3b57affdf8b1e9b2b Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 11:25:38 +0200 Subject: [PATCH 14/23] refactor(headless.renderer): remove unused browser launch arguments --- packages/mcp/src/renderer/headless.renderer.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/mcp/src/renderer/headless.renderer.ts b/packages/mcp/src/renderer/headless.renderer.ts index e77960ef..733b7c8e 100644 --- a/packages/mcp/src/renderer/headless.renderer.ts +++ b/packages/mcp/src/renderer/headless.renderer.ts @@ -10,13 +10,6 @@ import { watchNetworkFailures, } from './page.session'; -const BROWSER_LAUNCH_ARGS = [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-web-security', - '--disable-features=IsolateOrigins,site-per-process', -]; - /** Renders a .qm file in a headless Chromium instance and returns a PNG buffer. */ export async function renderWireframe( content: string, @@ -51,7 +44,6 @@ async function withBrowser( const browser = await puppeteer.launch({ headless: true, executablePath, - args: BROWSER_LAUNCH_ARGS, }); try { return await fn(browser); From 84c17973a95fd65d6dcb807d58fb34935c303dbc Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 11:53:12 +0200 Subject: [PATCH 15/23] feat: implement quickmock registry protocol and integrate with mcp and vscode extension --- package-lock.json | 31 +++++++++++++ packages/mcp/package.json | 1 + packages/mcp/src/core/index.ts | 1 - packages/mcp/src/core/registry.client.ts | 45 +++++++++++-------- packages/mcp/src/core/registry.utils.ts | 5 --- packages/registry-protocol/package.json | 16 +++++++ packages/registry-protocol/src/consts.ts | 4 ++ packages/registry-protocol/src/index.ts | 2 + packages/registry-protocol/src/utils.ts | 27 +++++++++++ packages/registry-protocol/tsconfig.json | 11 +++++ packages/vscode-extension/package.json | 1 + .../src/mcp/registry-server.ts | 38 +++++++++++----- 12 files changed, 145 insertions(+), 37 deletions(-) delete mode 100644 packages/mcp/src/core/registry.utils.ts create mode 100644 packages/registry-protocol/package.json create mode 100644 packages/registry-protocol/src/consts.ts create mode 100644 packages/registry-protocol/src/index.ts create mode 100644 packages/registry-protocol/src/utils.ts create mode 100644 packages/registry-protocol/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 4ab198fc..b9b651cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1283,6 +1283,10 @@ "resolved": "packages/mcp", "link": true }, + "node_modules/@lemoncode/quickmock-registry-protocol": { + "resolved": "packages/registry-protocol", + "link": true + }, "node_modules/@lemoncode/tsdown-config": { "resolved": "tooling/tsdown", "link": true @@ -11039,6 +11043,7 @@ }, "devDependencies": { "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-protocol": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", @@ -11062,6 +11067,31 @@ "dev": true, "license": "MIT" }, + "packages/registry-protocol": { + "name": "@lemoncode/quickmock-registry-protocol", + "version": "0.0.0", + "devDependencies": { + "@lemoncode/typescript-config": "*", + "@types/node": "^22.19.17" + } + }, + "packages/registry-protocol/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "packages/registry-protocol/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "packages/vscode-extension": { "name": "quickmock", "version": "0.0.1", @@ -11071,6 +11101,7 @@ }, "devDependencies": { "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-protocol": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 87272b9b..25cd77bd 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -31,6 +31,7 @@ }, "devDependencies": { "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-protocol": "*", "@lemoncode/typescript-config": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/vitest-config": "*", diff --git a/packages/mcp/src/core/index.ts b/packages/mcp/src/core/index.ts index 099305ae..b1f3231f 100644 --- a/packages/mcp/src/core/index.ts +++ b/packages/mcp/src/core/index.ts @@ -1,3 +1,2 @@ export * from './registry.client'; export * from './registry.models'; -export * from './registry.utils'; diff --git a/packages/mcp/src/core/registry.client.ts b/packages/mcp/src/core/registry.client.ts index af3ef5fa..c78eb9f7 100644 --- a/packages/mcp/src/core/registry.client.ts +++ b/packages/mcp/src/core/registry.client.ts @@ -1,33 +1,40 @@ import { readFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { + buildPortFilePath, + DOCUMENT_ROUTE, + LOOPBACK_HOST, + parsePortFile, + TOKEN_HEADER, +} from '@lemoncode/quickmock-registry-protocol'; import { nullClient, type RegistryClient } from './registry.models'; -import { workspaceHash } from './registry.utils'; -/** HTTP client for the VSCode extension's registry server. Falls back to nullClient when the extension is not running. */ -export function createRegistryClient(): RegistryClient { - const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd(); +const REQUEST_TIMEOUT_MS = 2_000; - let port: number; +function readPortFile(workspaceRoot: string) { try { - const hash = workspaceHash(workspaceRoot); - const portFile = join(tmpdir(), `quickmock-${hash}.port`); - port = parseInt(readFileSync(portFile, 'utf-8').trim(), 10); - if (Number.isNaN(port)) { - return nullClient; - } + const raw = readFileSync(buildPortFilePath(workspaceRoot), 'utf-8'); + return parsePortFile(raw); } catch { - return nullClient; + return null; } +} + +/** HTTP client for the VSCode extension's registry server. Falls back to nullClient when the extension is not running. */ +export function createRegistryClient(): RegistryClient { + const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd(); + const portFile = readPortFile(workspaceRoot); + if (!portFile) return nullClient; + const { port, token } = portFile; return { async getDocument(fsPath: string): Promise { try { - const url = `http://127.0.0.1:${port}/document?path=${encodeURIComponent(fsPath)}`; - const res = await fetch(url, { signal: AbortSignal.timeout(2_000) }); - if (!res.ok) { - return null; - } + const url = `http://${LOOPBACK_HOST}:${port}${DOCUMENT_ROUTE}?path=${encodeURIComponent(fsPath)}`; + const res = await fetch(url, { + headers: { [TOKEN_HEADER]: token }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) return null; return await res.text(); } catch { return null; diff --git a/packages/mcp/src/core/registry.utils.ts b/packages/mcp/src/core/registry.utils.ts deleted file mode 100644 index abed4a92..00000000 --- a/packages/mcp/src/core/registry.utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createHash } from 'node:crypto'; - -export function workspaceHash(root: string): string { - return createHash('md5').update(root).digest('hex').slice(0, 8); -} diff --git a/packages/registry-protocol/package.json b/packages/registry-protocol/package.json new file mode 100644 index 00000000..8a892b7b --- /dev/null +++ b/packages/registry-protocol/package.json @@ -0,0 +1,16 @@ +{ + "name": "@lemoncode/quickmock-registry-protocol", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@lemoncode/typescript-config": "*", + "@types/node": "^22.19.17" + } +} diff --git a/packages/registry-protocol/src/consts.ts b/packages/registry-protocol/src/consts.ts new file mode 100644 index 00000000..9c6418bb --- /dev/null +++ b/packages/registry-protocol/src/consts.ts @@ -0,0 +1,4 @@ +export const TOKEN_HEADER = 'x-quickmock-token'; +export const LOOPBACK_HOST = '127.0.0.1'; +export const DOCUMENT_ROUTE = '/document'; +export const PORT_TOKEN_SEPARATOR = ' '; diff --git a/packages/registry-protocol/src/index.ts b/packages/registry-protocol/src/index.ts new file mode 100644 index 00000000..8b52df48 --- /dev/null +++ b/packages/registry-protocol/src/index.ts @@ -0,0 +1,2 @@ +export * from './consts'; +export * from './utils'; diff --git a/packages/registry-protocol/src/utils.ts b/packages/registry-protocol/src/utils.ts new file mode 100644 index 00000000..2bad2c89 --- /dev/null +++ b/packages/registry-protocol/src/utils.ts @@ -0,0 +1,27 @@ +import { createHash } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { PORT_TOKEN_SEPARATOR } from './consts'; + +const WORKSPACE_HASH_LENGTH = 8; + +export function buildPortFilePath(workspaceRoot: string): string { + const hash = createHash('md5') + .update(workspaceRoot) + .digest('hex') + .slice(0, WORKSPACE_HASH_LENGTH); + return join(tmpdir(), `quickmock-${hash}.port`); +} + +export function encodePortFile(port: number, token: string): string { + return `${port}${PORT_TOKEN_SEPARATOR}${token}`; +} + +export function parsePortFile( + raw: string +): { port: number; token: string } | null { + const [portStr, token] = raw.trim().split(PORT_TOKEN_SEPARATOR); + const port = parseInt(portStr ?? '', 10); + if (Number.isNaN(port) || !token) return null; + return { port, token }; +} diff --git a/packages/registry-protocol/tsconfig.json b/packages/registry-protocol/tsconfig.json new file mode 100644 index 00000000..e5896c35 --- /dev/null +++ b/packages/registry-protocol/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@lemoncode/typescript-config/base", + "include": ["src"], + "compilerOptions": { + "target": "ES2024", + "types": ["node"], + "lib": ["ES2024"], + "noEmit": true, + "rootDir": "src" + } +} diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index cc7ba954..40117d14 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-protocol": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*", diff --git a/packages/vscode-extension/src/mcp/registry-server.ts b/packages/vscode-extension/src/mcp/registry-server.ts index 3b190f03..55ac1214 100644 --- a/packages/vscode-extension/src/mcp/registry-server.ts +++ b/packages/vscode-extension/src/mcp/registry-server.ts @@ -1,17 +1,26 @@ -import { createHash } from 'node:crypto'; +import { randomBytes } from 'node:crypto'; import { unlinkSync, writeFileSync } from 'node:fs'; import { createServer, type IncomingMessage, type ServerResponse, } from 'node:http'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { + buildPortFilePath, + DOCUMENT_ROUTE, + encodePortFile, + LOOPBACK_HOST, + TOKEN_HEADER, +} from '@lemoncode/quickmock-registry-protocol'; import * as vscode from 'vscode'; import { documentRegistry } from '#core/document-registry'; +const TOKEN_BYTE_LENGTH = 32; +const PORT_FILE_MODE = 0o600; + export class RegistryServer { private portFile: string | null = null; + private token = ''; async start(context: vscode.ExtensionContext): Promise { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; @@ -19,17 +28,19 @@ export class RegistryServer { return; } - const hash = workspaceHash(workspaceRoot); - this.portFile = join(tmpdir(), `quickmock-${hash}.port`); + this.portFile = buildPortFilePath(workspaceRoot); + this.token = randomBytes(TOKEN_BYTE_LENGTH).toString('hex'); const server = createServer((req, res) => this.handleRequest(req, res)); await new Promise((resolve, reject) => { server.on('error', reject); - server.listen(0, '127.0.0.1', () => { - const addr = server.address() as { port: number }; + server.listen(0, LOOPBACK_HOST, () => { + const { port } = server.address() as { port: number }; try { - writeFileSync(this.portFile!, String(addr.port), 'utf-8'); + writeFileSync(this.portFile!, encodePortFile(port, this.token), { + mode: PORT_FILE_MODE, + }); } catch (err) { reject(err); return; @@ -51,9 +62,15 @@ export class RegistryServer { } private handleRequest(req: IncomingMessage, res: ServerResponse): void { + if (req.headers[TOKEN_HEADER] !== this.token) { + res.writeHead(401); + res.end(); + return; + } + const url = new URL(req.url ?? '/', 'http://localhost'); - if (url.pathname !== '/document') { + if (url.pathname !== DOCUMENT_ROUTE) { res.writeHead(404); res.end(); return; @@ -77,6 +94,3 @@ export class RegistryServer { res.end(content); } } - -const workspaceHash = (root: string): string => - createHash('md5').update(root).digest('hex').slice(0, 8); From 683b438b87a4b10a8ed413b585dc11232aba8c45 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Mon, 20 Apr 2026 12:31:05 +0200 Subject: [PATCH 16/23] feat(vscode-extension): add sandbox attribute to iframe for allow png export downloads --- packages/mcp/src/renderer/bridge.server.ts | 2 +- packages/vscode-extension/src/webview/main.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mcp/src/renderer/bridge.server.ts b/packages/mcp/src/renderer/bridge.server.ts index 3c8b4a97..01c758af 100644 --- a/packages/mcp/src/renderer/bridge.server.ts +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -41,7 +41,7 @@ function buildBridgeHtml(): string { - +