From dcf1bea20b07a9ae5956ecba9547d073c432be85 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 30 Mar 2026 01:18:22 -0300 Subject: [PATCH] feat: scaffold VS Code extension (Phase 1 MVP) Adds the Velxio VS Code extension skeleton with: - Extension host: commands (Open/Compile/Run/Stop/Select Board), WebView panel with postMessage bridge, FastAPI backend manager, velxio.toml + diagram.json config parser, file watcher, serial pseudo-terminal - WebView: placeholder React app with board display and serial monitor, ready for wiring to the real simulation engines - Build system: esbuild for extension (94KB), Vite for webview (198KB) - Wokwi-compatible diagram.json schema for future import support Based on reverse-engineering of the Wokwi VS Code extension (v3.5.0). Key differentiator: Velxio runs simulation 100% locally (avr8js/rp2040js in WebView, QEMU for ESP32) vs Wokwi's cloud iframe approach. Refs #4 Co-Authored-By: Claude Opus 4.6 --- vscode-extension/package-lock.json | 499 +++++ vscode-extension/package.json | 140 ++ vscode-extension/schemas/diagram.schema.json | 59 + vscode-extension/src/BackendManager.ts | 170 ++ vscode-extension/src/FileWatcher.ts | 50 + vscode-extension/src/ProjectConfig.ts | 91 + vscode-extension/src/SerialTerminal.ts | 77 + vscode-extension/src/SimulatorPanel.ts | 198 ++ vscode-extension/src/extension.ts | 285 +++ vscode-extension/src/types.ts | 83 + vscode-extension/tsconfig.json | 19 + vscode-extension/webview/index.html | 12 + vscode-extension/webview/package-lock.json | 1820 ++++++++++++++++++ vscode-extension/webview/package.json | 20 + vscode-extension/webview/src/bridge.ts | 61 + vscode-extension/webview/src/webviewApp.tsx | 259 +++ vscode-extension/webview/tsconfig.json | 14 + vscode-extension/webview/vite.config.ts | 23 + 18 files changed, 3880 insertions(+) create mode 100644 vscode-extension/package-lock.json create mode 100644 vscode-extension/package.json create mode 100644 vscode-extension/schemas/diagram.schema.json create mode 100644 vscode-extension/src/BackendManager.ts create mode 100644 vscode-extension/src/FileWatcher.ts create mode 100644 vscode-extension/src/ProjectConfig.ts create mode 100644 vscode-extension/src/SerialTerminal.ts create mode 100644 vscode-extension/src/SimulatorPanel.ts create mode 100644 vscode-extension/src/extension.ts create mode 100644 vscode-extension/src/types.ts create mode 100644 vscode-extension/tsconfig.json create mode 100644 vscode-extension/webview/index.html create mode 100644 vscode-extension/webview/package-lock.json create mode 100644 vscode-extension/webview/package.json create mode 100644 vscode-extension/webview/src/bridge.ts create mode 100644 vscode-extension/webview/src/webviewApp.tsx create mode 100644 vscode-extension/webview/tsconfig.json create mode 100644 vscode-extension/webview/vite.config.ts diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json new file mode 100644 index 0000000..90322e1 --- /dev/null +++ b/vscode-extension/package-lock.json @@ -0,0 +1,499 @@ +{ + "name": "velxio-simulator", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "velxio-simulator", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "license": "ISC" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 0000000..8264e89 --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,140 @@ +{ + "name": "velxio-simulator", + "displayName": "Velxio Simulator", + "description": "Local Arduino & ESP32 simulator for VS Code — compile, simulate, and debug embedded projects without leaving your editor", + "version": "0.1.0", + "publisher": "velxio", + "license": "MIT", + "icon": "media/icon.png", + "repository": { + "type": "git", + "url": "https://github.com/davidmonterocrespo24/velxio" + }, + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other", + "Testing", + "Education" + ], + "keywords": [ + "arduino", + "esp32", + "simulator", + "emulator", + "embedded", + "micropython", + "raspberry-pi-pico", + "wokwi", + "electronics" + ], + "activationEvents": [ + "workspaceContains:velxio.toml", + "workspaceContains:diagram.json" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "velxio.openSimulator", + "title": "Open Simulator", + "category": "Velxio", + "icon": "$(circuit-board)" + }, + { + "command": "velxio.compile", + "title": "Compile Sketch", + "category": "Velxio", + "icon": "$(tools)" + }, + { + "command": "velxio.run", + "title": "Run Simulation", + "category": "Velxio", + "icon": "$(play)" + }, + { + "command": "velxio.stop", + "title": "Stop Simulation", + "category": "Velxio", + "icon": "$(debug-stop)" + }, + { + "command": "velxio.selectBoard", + "title": "Select Board", + "category": "Velxio", + "icon": "$(list-selection)" + } + ], + "menus": { + "editor/title": [ + { + "command": "velxio.run", + "when": "resourceExtname == .ino || resourceExtname == .py", + "group": "navigation" + } + ] + }, + "configuration": { + "title": "Velxio Simulator", + "properties": { + "velxio.defaultBoard": { + "type": "string", + "default": "arduino-uno", + "enum": [ + "arduino-uno", + "arduino-nano", + "arduino-mega", + "raspberry-pi-pico", + "pi-pico-w", + "esp32", + "esp32-s3", + "esp32-c3", + "attiny85" + ], + "description": "Default board type for new projects" + }, + "velxio.autoStartBackend": { + "type": "boolean", + "default": true, + "description": "Automatically start the compilation backend when needed" + }, + "velxio.backendPort": { + "type": "number", + "default": 0, + "description": "Fixed port for the backend (0 = auto-assign)" + }, + "velxio.arduinoCliPath": { + "type": "string", + "default": "", + "description": "Path to arduino-cli executable (leave empty to auto-detect)" + } + } + }, + "jsonValidation": [ + { + "fileMatch": "diagram.json", + "url": "./schemas/diagram.schema.json" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run build", + "build": "npm run build:extension && npm run build:webview", + "build:extension": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --target=node18 --sourcemap", + "build:webview": "cd webview && npm run build", + "watch": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --target=node18 --sourcemap --watch", + "lint": "eslint src/", + "package": "vsce package" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "esbuild": "^0.20.0", + "typescript": "^5.3.0" + }, + "dependencies": { + "@iarna/toml": "^2.2.5" + } +} diff --git a/vscode-extension/schemas/diagram.schema.json b/vscode-extension/schemas/diagram.schema.json new file mode 100644 index 0000000..26de915 --- /dev/null +++ b/vscode-extension/schemas/diagram.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Velxio Diagram", + "description": "Circuit diagram layout (Wokwi-compatible format)", + "type": "object", + "required": ["version", "parts", "connections"], + "properties": { + "version": { + "type": "integer", + "const": 1 + }, + "author": { "type": "string" }, + "editor": { "type": "string" }, + "parts": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "type", "left", "top"], + "properties": { + "id": { "type": "string" }, + "type": { "type": "string" }, + "left": { "type": "number" }, + "top": { "type": "number" }, + "rotate": { "type": "number" }, + "hide": { "type": "boolean" }, + "attrs": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + }, + "connections": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 4, + "items": [ + { "type": "string", "description": "Source pin (componentId:pinName)" }, + { "type": "string", "description": "Target pin (componentId:pinName)" }, + { "type": "string", "description": "Wire color" }, + { + "type": "array", + "items": { "type": "string" }, + "description": "Routing hints" + } + ] + } + }, + "serialMonitor": { + "type": "object", + "properties": { + "display": { "type": "string", "enum": ["terminal", "plotter"] }, + "newline": { "type": "string", "enum": ["lf", "cr", "crlf"] } + } + } + } +} diff --git a/vscode-extension/src/BackendManager.ts b/vscode-extension/src/BackendManager.ts new file mode 100644 index 0000000..6ed7623 --- /dev/null +++ b/vscode-extension/src/BackendManager.ts @@ -0,0 +1,170 @@ +/** + * BackendManager — Spawns and manages the Velxio FastAPI backend process. + * + * The backend provides: + * - /api/compile — Arduino sketch compilation via arduino-cli + * - /api/simulation/ws — WebSocket bridge to QEMU for ESP32 simulation + * + * AVR/RP2040 boards don't need the backend (simulation runs in the WebView). + */ + +import * as vscode from 'vscode'; +import { ChildProcess, spawn } from 'child_process'; +import * as net from 'net'; +import * as path from 'path'; +import * as fs from 'fs'; + +export class BackendManager { + private process: ChildProcess | null = null; + private _port = 0; + private _ready = false; + private outputChannel: vscode.OutputChannel; + + constructor(outputChannel: vscode.OutputChannel) { + this.outputChannel = outputChannel; + } + + get port(): number { return this._port; } + get ready(): boolean { return this._ready; } + get apiBase(): string { return `http://localhost:${this._port}/api`; } + + /** Start the backend on a free port. Returns the port number. */ + async start(): Promise { + if (this.process && this._ready) return this._port; + + this._port = await this.findFreePort(); + const configPort = vscode.workspace.getConfiguration('velxio').get('backendPort'); + if (configPort && configPort > 0) { + this._port = configPort; + } + + // Find the backend directory + const backendDir = await this.findBackendDir(); + if (!backendDir) { + throw new Error('Velxio backend not found. Please install the Velxio backend or set the path in settings.'); + } + + this.outputChannel.appendLine(`[Backend] Starting on port ${this._port}...`); + this.outputChannel.appendLine(`[Backend] Directory: ${backendDir}`); + + // Spawn uvicorn + const python = this.findPython(backendDir); + this.process = spawn(python, [ + '-m', 'uvicorn', + 'app.main:app', + '--port', String(this._port), + '--host', '127.0.0.1', + ], { + cwd: backendDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, PYTHONUNBUFFERED: '1' }, + }); + + this.process.stdout?.on('data', (data) => { + this.outputChannel.appendLine(`[Backend] ${data.toString().trim()}`); + }); + + this.process.stderr?.on('data', (data) => { + this.outputChannel.appendLine(`[Backend] ${data.toString().trim()}`); + }); + + this.process.on('exit', (code) => { + this.outputChannel.appendLine(`[Backend] Process exited (code=${code})`); + this._ready = false; + this.process = null; + }); + + // Wait for the server to be ready + await this.waitForReady(); + this._ready = true; + this.outputChannel.appendLine(`[Backend] Ready at ${this.apiBase}`); + return this._port; + } + + /** Stop the backend process */ + async stop(): Promise { + if (!this.process) return; + + this.outputChannel.appendLine('[Backend] Stopping...'); + this.process.kill('SIGTERM'); + + // Give it 3 seconds to shut down gracefully + await new Promise((resolve) => { + const timeout = setTimeout(() => { + this.process?.kill('SIGKILL'); + resolve(); + }, 3000); + + this.process?.on('exit', () => { + clearTimeout(timeout); + resolve(); + }); + }); + + this.process = null; + this._ready = false; + } + + /** Find the backend directory (relative to extension or workspace) */ + private async findBackendDir(): Promise { + // 1. Check relative to the extension (monorepo layout) + const extensionDir = path.resolve(__dirname, '..'); + const monorepoBackend = path.resolve(extensionDir, '..', 'backend'); + if (fs.existsSync(path.join(monorepoBackend, 'app', 'main.py'))) { + return monorepoBackend; + } + + // 2. Check workspace folders + for (const folder of vscode.workspace.workspaceFolders ?? []) { + const wsBackend = path.join(folder.uri.fsPath, 'backend'); + if (fs.existsSync(path.join(wsBackend, 'app', 'main.py'))) { + return wsBackend; + } + } + + return null; + } + + /** Find the correct Python executable (venv or system) */ + private findPython(backendDir: string): string { + // Check for venv + const venvPaths = [ + path.join(backendDir, 'venv', 'Scripts', 'python.exe'), // Windows + path.join(backendDir, 'venv', 'bin', 'python'), // Unix + path.join(backendDir, '.venv', 'Scripts', 'python.exe'), + path.join(backendDir, '.venv', 'bin', 'python'), + ]; + + for (const p of venvPaths) { + if (fs.existsSync(p)) return p; + } + + return 'python'; + } + + /** Find a free TCP port */ + private findFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close(() => resolve(addr.port)); + }); + srv.on('error', reject); + }); + } + + /** Wait for the backend health endpoint to respond */ + private async waitForReady(maxRetries = 30): Promise { + for (let i = 0; i < maxRetries; i++) { + try { + const response = await fetch(`http://127.0.0.1:${this._port}/docs`); + if (response.ok) return; + } catch { + // Not ready yet + } + await new Promise(r => setTimeout(r, 1000)); + } + throw new Error(`Backend failed to start after ${maxRetries} seconds`); + } +} diff --git a/vscode-extension/src/FileWatcher.ts b/vscode-extension/src/FileWatcher.ts new file mode 100644 index 0000000..ea8915c --- /dev/null +++ b/vscode-extension/src/FileWatcher.ts @@ -0,0 +1,50 @@ +/** + * FileWatcher — Watches sketch files for changes and triggers recompilation. + */ + +import * as vscode from 'vscode'; + +export class FileWatcher { + private watcher: vscode.FileSystemWatcher | null = null; + private debounceTimer: NodeJS.Timeout | null = null; + private _onChange = new vscode.EventEmitter(); + + /** Fired when a sketch file changes (debounced by 500ms) */ + public readonly onChange = this._onChange.event; + + /** Start watching sketch files */ + start(): void { + if (this.watcher) return; + + this.watcher = vscode.workspace.createFileSystemWatcher( + '**/*.{ino,h,cpp,c,py}', + false, // creations + false, // changes + false, // deletions + ); + + const debouncedFire = (uri: vscode.Uri) => { + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(() => this._onChange.fire(uri), 500); + }; + + this.watcher.onDidChange(debouncedFire); + this.watcher.onDidCreate(debouncedFire); + this.watcher.onDidDelete(debouncedFire); + } + + /** Stop watching */ + stop(): void { + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + this.debounceTimer = null; + } + this.watcher?.dispose(); + this.watcher = null; + } + + dispose(): void { + this.stop(); + this._onChange.dispose(); + } +} diff --git a/vscode-extension/src/ProjectConfig.ts b/vscode-extension/src/ProjectConfig.ts new file mode 100644 index 0000000..d114df2 --- /dev/null +++ b/vscode-extension/src/ProjectConfig.ts @@ -0,0 +1,91 @@ +/** + * ProjectConfig — Reads velxio.toml and diagram.json from the workspace. + */ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import TOML from '@iarna/toml'; +import type { VelxioConfig, DiagramJson, BoardKind } from './types'; + +export class ProjectConfig { + private workspaceRoot: string; + + constructor(workspaceRoot: string) { + this.workspaceRoot = workspaceRoot; + } + + /** Read and parse velxio.toml from the workspace root */ + readVelxioToml(): VelxioConfig | null { + const tomlPath = path.join(this.workspaceRoot, 'velxio.toml'); + if (!fs.existsSync(tomlPath)) return null; + + try { + const content = fs.readFileSync(tomlPath, 'utf-8'); + const parsed = TOML.parse(content) as unknown as VelxioConfig; + return parsed; + } catch (err) { + vscode.window.showWarningMessage(`Failed to parse velxio.toml: ${err}`); + return null; + } + } + + /** Read and parse diagram.json from the workspace root */ + readDiagramJson(): DiagramJson | null { + const jsonPath = path.join(this.workspaceRoot, 'diagram.json'); + if (!fs.existsSync(jsonPath)) return null; + + try { + const content = fs.readFileSync(jsonPath, 'utf-8'); + return JSON.parse(content) as DiagramJson; + } catch (err) { + vscode.window.showWarningMessage(`Failed to parse diagram.json: ${err}`); + return null; + } + } + + /** Resolve the board kind from config or settings */ + getBoard(): BoardKind { + const config = this.readVelxioToml(); + if (config?.velxio?.board) { + return config.velxio.board as BoardKind; + } + return vscode.workspace.getConfiguration('velxio').get('defaultBoard') ?? 'arduino-uno'; + } + + /** Get the language mode (arduino or micropython) */ + getLanguageMode(): 'arduino' | 'micropython' { + const config = this.readVelxioToml(); + return config?.velxio?.language ?? 'arduino'; + } + + /** Get the pre-compiled firmware path (if specified) */ + getFirmwarePath(): string | null { + const config = this.readVelxioToml(); + if (!config?.velxio?.firmware) return null; + return path.resolve(this.workspaceRoot, config.velxio.firmware); + } + + /** Collect all sketch files (.ino, .h, .cpp, .py) from the workspace */ + async getSketchFiles(): Promise> { + const language = this.getLanguageMode(); + const pattern = language === 'micropython' ? '**/*.py' : '**/*.{ino,h,cpp,c}'; + const uris = await vscode.workspace.findFiles(pattern, '**/node_modules/**'); + const files: Array<{ name: string; content: string }> = []; + + for (const uri of uris) { + const relativePath = path.relative(this.workspaceRoot, uri.fsPath); + const content = fs.readFileSync(uri.fsPath, 'utf-8'); + files.push({ name: relativePath, content }); + } + + return files; + } + + /** Create a default velxio.toml in the workspace */ + async createDefaultConfig(board: BoardKind): Promise { + const tomlPath = path.join(this.workspaceRoot, 'velxio.toml'); + const content = `[velxio]\nversion = 1\nboard = "${board}"\n`; + fs.writeFileSync(tomlPath, content, 'utf-8'); + } +} diff --git a/vscode-extension/src/SerialTerminal.ts b/vscode-extension/src/SerialTerminal.ts new file mode 100644 index 0000000..e3feabd --- /dev/null +++ b/vscode-extension/src/SerialTerminal.ts @@ -0,0 +1,77 @@ +/** + * SerialTerminal — VS Code pseudo-terminal for simulation serial I/O. + * + * Provides a native VS Code terminal that displays serial output from the + * running simulation and sends user input back to the simulated UART. + */ + +import * as vscode from 'vscode'; + +export class SerialTerminal { + private terminal: vscode.Terminal | null = null; + private writeEmitter = new vscode.EventEmitter(); + private closeEmitter = new vscode.EventEmitter(); + private _onInput = new vscode.EventEmitter(); + + /** Fired when the user types in the terminal */ + public readonly onInput = this._onInput.event; + + /** Create and show the serial terminal */ + open(boardName: string): void { + if (this.terminal) { + this.terminal.show(); + return; + } + + const pty: vscode.Pseudoterminal = { + onDidWrite: this.writeEmitter.event, + onDidClose: this.closeEmitter.event, + open: () => { + this.writeEmitter.fire(`\x1b[36m--- Velxio Serial Monitor (${boardName}) ---\x1b[0m\r\n`); + }, + close: () => { + this.terminal = null; + }, + handleInput: (data: string) => { + this._onInput.fire(data); + // Echo input in a dim color + this.writeEmitter.fire(`\x1b[90m${data}\x1b[0m`); + }, + }; + + this.terminal = vscode.window.createTerminal({ + name: `Velxio Serial (${boardName})`, + pty, + iconPath: new vscode.ThemeIcon('circuit-board'), + }); + this.terminal.show(true); // preserve focus + } + + /** Write serial output to the terminal */ + write(text: string): void { + if (!this.terminal) return; + // Convert \n to \r\n for terminal display + this.writeEmitter.fire(text.replace(/\n/g, '\r\n')); + } + + /** Clear the terminal */ + clear(): void { + this.writeEmitter.fire('\x1b[2J\x1b[H'); // ANSI clear + home + } + + /** Close the terminal */ + close(): void { + if (this.terminal) { + this.closeEmitter.fire(); + this.terminal.dispose(); + this.terminal = null; + } + } + + dispose(): void { + this.close(); + this.writeEmitter.dispose(); + this.closeEmitter.dispose(); + this._onInput.dispose(); + } +} diff --git a/vscode-extension/src/SimulatorPanel.ts b/vscode-extension/src/SimulatorPanel.ts new file mode 100644 index 0000000..1227a2e --- /dev/null +++ b/vscode-extension/src/SimulatorPanel.ts @@ -0,0 +1,198 @@ +/** + * SimulatorPanel — Manages the VS Code WebView panel that hosts the simulation UI. + * + * The WebView runs a stripped-down version of the Velxio React frontend + * (SimulatorCanvas + SerialMonitor) with simulation engines (avr8js, rp2040js) + * running directly in the WebView's JavaScript context. + * + * Communication with the extension host uses VS Code's postMessage API. + */ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import type { ToWebviewMessage, FromWebviewMessage, BoardKind } from './types'; + +export class SimulatorPanel { + public static readonly viewType = 'velxio.simulator'; + private static instance: SimulatorPanel | undefined; + + private readonly panel: vscode.WebviewPanel; + private readonly extensionUri: vscode.Uri; + private disposables: vscode.Disposable[] = []; + private _ready = false; + private _onSerialOutput = new vscode.EventEmitter(); + private _onSimulationState = new vscode.EventEmitter(); + private _onReady = new vscode.EventEmitter(); + + /** Fired when serial data arrives from the simulation */ + public readonly onSerialOutput = this._onSerialOutput.event; + /** Fired when simulation starts/stops */ + public readonly onSimulationState = this._onSimulationState.event; + /** Fired when the WebView is ready */ + public readonly onReady = this._onReady.event; + + public get ready(): boolean { return this._ready; } + + /** Get or create the singleton panel */ + public static createOrShow(extensionUri: vscode.Uri): SimulatorPanel { + const column = vscode.ViewColumn.Beside; + + if (SimulatorPanel.instance) { + SimulatorPanel.instance.panel.reveal(column); + return SimulatorPanel.instance; + } + + const panel = vscode.window.createWebviewPanel( + SimulatorPanel.viewType, + 'Velxio Simulator', + column, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [ + vscode.Uri.joinPath(extensionUri, 'dist', 'webview'), + vscode.Uri.joinPath(extensionUri, 'media'), + ], + }, + ); + + SimulatorPanel.instance = new SimulatorPanel(panel, extensionUri); + return SimulatorPanel.instance; + } + + private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { + this.panel = panel; + this.extensionUri = extensionUri; + + this.panel.webview.html = this.getHtmlContent(); + + // Handle messages from the WebView + this.panel.webview.onDidReceiveMessage( + (msg: FromWebviewMessage) => this.handleMessage(msg), + null, + this.disposables, + ); + + // Cleanup on dispose + this.panel.onDidDispose(() => this.dispose(), null, this.disposables); + } + + /** Send a message to the WebView */ + public postMessage(message: ToWebviewMessage): void { + if (this._ready) { + this.panel.webview.postMessage(message); + } + } + + /** Send compiled hex to the WebView */ + public loadHex(hex: string, board: BoardKind): void { + this.postMessage({ type: 'loadHex', hex, board }); + } + + /** Send firmware binary (base64) for ESP32 */ + public loadBinary(firmwareBase64: string, board: BoardKind): void { + this.postMessage({ type: 'loadBinary', firmwareBase64, board }); + } + + /** Load MicroPython files */ + public loadMicroPython(files: Array<{ name: string; content: string }>, board: BoardKind): void { + this.postMessage({ type: 'loadMicroPython', files, board }); + } + + /** Start the simulation */ + public start(): void { + this.postMessage({ type: 'start' }); + } + + /** Stop the simulation */ + public stop(): void { + this.postMessage({ type: 'stop' }); + } + + /** Send serial input text */ + public serialInput(text: string): void { + this.postMessage({ type: 'serialInput', text }); + } + + /** Set the board type */ + public setBoard(board: BoardKind): void { + this.postMessage({ type: 'setBoard', board }); + } + + /** Set the backend API base URL */ + public setApiBase(apiBase: string): void { + this.postMessage({ type: 'setApiBase', apiBase }); + } + + private handleMessage(msg: FromWebviewMessage): void { + switch (msg.type) { + case 'ready': + this._ready = true; + this._onReady.fire(); + break; + case 'serialOutput': + this._onSerialOutput.fire(msg.text); + break; + case 'simulationState': + this._onSimulationState.fire(msg.running); + break; + case 'error': + vscode.window.showErrorMessage(`Velxio: ${msg.message}`); + break; + case 'log': + // Forward WebView logs to the output channel + break; + } + } + + private getHtmlContent(): string { + const webview = this.panel.webview; + const webviewDistUri = vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview'); + + // In development, point to local files; in production, use bundled assets + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(webviewDistUri, 'index.js')); + const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(webviewDistUri, 'index.css')); + + const nonce = getNonce(); + + return ` + + + + + + + Velxio Simulator + + +
+ + +`; + } + + private dispose(): void { + SimulatorPanel.instance = undefined; + this._onSerialOutput.dispose(); + this._onSimulationState.dispose(); + this._onReady.dispose(); + for (const d of this.disposables) d.dispose(); + this.disposables = []; + } +} + +function getNonce(): string { + let text = ''; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return text; +} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts new file mode 100644 index 0000000..ec11d42 --- /dev/null +++ b/vscode-extension/src/extension.ts @@ -0,0 +1,285 @@ +/** + * Velxio VS Code Extension — Entry point + * + * Provides commands to compile, simulate, and interact with Arduino/ESP32 + * sketches directly within VS Code. Simulation runs locally using avr8js, + * rp2040js (in the WebView), and QEMU (via the backend) for ESP32 boards. + */ + +import * as vscode from 'vscode'; +import { SimulatorPanel } from './SimulatorPanel'; +import { BackendManager } from './BackendManager'; +import { ProjectConfig } from './ProjectConfig'; +import { SerialTerminal } from './SerialTerminal'; +import { FileWatcher } from './FileWatcher'; +import { BOARD_LABELS, type BoardKind } from './types'; + +let backend: BackendManager; +let serialTerminal: SerialTerminal; +let fileWatcher: FileWatcher; +let outputChannel: vscode.OutputChannel; +let statusBarItem: vscode.StatusBarItem; + +export function activate(context: vscode.ExtensionContext) { + outputChannel = vscode.window.createOutputChannel('Velxio'); + backend = new BackendManager(outputChannel); + serialTerminal = new SerialTerminal(); + fileWatcher = new FileWatcher(); + + // Status bar item showing current board + statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); + statusBarItem.command = 'velxio.selectBoard'; + statusBarItem.tooltip = 'Click to change board'; + updateStatusBar('arduino-uno'); + + // ── Commands ────────────────────────────────────────────────────────────── + + context.subscriptions.push( + vscode.commands.registerCommand('velxio.openSimulator', () => { + const panel = SimulatorPanel.createOrShow(context.extensionUri); + setupPanelListeners(panel, context); + statusBarItem.show(); + }), + + vscode.commands.registerCommand('velxio.compile', async () => { + await compileAndLoad(context); + }), + + vscode.commands.registerCommand('velxio.run', async () => { + const panel = SimulatorPanel.createOrShow(context.extensionUri); + setupPanelListeners(panel, context); + + if (!panel.ready) { + // Wait for the WebView to initialize + await new Promise(resolve => { + const disposable = panel.onReady(() => { disposable.dispose(); resolve(); }); + }); + } + + await compileAndLoad(context); + panel.start(); + }), + + vscode.commands.registerCommand('velxio.stop', () => { + const panel = SimulatorPanel.createOrShow(context.extensionUri); + panel.stop(); + }), + + vscode.commands.registerCommand('velxio.selectBoard', async () => { + const boards = Object.entries(BOARD_LABELS) as [BoardKind, string][]; + const items = boards.map(([kind, label]) => ({ + label, + description: kind, + kind: kind, + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a board', + title: 'Velxio: Select Board', + }); + + if (selected) { + const boardKind = selected.description as BoardKind; + updateStatusBar(boardKind); + + // Update velxio.toml if it exists + const workspaceRoot = getWorkspaceRoot(); + if (workspaceRoot) { + const config = new ProjectConfig(workspaceRoot); + const existingConfig = config.readVelxioToml(); + if (existingConfig) { + await config.createDefaultConfig(boardKind); + } + } + + // Update the WebView + try { + const panel = SimulatorPanel.createOrShow(context.extensionUri); + panel.setBoard(boardKind); + } catch { + // Panel not open yet, that's fine + } + } + }), + ); + + // ── Auto-activation ─────────────────────────────────────────────────────── + + // If velxio.toml or diagram.json exists, show the status bar + const workspaceRoot = getWorkspaceRoot(); + if (workspaceRoot) { + const config = new ProjectConfig(workspaceRoot); + const velxioConfig = config.readVelxioToml(); + if (velxioConfig) { + updateStatusBar(config.getBoard()); + statusBarItem.show(); + } + } + + // ── Cleanup ─────────────────────────────────────────────────────────────── + + context.subscriptions.push( + outputChannel, + statusBarItem, + serialTerminal, + fileWatcher, + { dispose: () => { backend.stop(); } }, + ); + + outputChannel.appendLine('Velxio extension activated'); +} + +export function deactivate() { + backend.stop(); + fileWatcher.stop(); +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function getWorkspaceRoot(): string | null { + const folders = vscode.workspace.workspaceFolders; + return folders?.[0]?.uri.fsPath ?? null; +} + +function updateStatusBar(board: BoardKind): void { + const label = BOARD_LABELS[board] ?? board; + statusBarItem.text = `$(circuit-board) ${label}`; +} + +let panelListenersSet = false; + +function setupPanelListeners(panel: SimulatorPanel, context: vscode.ExtensionContext): void { + if (panelListenersSet) return; + panelListenersSet = true; + + // Wire serial output to the VS Code terminal + panel.onSerialOutput((text) => { + serialTerminal.write(text); + }); + + // Wire terminal input back to the simulation + serialTerminal.onInput((text) => { + panel.serialInput(text); + }); + + // When the panel is ready, send initial configuration + panel.onReady(async () => { + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) return; + + const config = new ProjectConfig(workspaceRoot); + const board = config.getBoard(); + panel.setBoard(board); + + // Read diagram.json if it exists + const diagram = config.readDiagramJson(); + if (diagram) { + panel.postMessage({ type: 'setDiagram', diagram }); + } + + // Start backend if needed (for ESP32 boards) + if (needsBackend(board)) { + try { + await backend.start(); + panel.setApiBase(backend.apiBase); + } catch (err) { + outputChannel.appendLine(`[Backend] Failed to start: ${err}`); + } + } + }); +} + +function needsBackend(board: BoardKind): boolean { + // Arduino compilation always needs the backend + // ESP32 boards also need QEMU via the backend WebSocket + return true; // For MVP, always start the backend +} + +async function compileAndLoad(context: vscode.ExtensionContext): Promise { + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) { + vscode.window.showErrorMessage('No workspace folder open'); + return; + } + + const config = new ProjectConfig(workspaceRoot); + const board = config.getBoard(); + const language = config.getLanguageMode(); + + const panel = SimulatorPanel.createOrShow(context.extensionUri); + setupPanelListeners(panel, context); + + // Check for pre-compiled firmware first + const firmwarePath = config.getFirmwarePath(); + if (firmwarePath) { + outputChannel.appendLine(`[Compile] Loading pre-compiled firmware: ${firmwarePath}`); + const fs = await import('fs'); + const data = fs.readFileSync(firmwarePath); + + if (firmwarePath.endsWith('.hex')) { + panel.loadHex(data.toString('utf-8'), board); + } else { + panel.loadBinary(data.toString('base64'), board); + } + return; + } + + // MicroPython: just send .py files + if (language === 'micropython') { + const files = await config.getSketchFiles(); + panel.loadMicroPython(files, board); + return; + } + + // Arduino: compile via backend + try { + await backend.start(); + + const files = await config.getSketchFiles(); + outputChannel.appendLine(`[Compile] Compiling ${files.length} files for ${board}...`); + + await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: 'Velxio: Compiling...' }, + async () => { + const response = await fetch(`${backend.apiBase}/compile`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + files: files.map(f => ({ name: f.name, content: f.content })), + board_fqbn: getBoardFqbn(board), + }), + }); + + if (!response.ok) { + const error = await response.json() as { detail?: string }; + throw new Error(error.detail ?? `Compilation failed (${response.status})`); + } + + const result = await response.json() as { hex?: string; binary?: string }; + if (result.hex) { + panel.loadHex(result.hex, board); + outputChannel.appendLine('[Compile] Success — hex loaded'); + } + }, + ); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Compilation failed: ${msg}`); + outputChannel.appendLine(`[Compile] Error: ${msg}`); + } +} + +function getBoardFqbn(board: BoardKind): string { + const fqbnMap: Record = { + 'arduino-uno': 'arduino:avr:uno', + 'arduino-nano': 'arduino:avr:nano:cpu=atmega328', + 'arduino-mega': 'arduino:avr:mega', + 'raspberry-pi-pico': 'rp2040:rp2040:rpipico', + 'pi-pico-w': 'rp2040:rp2040:rpipicow', + 'esp32': 'esp32:esp32:esp32', + 'esp32-s3': 'esp32:esp32:esp32s3', + 'esp32-c3': 'esp32:esp32:esp32c3', + 'attiny85': 'ATTinyCore:avr:attinyx5:chip=85,clock=internal16mhz', + }; + return fqbnMap[board] ?? 'arduino:avr:uno'; +} diff --git a/vscode-extension/src/types.ts b/vscode-extension/src/types.ts new file mode 100644 index 0000000..f7fe07f --- /dev/null +++ b/vscode-extension/src/types.ts @@ -0,0 +1,83 @@ +/** + * Shared types for extension ↔ WebView communication. + */ + +export type BoardKind = + | 'arduino-uno' + | 'arduino-nano' + | 'arduino-mega' + | 'raspberry-pi-pico' + | 'pi-pico-w' + | 'esp32' + | 'esp32-s3' + | 'esp32-c3' + | 'attiny85'; + +export type LanguageMode = 'arduino' | 'micropython'; + +/** Messages from the VS Code extension → WebView */ +export type ToWebviewMessage = + | { type: 'loadHex'; hex: string; board: BoardKind } + | { type: 'loadBinary'; firmwareBase64: string; board: BoardKind } + | { type: 'loadMicroPython'; files: Array<{ name: string; content: string }>; board: BoardKind } + | { type: 'start' } + | { type: 'stop' } + | { type: 'serialInput'; text: string } + | { type: 'setBoard'; board: BoardKind } + | { type: 'setDiagram'; diagram: DiagramJson } + | { type: 'setApiBase'; apiBase: string }; + +/** Messages from the WebView → VS Code extension */ +export type FromWebviewMessage = + | { type: 'ready' } + | { type: 'serialOutput'; text: string } + | { type: 'simulationState'; running: boolean } + | { type: 'error'; message: string } + | { type: 'requestCompile'; files: Array<{ name: string; content: string }>; board: string; fqbn: string } + | { type: 'log'; level: 'info' | 'warn' | 'error'; message: string }; + +/** diagram.json format (Wokwi-compatible) */ +export interface DiagramJson { + version: 1; + author?: string; + editor?: string; + parts: DiagramPart[]; + connections: DiagramConnection[]; + serialMonitor?: { display?: string; newline?: string }; +} + +export interface DiagramPart { + id: string; + type: string; + left: number; + top: number; + rotate?: number; + hide?: boolean; + attrs?: Record; +} + +/** [fromPin, toPin, wireColor, routingHints?] */ +export type DiagramConnection = [string, string, string, string[]?]; + +/** velxio.toml parsed config */ +export interface VelxioConfig { + velxio: { + version: number; + board?: string; + firmware?: string; + elf?: string; + language?: LanguageMode; + }; +} + +export const BOARD_LABELS: Record = { + 'arduino-uno': 'Arduino Uno', + 'arduino-nano': 'Arduino Nano', + 'arduino-mega': 'Arduino Mega 2560', + 'raspberry-pi-pico': 'Raspberry Pi Pico', + 'pi-pico-w': 'Raspberry Pi Pico W', + 'esp32': 'ESP32 DevKit V1', + 'esp32-s3': 'ESP32-S3 DevKit', + 'esp32-c3': 'ESP32-C3 DevKit', + 'attiny85': 'ATtiny85', +}; diff --git a/vscode-extension/tsconfig.json b/vscode-extension/tsconfig.json new file mode 100644 index 0000000..59c9b9c --- /dev/null +++ b/vscode-extension/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "webview"] +} diff --git a/vscode-extension/webview/index.html b/vscode-extension/webview/index.html new file mode 100644 index 0000000..f2c5557 --- /dev/null +++ b/vscode-extension/webview/index.html @@ -0,0 +1,12 @@ + + + + + + Velxio Simulator + + +
+ + + diff --git a/vscode-extension/webview/package-lock.json b/vscode-extension/webview/package-lock.json new file mode 100644 index 0000000..07bd85d --- /dev/null +++ b/vscode-extension/webview/package-lock.json @@ -0,0 +1,1820 @@ +{ + "name": "velxio-webview", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "velxio-webview", + "version": "0.1.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.3.0", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", + "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", + "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.328", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", + "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/vscode-extension/webview/package.json b/vscode-extension/webview/package.json new file mode 100644 index 0000000..2d0ed27 --- /dev/null +++ b/vscode-extension/webview/package.json @@ -0,0 +1,20 @@ +{ + "name": "velxio-webview", + "private": true, + "version": "0.1.0", + "scripts": { + "build": "vite build", + "dev": "vite build --watch" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.3.0", + "vite": "^6.0.0" + } +} diff --git a/vscode-extension/webview/src/bridge.ts b/vscode-extension/webview/src/bridge.ts new file mode 100644 index 0000000..0fd02f9 --- /dev/null +++ b/vscode-extension/webview/src/bridge.ts @@ -0,0 +1,61 @@ +/** + * WebView ↔ Extension bridge + * + * Receives messages from the VS Code extension host and translates them + * into simulation actions. Sends simulation events back to the extension. + */ + +// @ts-expect-error — acquireVsCodeApi is injected by VS Code's webview runtime +const vscode = acquireVsCodeApi(); + +export type ToWebviewMessage = + | { type: 'loadHex'; hex: string; board: string } + | { type: 'loadBinary'; firmwareBase64: string; board: string } + | { type: 'loadMicroPython'; files: Array<{ name: string; content: string }>; board: string } + | { type: 'start' } + | { type: 'stop' } + | { type: 'serialInput'; text: string } + | { type: 'setBoard'; board: string } + | { type: 'setDiagram'; diagram: unknown } + | { type: 'setApiBase'; apiBase: string }; + +type MessageHandler = (msg: ToWebviewMessage) => void; +const handlers: MessageHandler[] = []; + +/** Register a handler for messages from the extension */ +export function onMessage(handler: MessageHandler): void { + handlers.push(handler); +} + +/** Send a message to the extension host */ +export function postToExtension(message: unknown): void { + vscode.postMessage(message); +} + +/** Notify the extension that the WebView is ready */ +export function notifyReady(): void { + postToExtension({ type: 'ready' }); +} + +/** Send serial output to the extension */ +export function sendSerialOutput(text: string): void { + postToExtension({ type: 'serialOutput', text }); +} + +/** Send simulation state change to the extension */ +export function sendSimulationState(running: boolean): void { + postToExtension({ type: 'simulationState', running }); +} + +/** Send an error to the extension */ +export function sendError(message: string): void { + postToExtension({ type: 'error', message }); +} + +// Listen for messages from the extension host +window.addEventListener('message', (event) => { + const msg = event.data as ToWebviewMessage; + for (const handler of handlers) { + handler(msg); + } +}); diff --git a/vscode-extension/webview/src/webviewApp.tsx b/vscode-extension/webview/src/webviewApp.tsx new file mode 100644 index 0000000..8589cfb --- /dev/null +++ b/vscode-extension/webview/src/webviewApp.tsx @@ -0,0 +1,259 @@ +/** + * WebView App — Stripped-down Velxio simulator for VS Code WebView. + * + * This is a minimal React app that embeds: + * - SimulatorCanvas (board visualization + components) + * - SerialMonitor (serial output/input) + * + * It does NOT include: routing, auth, Monaco editor, project persistence. + * Communication with the VS Code extension is via postMessage bridge. + */ + +import React, { useEffect, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { onMessage, notifyReady, sendSerialOutput, sendSimulationState, sendError } from './bridge'; + +/** + * Placeholder app — Phase 1 MVP + * + * In the full implementation, this will import and render: + * - SimulatorCanvas from the shared frontend/src/components/ + * - SerialMonitor from the shared frontend/src/components/ + * - useSimulatorStore from the shared frontend/src/store/ + * + * For now, this is a functional skeleton that handles the postMessage + * protocol and shows a basic simulation status panel. + */ +const App: React.FC = () => { + const [board, setBoard] = useState('arduino-uno'); + const [running, setRunning] = useState(false); + const [serialOutput, setSerialOutput] = useState(''); + const [status, setStatus] = useState('Ready'); + const [hexLoaded, setHexLoaded] = useState(false); + + useEffect(() => { + onMessage((msg) => { + switch (msg.type) { + case 'setBoard': + setBoard(msg.board); + setStatus(`Board: ${msg.board}`); + break; + case 'loadHex': + setHexLoaded(true); + setStatus(`Firmware loaded for ${msg.board}`); + break; + case 'loadBinary': + setHexLoaded(true); + setStatus(`Binary firmware loaded for ${msg.board}`); + break; + case 'loadMicroPython': + setHexLoaded(true); + setStatus(`MicroPython loaded (${msg.files.length} files)`); + break; + case 'start': + setRunning(true); + setStatus('Simulation running'); + sendSimulationState(true); + break; + case 'stop': + setRunning(false); + setStatus('Simulation stopped'); + sendSimulationState(false); + break; + case 'serialInput': + setSerialOutput(prev => prev + `> ${msg.text}\n`); + break; + case 'setApiBase': + setStatus(`Backend: ${msg.apiBase}`); + break; + } + }); + + // Tell the extension we're ready + notifyReady(); + }, []); + + return ( +
+ {/* Header */} +
+ Velxio + {board} + + {status} +
+ + {/* Simulation Canvas Placeholder */} +
+
+
+ {board.includes('esp32') ? '⬡' : board.includes('pico') ? '◆' : '⬤'} +
+
{board}
+ {running && ( +
+ + Simulating... +
+ )} + {!running && !hexLoaded && ( +
+ Use Velxio: Run Simulation to start +
+ )} + {!running && hexLoaded && ( +
+ Firmware loaded. Press Run to start. +
+ )} +
+
+ + {/* Serial Monitor */} +
+
+ Serial Monitor + {running && CONNECTED} +
+
+          {serialOutput || (running ? 'Waiting for serial data...\n' : 'Start simulation to see output.\n')}
+        
+
+
+ ); +}; + +const styles: Record = { + container: { + display: 'flex', + flexDirection: 'column', + height: '100vh', + background: 'var(--vscode-editor-background, #1e1e1e)', + color: 'var(--vscode-editor-foreground, #cccccc)', + fontFamily: 'var(--vscode-font-family, monospace)', + fontSize: 13, + overflow: 'hidden', + }, + header: { + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '6px 12px', + background: 'var(--vscode-titleBar-activeBackground, #3c3c3c)', + borderBottom: '1px solid var(--vscode-panel-border, #333)', + flexShrink: 0, + }, + logo: { + fontWeight: 700, + fontSize: 14, + color: 'var(--vscode-textLink-foreground, #4fc3f7)', + }, + boardBadge: { + background: 'var(--vscode-badge-background, #0e639c)', + color: 'var(--vscode-badge-foreground, #fff)', + padding: '1px 8px', + borderRadius: 10, + fontSize: 11, + fontWeight: 600, + }, + statusDot: { + width: 8, + height: 8, + borderRadius: '50%', + flexShrink: 0, + }, + status: { + fontSize: 11, + color: 'var(--vscode-descriptionForeground, #999)', + }, + canvas: { + flex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: 0, + background: 'var(--vscode-editor-background, #1e1e1e)', + }, + canvasPlaceholder: { + textAlign: 'center' as const, + padding: 40, + }, + boardIcon: { + fontSize: 64, + marginBottom: 16, + opacity: 0.5, + }, + boardName: { + fontSize: 18, + fontWeight: 600, + marginBottom: 8, + }, + runningIndicator: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + color: '#4caf50', + fontSize: 14, + marginTop: 12, + }, + pulsingDot: { + width: 10, + height: 10, + borderRadius: '50%', + background: '#4caf50', + animation: 'pulse 1.5s ease-in-out infinite', + }, + hint: { + color: 'var(--vscode-descriptionForeground, #888)', + fontSize: 12, + marginTop: 12, + }, + serialContainer: { + height: 200, + display: 'flex', + flexDirection: 'column', + borderTop: '1px solid var(--vscode-panel-border, #333)', + flexShrink: 0, + }, + serialHeader: { + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '4px 12px', + background: 'var(--vscode-sideBar-background, #252526)', + borderBottom: '1px solid var(--vscode-panel-border, #333)', + }, + serialTitle: { + fontWeight: 600, + fontSize: 12, + }, + serialBadge: { + background: '#4caf50', + color: '#fff', + padding: '0 6px', + borderRadius: 3, + fontSize: 9, + fontWeight: 700, + }, + serialOutput: { + flex: 1, + margin: 0, + padding: 8, + color: '#00ff41', + background: '#0a0a0a', + overflowY: 'auto' as const, + whiteSpace: 'pre-wrap' as const, + fontSize: 12, + lineHeight: 1.4, + }, +}; + +// Mount +const root = document.getElementById('root'); +if (root) { + createRoot(root).render(); +} diff --git a/vscode-extension/webview/tsconfig.json b/vscode-extension/webview/tsconfig.json new file mode 100644 index 0000000..5655f73 --- /dev/null +++ b/vscode-extension/webview/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"] + }, + "include": ["src"] +} diff --git a/vscode-extension/webview/vite.config.ts b/vscode-extension/webview/vite.config.ts new file mode 100644 index 0000000..f678d71 --- /dev/null +++ b/vscode-extension/webview/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: path.resolve(__dirname, '..', 'dist', 'webview'), + emptyOutDir: true, + rollupOptions: { + input: path.resolve(__dirname, 'src', 'webviewApp.tsx'), + output: { + entryFileNames: 'index.js', + assetFileNames: 'index.[ext]', + // Single chunk for simplicity in WebView loading + manualChunks: undefined, + }, + }, + // Inline small assets to avoid CSP issues + assetsInlineLimit: 100000, + sourcemap: true, + }, +});