diff --git a/.changeset/quick-meals-send.md b/.changeset/quick-meals-send.md new file mode 100644 index 00000000..c112993e --- /dev/null +++ b/.changeset/quick-meals-send.md @@ -0,0 +1,26 @@ +--- +'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`. Existing entries are refreshed on activation so users always end up pointing at the right MCP invocation. + +- The MCP server is no longer bundled inside the `.vsix`. In production the extension spawns it on demand via `npx -y @lemoncode/quickmock-mcp`, so users always run the latest published MCP without waiting for an extension release. In development it resolves the local workspace build. + +**`@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. 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/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..dd4c0975 --- /dev/null +++ b/apps/web/src/core/vscode/use-vscode-file-load.hook.ts @@ -0,0 +1,52 @@ +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/local-npm-registry/config/config.yaml b/local-npm-registry/config/config.yaml index 1baba65b..02a5f8a8 100644 --- a/local-npm-registry/config/config.yaml +++ b/local-npm-registry/config/config.yaml @@ -1,4 +1,9 @@ -storage: ./storage +storage: /verdaccio/storage + +auth: + htpasswd: + file: /verdaccio/storage/htpasswd + max_users: 1000 uplinks: npmjs: @@ -6,9 +11,9 @@ uplinks: packages: '@lemoncode/*': - access: $anonymous - publish: $anonymous + access: $all + publish: $authenticated '**': - access: $anonymous - publish: $anonymous + access: $all + publish: $authenticated proxy: npmjs diff --git a/package-lock.json b/package-lock.json index 27781100..e6d8ccb9 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", @@ -405,6 +406,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 +738,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 +1192,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", @@ -1295,10 +1275,18 @@ "@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 }, + "node_modules/@lemoncode/quickmock-registry-protocol": { + "resolved": "packages/registry-protocol", + "link": true + }, "node_modules/@lemoncode/tsdown-config": { "resolved": "tooling/tsdown", "link": true @@ -1387,6 +1375,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 +1828,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 +2532,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 +2686,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 +2710,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" @@ -2653,13 +2749,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", @@ -2667,6 +2756,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 +3025,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 +3050,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 +3180,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 +3414,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 +3465,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 +3477,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 +3524,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" @@ -3503,6 +3657,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 +3716,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 +3838,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 +3905,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 +3997,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 +4025,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 +4048,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 +4061,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", @@ -3879,6 +4175,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,37 +4230,110 @@ "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, + "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/colorette": { + "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", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", @@ -3979,6 +4370,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 +4399,45 @@ "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/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 +4500,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 +4604,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 +4628,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 +4661,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 +4772,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 +4809,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 +4832,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 +4872,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" } @@ -4424,7 +4921,6 @@ "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 +4930,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 +4946,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 +4977,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4518,11 +5013,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 +5062,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 +5081,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 +5106,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 +5157,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 +5250,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 +5328,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 +5359,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 +5406,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 +5482,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 +5542,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 +5573,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 +5597,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 +5606,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 +5634,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 +5758,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 +5787,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 +5815,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 +5823,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 +5906,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 +5943,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 +5982,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" @@ -5262,9 +6076,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 +6086,24 @@ "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-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -5358,6 +6188,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 +6237,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 +6324,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", @@ -5526,9 +6370,14 @@ "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 +6485,8 @@ "url": "https://github.com/sponsors/lavrton" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/leven": { "version": "3.1.0", @@ -6207,7 +7057,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 +7069,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 +7234,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 +7272,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 +7308,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 +7448,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 +7480,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 +7650,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", @@ -6847,6 +7790,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 +7813,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 +7845,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,7 +7876,6 @@ "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": { @@ -6948,6 +7908,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 +8058,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 +8122,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" @@ -7115,11 +8138,28 @@ "node": ">=6" } }, + "node_modules/puppeteer-core": { + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz", + "integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==", + "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" @@ -7173,11 +8213,35 @@ "resolved": "packages/vscode-extension", "link": true }, - "node_modules/raf-schd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", - "license": "MIT" + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "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", @@ -7214,6 +8278,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 +8291,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 +8319,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@types/react-reconciler": "^0.28.2", "its-fine": "^1.1.1", @@ -7399,11 +8466,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 +8545,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 +8677,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 +8755,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 +8872,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 +8880,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 +8972,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 +8981,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 +9000,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 +9016,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 +9034,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 +9166,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 +9285,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 +9301,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 +9383,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 +9580,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 +9619,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 +9747,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 +9846,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 +9854,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 +9924,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", @@ -8674,6 +9987,7 @@ "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8741,7 +10055,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 +10081,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 +10187,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 +10215,7 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -9353,6 +10686,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 +10733,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 +10830,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 +10893,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 +10915,7 @@ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -9581,6 +10926,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,30 +11006,86 @@ "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/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", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "1.29.0", + "@puppeteer/browsers": "2.13.0", + "puppeteer-core": "24.42.0", + "zod": "4.3.6" + }, + "bin": { + "quickmock-mcp": "dist/index.mjs" + }, "devDependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-protocol": "*", "@lemoncode/tsdown-config": "*", "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*" } }, + "packages/registry-protocol": { + "name": "@lemoncode/quickmock-registry-protocol", + "version": "0.0.0", + "devDependencies": { + "@lemoncode/typescript-config": "*" + } + }, "packages/vscode-extension": { "name": "quickmock", - "version": "0.0.1", + "version": "0.1.0", "license": "MIT", + "dependencies": { + "@lemoncode/quickmock-mcp": "*" + }, "devDependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-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.116.0" + "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/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/bridge-protocol/src/constant.ts b/packages/bridge-protocol/src/constant.ts new file mode 100644 index 00000000..85cdad50 --- /dev/null +++ b/packages/bridge-protocol/src/constant.ts @@ -0,0 +1,12 @@ +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; diff --git a/packages/bridge-protocol/src/index.ts b/packages/bridge-protocol/src/index.ts new file mode 100644 index 00000000..61dc567b --- /dev/null +++ b/packages/bridge-protocol/src/index.ts @@ -0,0 +1,2 @@ +export * from './constant'; +export * from './model'; diff --git a/packages/bridge-protocol/src/model.ts b/packages/bridge-protocol/src/model.ts new file mode 100644 index 00000000..055466e7 --- /dev/null +++ b/packages/bridge-protocol/src/model.ts @@ -0,0 +1,33 @@ +import type { APP_MESSAGE_TYPE, HOST_MESSAGE_TYPE } from './constant'; + +export interface ContentBbox { + x: number; + y: number; + width: number; + height: number; +} + +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.WEBVIEW_READY } + | { type: typeof APP_MESSAGE_TYPE.SAVE; payload: { content: string } } + | { + type: typeof APP_MESSAGE_TYPE.RENDER_COMPLETE; + payload?: ContentBbox; + }; + +export type PayloadOf = + 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..77ce3f8a --- /dev/null +++ b/packages/bridge-protocol/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@lemoncode/typescript-config/node", + "include": ["src"] +} diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index e7be37d0..e1af6e22 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,24 @@ # @lemoncode/quickmock-mcp +## 0.1.0 + +### Minor Changes + +- Bump 0.1.0 +- 2282316: 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`. Existing entries are refreshed on activation so users always end up pointing at the right MCP invocation. + - The MCP server is no longer bundled inside the `.vsix`. In production the extension spawns it on demand via `npx -y @lemoncode/quickmock-mcp`, so users always run the latest published MCP without waiting for an extension release. In development it resolves the local workspace build. + + **`@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. + ## 0.0.1 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index dff5fdff..c08a3f48 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/*" @@ -14,16 +15,28 @@ "files": [ "dist" ], + "bin": { + "quickmock-mcp": "./dist/index.mjs" + }, "scripts": { "build": "tsdown", "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.29.0", + "@puppeteer/browsers": "2.13.0", + "puppeteer-core": "24.42.0", + "zod": "4.3.6" }, "devDependencies": { - "@lemoncode/typescript-config": "*", + "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-protocol": "*", "@lemoncode/tsdown-config": "*", + "@lemoncode/typescript-config": "*", "@lemoncode/vitest-config": "*" }, "repository": { 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..8019f36d --- /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..9a6b0aa3 --- /dev/null +++ b/packages/mcp/src/commons/qm-file.utils.ts @@ -0,0 +1,31 @@ +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..88e89218 --- /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..0531e134 --- /dev/null +++ b/packages/mcp/src/commons/wireframe-file.service.ts @@ -0,0 +1,15 @@ +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..b1f3231f --- /dev/null +++ b/packages/mcp/src/core/index.ts @@ -0,0 +1,2 @@ +export * from './registry.client'; +export * from './registry.models'; diff --git a/packages/mcp/src/core/mcp.logger.ts b/packages/mcp/src/core/mcp.logger.ts new file mode 100644 index 00000000..7ba896e3 --- /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.info(`${PREFIX} ${message}`, ...rest); +}; + +export const logError = (message: string, ...rest: unknown[]): void => { + console.error(`${PREFIX} ${message}`, ...rest); +}; diff --git a/packages/mcp/src/core/registry.client.ts b/packages/mcp/src/core/registry.client.ts new file mode 100644 index 00000000..c78eb9f7 --- /dev/null +++ b/packages/mcp/src/core/registry.client.ts @@ -0,0 +1,44 @@ +import { readFileSync } from 'node:fs'; +import { + buildPortFilePath, + DOCUMENT_ROUTE, + LOOPBACK_HOST, + parsePortFile, + TOKEN_HEADER, +} from '@lemoncode/quickmock-registry-protocol'; +import { nullClient, type RegistryClient } from './registry.models'; + +const REQUEST_TIMEOUT_MS = 2_000; + +function readPortFile(workspaceRoot: string) { + try { + const raw = readFileSync(buildPortFilePath(workspaceRoot), 'utf-8'); + return parsePortFile(raw); + } catch { + 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://${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.models.ts b/packages/mcp/src/core/registry.models.ts new file mode 100644 index 00000000..61b6e4e9 --- /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/index.ts b/packages/mcp/src/index.ts index cb0ff5c3..d7c665ea 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1 +1,70 @@ -export {}; +// The SDK's `exports` wildcard maps `./*` → `./dist/esm/*` without `.js`, +// so Node ESM cannot resolve subpath imports at runtime. +// See https://github.com/modelcontextprotocol/typescript-sdk/issues/440 +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +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) +); + +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 => { + 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..cb66ad5c --- /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 new file mode 100644 index 00000000..01c758af --- /dev/null +++ b/packages/mcp/src/renderer/bridge.server.ts @@ -0,0 +1,75 @@ +import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol'; +import { createServer, type Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { QM_APP_ORIGIN, QUICKMOCK_URL } from './app-url.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 { + 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 */ ` + + + + + + + + + +`; +} diff --git a/packages/mcp/src/renderer/chromium.resolver.ts b/packages/mcp/src/renderer/chromium.resolver.ts new file mode 100644 index 00000000..900d9b47 --- /dev/null +++ b/packages/mcp/src/renderer/chromium.resolver.ts @@ -0,0 +1,76 @@ +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 new file mode 100644 index 00000000..733b7c8e --- /dev/null +++ b/packages/mcp/src/renderer/headless.renderer.ts @@ -0,0 +1,53 @@ +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'; + +/** 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 executablePath = await resolveChromiumExecutable(); + const browser = await puppeteer.launch({ + headless: true, + executablePath, + }); + 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..74363deb --- /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..f732c9d3 --- /dev/null +++ b/packages/mcp/src/renderer/page.session.ts @@ -0,0 +1,119 @@ +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'; + +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, origin }) => { + const iframe = document.querySelector('iframe') as HTMLIFrameElement; + iframe.contentWindow?.postMessage( + { type: 'LOAD_FILE', payload: { data: JSON.parse(content), fileName } }, + origin + ); + }, + { content, fileName, origin: QM_APP_ORIGIN } + ); +} + +/** 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..21a4ccb9 --- /dev/null +++ b/packages/mcp/src/renderer/renderer.consts.ts @@ -0,0 +1,5 @@ +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.'; 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..4e859003 --- /dev/null +++ b/packages/mcp/src/tools/capture-wireframe/capture-wireframe.handler.ts @@ -0,0 +1,43 @@ +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 }, + 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} 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 new file mode 100644 index 00000000..bfc974ce --- /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..7e90be0e --- /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..4a90e035 --- /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..4185dfbc --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-assets/get-wireframe-assets.handler.ts @@ -0,0 +1,118 @@ +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; +} + +function parseDataUrl(src: string): ParsedDataUrl | null { + const match = src.match(/^data:([^;]+);base64,(.+)$/); + if (!match?.[1] || !match[2]) { + 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..0fc06689 --- /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..cf1d203d --- /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..a8a372fd --- /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..0d67ef00 --- /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..257c75ab --- /dev/null +++ b/packages/mcp/src/tools/get-wireframe-json/get-wireframe-json.handler.ts @@ -0,0 +1,16 @@ +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..5f3dec93 --- /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..ba237ba2 --- /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..7e2ea9a1 --- /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..ce5a473b --- /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..b52537f7 --- /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..ef59c58e --- /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..970a8163 --- /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..9de8913e --- /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..5467112e --- /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..15323d30 --- /dev/null +++ b/packages/mcp/src/tools/list-wireframes/list-wireframes.handler.ts @@ -0,0 +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', +]); + +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..9b1473d1 --- /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/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/*"] } diff --git a/packages/mcp/tsdown.config.ts b/packages/mcp/tsdown.config.ts index 33d5ceda..0ecb8f03 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'], + // Add a shebang so the built file can run as an npm bin (npx @lemoncode/quickmock-mcp). + outputOptions: { + banner: '#!/usr/bin/env node', + }, }; diff --git a/packages/registry-protocol/package.json b/packages/registry-protocol/package.json new file mode 100644 index 00000000..59f317b9 --- /dev/null +++ b/packages/registry-protocol/package.json @@ -0,0 +1,15 @@ +{ + "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": "*" + } +} diff --git a/packages/registry-protocol/src/constant.ts b/packages/registry-protocol/src/constant.ts new file mode 100644 index 00000000..90bb6ecb --- /dev/null +++ b/packages/registry-protocol/src/constant.ts @@ -0,0 +1,5 @@ +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 = ' '; +export const WORKSPACE_HASH_LENGTH = 8; diff --git a/packages/registry-protocol/src/index.ts b/packages/registry-protocol/src/index.ts new file mode 100644 index 00000000..a99efb16 --- /dev/null +++ b/packages/registry-protocol/src/index.ts @@ -0,0 +1,2 @@ +export * from './constant'; +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..01ff618d --- /dev/null +++ b/packages/registry-protocol/src/utils.ts @@ -0,0 +1,25 @@ +import { createHash } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { PORT_TOKEN_SEPARATOR, WORKSPACE_HASH_LENGTH } from './constant'; + +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 = Number.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..77ce3f8a --- /dev/null +++ b/packages/registry-protocol/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@lemoncode/typescript-config/node", + "include": ["src"] +} 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/CHANGELOG.md b/packages/vscode-extension/CHANGELOG.md index 66de67b2..418af359 100644 --- a/packages/vscode-extension/CHANGELOG.md +++ b/packages/vscode-extension/CHANGELOG.md @@ -1,5 +1,30 @@ # @lemoncode/quickmock-vscode-extension +## 0.1.0 + +### Minor Changes + +- Bump 0.1.0 +- 2282316: 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`. Existing entries are refreshed on activation so users always end up pointing at the right MCP invocation. + - The MCP server is no longer bundled inside the `.vsix`. In production the extension spawns it on demand via `npx -y @lemoncode/quickmock-mcp`, so users always run the latest published MCP without waiting for an extension release. In development it resolves the local workspace build. + + **`@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. + +### Patch Changes + +- Updated dependencies +- Updated dependencies [2282316] + - @lemoncode/quickmock-mcp@0.1.0 + ## 0.0.1 ### Patch Changes diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 63b796f1..79a738b5 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -2,7 +2,8 @@ "name": "quickmock", "version": "0.0.1", "type": "module", - "main": "./dist/index.mjs", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", "imports": { "#*": "./src/*" }, @@ -14,11 +15,16 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage" }, + "dependencies": { + "@lemoncode/quickmock-mcp": "*" + }, "devDependencies": { + "@lemoncode/quickmock-bridge-protocol": "*", + "@lemoncode/quickmock-registry-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", @@ -47,7 +53,7 @@ "directory": "packages/vscode-extension" }, "engines": { - "vscode": "^1.116.0" + "vscode": "^1.115.0" }, "icon": "./assets/app-icon.webp", "galleryBanner": { @@ -55,13 +61,44 @@ "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.newWireframe", + "title": "QuickMock: New Wireframe" + } + ], + "mcpServerDefinitionProviders": [ + { + "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/src/core/config.ts b/packages/vscode-extension/src/core/config.ts new file mode 100644 index 00000000..5eb6ff16 --- /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/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/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..6f2fcb72 --- /dev/null +++ b/packages/vscode-extension/src/core/paths.ts @@ -0,0 +1,5 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +export const QUICKMOCK_HOME = join(homedir(), '.quickmock'); +export const APP_URL_FILE = join(QUICKMOCK_HOME, 'app-url'); 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..e6795936 --- /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 '@lemoncode/quickmock-bridge-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..f943115f --- /dev/null +++ b/packages/vscode-extension/src/editor/panel.ts @@ -0,0 +1,33 @@ +import * as vscode from 'vscode'; + +const escapeAttr = (value: string): string => + value.replace(/&/g, '&').replace(/"/g, '"'); + +export const getHtml = ( + webview: vscode.Webview, + 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'); + + 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..a9b9fa4e --- /dev/null +++ b/packages/vscode-extension/src/editor/provider.ts @@ -0,0 +1,141 @@ +import { basename } from 'node:path'; +import * as vscode from 'vscode'; +import { getEditorAppUrl, onAppUrlChange } from '#core/config'; +import { documentRegistry } from '#core/document-registry'; +import { + type AppMessage, + HOST_MESSAGE_TYPE, + type HostMessage, +} from '@lemoncode/quickmock-bridge-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 { + const provider = new QuickMockEditorProvider(context.extensionUri); + const editorRegistration = vscode.window.registerCustomEditorProvider( + 'quickmock.editor', + provider, + { + supportsMultipleEditorsPerDocument: false, + webviewOptions: { retainContextWhenHidden: true }, + } + ); + const configListener = onAppUrlChange(() => provider.refreshAllPanels()); + return vscode.Disposable.from(editorRegistration, configListener); + } + + 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, + getEditorAppUrl() + ); + + 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); + } + } + + 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 c0a7ab1d..0916497f 100644 --- a/packages/vscode-extension/src/index.ts +++ b/packages/vscode-extension/src/index.ts @@ -1,14 +1,33 @@ +import { onAppUrlChange, syncAppUrlFile } from '#core/config'; +import { logError } from '#core/logger'; +import { QuickMockEditorProvider } from '#editor/provider'; +import { registerMcpServer } from '#mcp/mcp-registration'; +import { RegistryServer } from '#mcp/registry-server'; +import { registerQuickMockMcpServerProvider } from '#mcp/server-definition-provider'; import * as vscode from 'vscode'; export const activate = (context: vscode.ExtensionContext) => { - const disposable = vscode.commands.registerCommand( - 'quickmock.helloWorld', - () => { - vscode.window.showInformationMessage('Quickmock extension is running!'); - } + syncAppUrlFile(); + context.subscriptions.push(onAppUrlChange(syncAppUrlFile)); + + context.subscriptions.push(QuickMockEditorProvider.register(context)); + + const registryServer = new RegistryServer(); + registryServer + .start(context) + .catch(err => logError('Failed to start MCP registry server:', err)); + + context.subscriptions.push(registerQuickMockMcpServerProvider(context)); + + registerMcpServer(context).catch(err => + logError('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-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-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-invocation.ts b/packages/vscode-extension/src/mcp/mcp-invocation.ts new file mode 100644 index 00000000..c5b07a5d --- /dev/null +++ b/packages/vscode-extension/src/mcp/mcp-invocation.ts @@ -0,0 +1,27 @@ +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import * as vscode from 'vscode'; + +export const MCP_SERVER_ID = 'quickmock'; + +const MCP_PKG = '@lemoncode/quickmock-mcp'; + +export interface McpInvocation { + command: string; + args: string[]; +} + +// Production: spawn the MCP from the published npm package via npx. +// Development: spawn the local workspace build so changes are picked up on rebuild. +export const getMcpInvocation = ( + context: vscode.ExtensionContext +): McpInvocation => + context.extensionMode === vscode.ExtensionMode.Production + ? { command: 'npx', args: ['-y', MCP_PKG] } + : { command: 'node', args: [resolveLocalMcpEntry()] }; + +const resolveLocalMcpEntry = (): string => { + const require = createRequire(import.meta.url); + const pkgJsonPath = require.resolve(`${MCP_PKG}/package.json`); + return join(dirname(pkgJsonPath), 'dist', 'index.mjs'); +}; 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..79e4ad58 --- /dev/null +++ b/packages/vscode-extension/src/mcp/mcp-registration.ts @@ -0,0 +1,100 @@ +import { existsSync } from 'node:fs'; +import { dirname } from 'node:path'; +import * as vscode from 'vscode'; +import { logError, logInfo } from '#core/logger'; +import { getMcpInvocation, MCP_SERVER_ID } from '#mcp/mcp-invocation'; +import type { McpInvocation } from '#mcp/mcp-invocation'; +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'; + +export interface RegistrationResult { + label: string; + status: RegistrationStatus; + detail?: string; +} + +interface McpServerEntry { + type: 'stdio'; + command: string; + args: string[]; +} + +const buildMcpServerEntry = ({ + command, + args, +}: McpInvocation): McpServerEntry => ({ + type: 'stdio', + command, + args, +}); + +const registerInVSCode = async ( + entry: McpServerEntry +): Promise => { + try { + 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: VSCODE_CLIENT_LABEL, + status: 'error', + detail: String(err), + }; + } +}; + +const registerInClientTarget = ( + target: McpClientTarget, + entry: McpServerEntry +): RegistrationResult => { + if (!existsSync(target.path) && !existsSync(dirname(target.path))) { + return { label: target.label, status: 'skipped', detail: 'Not installed' }; + } + + try { + 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 registerMcpServer = async ( + context: vscode.ExtensionContext +): Promise => { + const entry = buildMcpServerEntry(getMcpInvocation(context)); + + const results: RegistrationResult[] = [ + await registerInVSCode(entry), + ...getMcpClientTargets().map(t => registerInClientTarget(t, entry)), + ]; + + for (const r of results) { + if (r.status === 'registered') { + logInfo(`MCP registered — ${r.label}`); + } else if (r.status === 'error') { + logError(`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..55ac1214 --- /dev/null +++ b/packages/vscode-extension/src/mcp/registry-server.ts @@ -0,0 +1,96 @@ +import { randomBytes } from 'node:crypto'; +import { unlinkSync, writeFileSync } from 'node:fs'; +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from 'node:http'; +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; + if (!workspaceRoot) { + return; + } + + 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, LOOPBACK_HOST, () => { + const { port } = server.address() as { port: number }; + try { + writeFileSync(this.portFile!, encodePortFile(port, this.token), { + mode: PORT_FILE_MODE, + }); + } 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 { + 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_ROUTE) { + 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); + } +} 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..0dfcff0a --- /dev/null +++ b/packages/vscode-extension/src/mcp/server-definition-provider.ts @@ -0,0 +1,91 @@ +import { createHash } from 'node:crypto'; +import * as vscode from 'vscode'; +import { getHeadlessAppUrl, onAppUrlChange } from '#core/config'; +import { logInfo } from '#core/logger'; +import { getMcpInvocation, MCP_SERVER_ID } from '#mcp/mcp-invocation'; +import { version as EXTENSION_VERSION } from '../../package.json'; + +const SERVER_LABEL = 'QuickMock Wireframe Tools'; +const VERSION_HASH_ALGO = 'sha1'; +const VERSION_HASH_LENGTH = 8; + +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); + + const { command, args } = getMcpInvocation(context); + + return new vscode.McpStdioServerDefinition( + SERVER_LABEL, + command, + args, + { QM_WORKSPACE_ROOT: workspaceFolder.uri.fsPath }, + `${EXTENSION_VERSION}+${versionSuffix}` + ); +}; + +export const registerQuickMockMcpServerProvider = ( + context: vscode.ExtensionContext +): vscode.Disposable => { + const didChangeDefinitions = new vscode.EventEmitter(); + 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(); + + const subscriptions: vscode.Disposable[] = [ + didChangeDefinitions, + vscode.workspace.onDidChangeWorkspaceFolders(() => + didChangeDefinitions.fire() + ), + onAppUrlChange(() => { + logInfo('appUrl changed, re-registering MCP provider'); + register(); + didChangeDefinitions.fire(); + }), + { dispose: () => providerRegistration?.dispose() }, + ]; + + return vscode.Disposable.from(...subscriptions); +}; diff --git a/packages/vscode-extension/src/webview/bridge.ts b/packages/vscode-extension/src/webview/bridge.ts new file mode 100644 index 00000000..513d71f6 --- /dev/null +++ b/packages/vscode-extension/src/webview/bridge.ts @@ -0,0 +1,32 @@ +import { + type AppMessage, + HOST_MESSAGE_TYPE, + type HostMessage, +} from '@lemoncode/quickmock-bridge-protocol'; + +// Reference: https://code.visualstudio.com/api/extension-guides/webview#loading-local-content +declare function acquireVsCodeApi(): { postMessage(msg: AppMessage): void }; + +const vscode = acquireVsCodeApi(); + +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, + appOrigin: string +): void => { + window.addEventListener('message', (event: MessageEvent) => { + 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, appOrigin); + } + } + }); +}; diff --git a/packages/vscode-extension/src/webview/main.ts b/packages/vscode-extension/src/webview/main.ts new file mode 100644 index 00000000..63e4057d --- /dev/null +++ b/packages/vscode-extension/src/webview/main.ts @@ -0,0 +1,20 @@ +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 = appUrl; +iframe.setAttribute( + 'sandbox', + 'allow-scripts allow-same-origin allow-downloads' +); +iframe.allow = 'clipboard-read; clipboard-write'; +iframe.title = 'QuickMock Application'; +document.body.appendChild(iframe); + +setupBridge(iframe, appOrigin); diff --git a/packages/vscode-extension/tsconfig.json b/packages/vscode-extension/tsconfig.json index 38115089..e9acf234 100644 --- a/packages/vscode-extension/tsconfig.json +++ b/packages/vscode-extension/tsconfig.json @@ -3,6 +3,7 @@ "include": ["src"], "compilerOptions": { "rootDir": "src", + "lib": ["ES2024", "DOM"], "paths": { "#*": ["./src/*"] } diff --git a/packages/vscode-extension/tsdown.config.ts b/packages/vscode-extension/tsdown.config.ts index 6462135d..a2514af7 100644 --- a/packages/vscode-extension/tsdown.config.ts +++ b/packages/vscode-extension/tsdown.config.ts @@ -1,7 +1,24 @@ 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'], + format: ['esm', 'cjs'], + dts: false, + deps: { + neverBundle: ['vscode'], + // alwaysBundle: ['@lemoncode/quickmock-mcp'], + }, + }, + { + ...baseTsdownConfig, + entry: { webview: 'src/webview/main.ts' }, + format: 'iife', + platform: 'browser', + outputOptions: { + entryFileNames: '[name].js', + }, + }, +]); 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 } }