diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf5de8cc587..5566c88029d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,7 +161,7 @@ jobs: # it correctly - which is why the Linux/macOS jobs already pass. # # The version/appVersion/cliVersion/buildDate metadata mirrors upstream's - # scripts/package.js, but with Arduino Agent's version: it is the version + # scripts/package.js, but with Open Duino's version: it is the version # in the window title and About dialog, and buildDate is what makes the # app load its bundled VS Code plugins (arduino-ide-electron-main.js). # The name gives the app its own userData folder (and single-instance @@ -172,7 +172,7 @@ jobs: cli_version=$(node -p "require('../arduino-ide-extension/package.json').arduino['arduino-cli'].version") node ../node_modules/electron-builder/cli.js --dir --publish never \ -c.electronVersion=30.1.2 \ - -c.extraMetadata.name=arduino-agent \ + -c.extraMetadata.name=open-duino \ -c.extraMetadata.main=./arduino-ide-electron-main.js \ -c.extraMetadata.version="$version" \ -c.extraMetadata.theia.frontend.config.appVersion="$version" \ @@ -185,9 +185,7 @@ jobs: run: | $src = Get-ChildItem -Directory electron-app/dist | Where-Object { $_.Name -like '*-unpacked' } | Select-Object -First 1 - Compress-Archive -Path "$($src.FullName)/*" -DestinationPath "arduino-agent-${{ matrix.platform }}.zip" - # Pre-0.7.0 asset name, kept so existing download links keep working. - Copy-Item "arduino-agent-${{ matrix.platform }}.zip" "arduino-ide-mcp-${{ matrix.platform }}.zip" + Compress-Archive -Path "$($src.FullName)/*" -DestinationPath "open-duino-${{ matrix.platform }}.zip" - name: Zip artifact (Linux/macOS) if: runner.os != 'Windows' @@ -197,9 +195,7 @@ jobs: src=$(ls -d electron-app/dist/*-unpacked 2>/dev/null | head -1 || true) if [ -z "$src" ]; then src=$(ls -d electron-app/dist/mac* 2>/dev/null | head -1); fi if [ -z "$src" ]; then echo "No build output found under electron-app/dist"; ls -la electron-app/dist; exit 1; fi - ( cd "$src" && zip -ry "$GITHUB_WORKSPACE/arduino-agent-${{ matrix.platform }}.zip" . ) - # Pre-0.7.0 asset name, kept so existing download links keep working. - cp "arduino-agent-${{ matrix.platform }}.zip" "arduino-ide-mcp-${{ matrix.platform }}.zip" + ( cd "$src" && zip -ry "$GITHUB_WORKSPACE/open-duino-${{ matrix.platform }}.zip" . ) - name: Upload to draft release uses: softprops/action-gh-release@v2 @@ -209,16 +205,15 @@ jobs: draft: true # publish manually after verifying the artifacts fail_on_unmatched_files: true body: | - Unsigned build of Arduino Agent: the Arduino IDE with an embedded MCP server. + Unsigned build of Open Duino: the Arduino IDE with an embedded MCP server. Not affiliated with or endorsed by Arduino SA. Each archive contains the unpacked application for its platform - unzip and run - `Arduino Agent` (`Arduino Agent.exe` on Windows). The MCP server starts on - `http://127.0.0.1:3847`; the connection config (with the auth token) is printed - to the IDE console on launch. + `Open Duino` (`Open Duino.exe` on Windows). The MCP server starts on + `http://127.0.0.1:3847`; use **Help > Connect an AI Agent** to configure OpenCode + or another MCP client. Artifacts are unsigned; on macOS you may need to allow the app in System Settings > Privacy & Security. files: | - arduino-agent-${{ matrix.platform }}.zip - arduino-ide-mcp-${{ matrix.platform }}.zip + open-duino-${{ matrix.platform }}.zip diff --git a/.gitignore b/.gitignore index f8847ad0c6c..4de22219a23 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,10 @@ electron-app/dist # Yarn local install state (machine-specific cache) .yarn/install-state.gz +# Local Electron and Theia cache used by the development launcher. +.dev-cache/ +# Local caches and generated development packages are not source artifacts. +.cache/ +electron-app/dist-open-duino/ +__pycache__/ +*.py[cod] diff --git a/.yarn/patches/node-pty-npm-0.11.0-beta24-bb53864dbd.patch b/.yarn/patches/node-pty-npm-0.11.0-beta24-bb53864dbd.patch index 4004dd7ab07..257bf32889f 100644 --- a/.yarn/patches/node-pty-npm-0.11.0-beta24-bb53864dbd.patch +++ b/.yarn/patches/node-pty-npm-0.11.0-beta24-bb53864dbd.patch @@ -1,38 +1,20 @@ -diff --git a/binding.gyp b/binding.gyp -index 66411ec3f7caa6880ad0f6284682fce5b0067416..bba36bf3be2a5d947a205e42658b50a18d368115 100644 ---- a/binding.gyp -+++ b/binding.gyp -@@ -6,7 +6,8 @@ - 'VCCLCompilerTool': { - 'AdditionalOptions': [ - '/Qspectre', -- '/guard:cf' -+ '/guard:cf', -+ '-permissive' - ] - }, - 'VCLinkerTool': { -diff --git a/src/win/conpty.cc b/src/win/conpty.cc -index 25793a5e1c97aa63b59117eeb1aea6bc3a9ca3bf..58000c01c42e91e413ae86d6b762908007e46cb1 100644 ---- a/src/win/conpty.cc -+++ b/src/win/conpty.cc -@@ -32,12 +32,17 @@ extern "C" void init(v8::Local); - ProcThreadAttributeValue(22, FALSE, TRUE, FALSE) +diff --git a/src/win/winpty.cc b/src/win/winpty.cc +index b054dee2194226a823f5c98befdecf06cda2e5dd..d92e4f4001e4708955e29a1db03c5ed6f92c340f 100644 +--- a/src/win/winpty.cc ++++ b/src/win/winpty.cc +@@ -165,6 +165,7 @@ static NAN_METHOD(PtyStartProcess) { + goto cleanup; + } + ++ { + int cols = info[4]->Int32Value(Nan::GetCurrentContext()).FromJust(); + int rows = info[5]->Int32Value(Nan::GetCurrentContext()).FromJust(); + bool debug = Nan::To(info[6]).FromJust(); +@@ -234,6 +235,7 @@ static NAN_METHOD(PtyStartProcess) { + info.GetReturnValue().Set(marshal); - typedef VOID* HPCON; -+ -+#endif -+ -+// These typedefs must exist regardless of the SDK version. Newer Windows SDKs -+// (>= 10.0.26100) define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, which previously -+// skipped these declarations and broke the build with "PFNCREATEPSEUDOCONSOLE -+// undeclared identifier". Fixed upstream in later node-pty releases. - typedef HRESULT (__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn, HANDLE hOut, DWORD dwFlags, HPCON* phpcon); - typedef HRESULT (__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize); - typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc); + goto cleanup; ++ } --#endif -- - struct pty_baton { - int id; - HANDLE hIn; + cleanup: + delete filename; diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..238d2d776ff --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Open Duino repository guide + +Read `CLAUDE.md` for the detailed workspace map, build commands, vendored-code +rules, MCP architecture, and hardware notes. Those engineering instructions +also apply when working with OpenCode. + +## Fork conventions + +- The application is branded **Open Duino**. Keep upstream Arduino IDE notices + and trademark language intact; do not imply Arduino SA endorses this fork. +- `arduino-ide-extension/` is vendored Arduino IDE code. Put fork-specific + features in `arduino-mcp-extension/` unless an upstream change is necessary. +- MCP file changes already notify the frontend and refresh the open editor. + Preserve that path when adding OpenCode workflows. +- OpenCode connects through the bundled stdio bridge. Generate its setup in + `MCPServiceImpl.getConnectionGuide()` so paths and executable details match + each installed build. +- Keep dark and light themes available. Theme IDs remain compatible with saved + user settings even when their display names or palettes change. diff --git a/CLAUDE.md b/CLAUDE.md index aec859c784e..e1f2bf344c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# Arduino Agent — repo guide for AI agents +# Open Duino — repo guide for AI agents Fork of Arduino IDE 2.x (Theia/Electron) with an embedded MCP server. `arduino-ide-extension/` is **vendored upstream code** (synced from @@ -42,7 +42,7 @@ release folder) without full packaging: file — piping through tail/grep has repeatedly swallowed the real error). If it dies with ENOTEMPTY on `lib/backend/native-webpack-plugin`, delete that directory and rerun (transient Windows file-lock race). -3. Kill "Arduino Agent" processes ("Arduino IDE" before 0.7.0), copy +3. Kill "Open Duino" processes (or "Arduino Agent" for a legacy build), copy `electron-app/lib/backend/main.js` over `\resources\app\lib\backend\main.js`, relaunch. 4. Confirm the copy landed by grepping the installed main.js for a distinctive diff --git a/README.md b/README.md index 2acc5984647..a858cd6bd9c 100644 --- a/README.md +++ b/README.md @@ -1,284 +1,153 @@ - +# Open Duino -# Arduino Agent +**Un entorno de desarrollo Arduino con un agente de IA integrado.** Open Duino combina el IDE Arduino 2.x con un servidor MCP integrado para que OpenCode y otros clientes compatibles puedan trabajar con tus sketches, placas y herramientas desde la misma aplicación. -**The AI-native Arduino IDE.** Arduino Agent is a full Arduino IDE 2.x with a -Model Context Protocol (MCP) server built into its core — so an AI agent like -Claude can write sketches, compile them, upload to real boards, read the serial -monitor, and manage libraries *alongside you*, editing the same files you see in -the editor in real time. +![Concepto visual de Open Duino: editor, vista de circuito y panel de OpenCode](docs/images/open-duino-neon.png) -There's no plugin to install into the AI, no sidecar process, no copy-pasting -code back and forth. **The IDE itself is the agent's workbench.** You launch it, -point your assistant at `http://127.0.0.1:3847`, and the two of you share one -editor, one board, one serial monitor. +> La imagen es un concepto visual de la interfaz. Open Duino es un proyecto comunitario independiente; no está afiliado, patrocinado ni respaldado por Arduino SA. -Built on [Arduino IDE 2.x](https://github.com/arduino/arduino-ide) · An -independent community project, not affiliated with or endorsed by Arduino SA · -AGPL-3.0 +[![Descargar para Windows](https://img.shields.io/badge/Descargar-Windows%20x64-00cfe8?style=for-the-badge&logo=windows)](https://github.com/lanuzadev/Open-Duino/releases/download/openduino-2.3.10-preview.1/open-duino_2.3.10-snapshot-2acf4fa_Windows_64bit.exe) + +**Versión de prueba:** `2.3.10-snapshot-2acf4fa` · **Autor:** Lanuzadev · **Repositorio:** [github.com/lanuzadev/Open-Duino](https://github.com/lanuzadev/Open-Duino) --- -## Why it exists - -The Arduino IDE is where embedded projects get built. Modern AI agents are great -at embedded code — but they work blind, guessing at your board, your errors, and -your wiring, and handing you snippets to paste. Arduino Agent closes that gap by -making the IDE a first-class participant in the conversation: - -- **The agent sees what you see** — the open sketch, the selected board and port, - connected devices, and real compiler output (not a guess). -- **You see what the agent does** — when it writes a file, the editor opens it, - reloads it, and shows a *"Created by Claude"* notification. True pair - programming on hardware. -- **It drives the real toolchain** — the same `arduino-cli`, clang-format, and - serial monitor the IDE uses. Compile results, memory usage, and upload status - are the genuine article. - -## Download - -Unsigned development builds of the **[latest release](https://github.com/mixelpixx/Arduino-Agent/releases/latest)** — these links always point at the newest version: - -- [**Windows x64**](https://github.com/mixelpixx/Arduino-Agent/releases/latest/download/arduino-agent-windows.zip) -- [**macOS**](https://github.com/mixelpixx/Arduino-Agent/releases/latest/download/arduino-agent-macos.zip) -- [**Linux x64**](https://github.com/mixelpixx/Arduino-Agent/releases/latest/download/arduino-agent-linux.zip) - -Unzip and run **Arduino Agent** (`Arduino Agent.exe` on Windows, -`Arduino Agent.app` on macOS, `arduino-agent` on Linux). Arduino Agent doesn't -check Arduino's update server; get new versions here. Release notes and older -builds are on the [Releases](https://github.com/mixelpixx/Arduino-Agent/releases) -page. - -> **Got the official Arduino IDE too?** They run side by side as separate apps. -> They share what should be shared, your sketchbook, installed boards and -> libraries, and preferences (`~/.arduinoIDE`), but each keeps its own window -> state and opens independently. -> -> **Upgrading from 0.6.x?** The executable was called `Arduino IDE` before 0.7.0. -> Your window layout and per-sketch board selections carry over on first launch, -> and an MCP config whose `ARDUINO_AGENT_PATH` still names `Arduino IDE.exe` -> keeps working (the bridge finds the renamed executable), but update it when -> convenient. - -> These are unsigned dev builds. On macOS you may need to allow the app under -> **System Settings → Privacy & Security**; on Windows, dismiss SmartScreen with -> **More info → Run anyway**. Prefer to build it yourself? See -> [Building from source](#building-from-source). - -## Quick start — connect your agent - -1. **Launch Arduino Agent.** The MCP server starts automatically on - `http://127.0.0.1:3847`. The status bar (bottom left) shows **MCP ready**, - and **Agent active** once an assistant is using it. - -2. **Help › Connect an AI Agent…** (or click that status bar item). It gives - you copy-ready setups with your installation's real paths already filled in: - - **Claude Code**: one `claude mcp add` command to paste into a terminal. - - **Claude Desktop, Cursor and other MCP clients**: an `mcpServers` entry - for the client's config file. - - **Direct HTTP**: for clients that speak Streamable HTTP, with your token. - - The first two go through the bundled stdio bridge, so the client stays - connected while Arduino Agent is closed and starts it when a tool needs it - (needs Node.js 18 or newer). - -
- Prefer to write the config yourself? - - Via the bridge (recommended), on Windows: - - ```json - { - "mcpServers": { - "arduino": { - "command": "node", - "args": ["C:/path/to/Arduino Agent/resources/mcp-bridge/bridge/arduino-agent-bridge.js"], - "env": { "ARDUINO_AGENT_PATH": "C:/path/to/Arduino Agent/Arduino Agent.exe" } - } - } - } - ``` +## Descargar e instalar - On macOS the bridge is at - `Arduino Agent.app/Contents/Resources/mcp-bridge/bridge/arduino-agent-bridge.js`; - on Linux, under `resources/` in the unzipped folder. From a source checkout, - use `arduino-mcp-extension/bridge/arduino-agent-bridge.js` after building the - extension. The bridge reads the token from `~/.arduinoIDE/mcp-token` itself - and has no dependencies; see [the bridge README](arduino-mcp-extension/bridge/README.md) - for its options. - - Direct HTTP (only connects while Arduino Agent is running): - - ```json - { - "mcpServers": { - "arduino": { - "type": "http", - "url": "http://127.0.0.1:3847/mcp", - "headers": { "Authorization": "Bearer " } - } - } - } - ``` +1. Descarga el [instalador para Windows de 64 bits](https://github.com/lanuzadev/Open-Duino/releases/download/openduino-2.3.10-preview.1/open-duino_2.3.10-snapshot-2acf4fa_Windows_64bit.exe). +2. Ejecuta el archivo y sigue los pasos del asistente. Puedes elegir la carpeta de instalación. +3. Abre Open Duino desde el acceso directo del menú Inicio o del escritorio. +4. Conecta una placa Arduino por USB y selecciónala en el menú de placas. -
+Esta compilación no está firmada con un certificado digital. Windows puede mostrar una advertencia de SmartScreen; revisa que el archivo provenga de este repositorio antes de ejecutarlo. - > **Claude Code users:** the server sends workflow guidance automatically, - > and ships three slash commands (`/bringup`, `/debug-serial`, - > `/profile-board`). For deeper hardware know-how, install the bundled - > skill: copy [`skills/arduino-agent/`](skills/arduino-agent/) into - > `~/.claude/skills/`. +Open Duino puede instalarse junto al IDE oficial de Arduino. Son aplicaciones distintas y comparten la carpeta de sketches, las placas, las bibliotecas y preferencias de Arduino que usan esos componentes. -3. **Talk to your board.** - - *"Create a Blink sketch and open it."* - - *"What boards are connected?"* - - *"Compile for the Uno and explain any errors."* - - *"Upload it, then show me the serial output at 115200."* +## Conectar OpenCode -## What the agent can do +La integración une OpenCode con el IDE mediante MCP. El agente puede leer y modificar el sketch que tienes abierto y tú puedes ver los cambios en el editor. -| Category | Operations | -|----------|------------| -| **Sketches** | Create, open, and edit sketches; read/write code; browse and clone built-in examples | -| **Build** | Compile with `wait:true` for one-call results; live progress; real compiler output with structured, explained errors | -| **Upload** | Compile + flash in one call; failures come back explained (bootloader mode, busy port, wrong FQBN, power) | -| **Boards** | Detect connected boards with USB vid/pid; identify unknown boards (`suggest_fqbn`); pin capabilities; install cores | -| **Serial** | Cursor-based lossless reads; `wait_for` a pattern; automatic crash/reset/watchdog/brownout detection | -| **Libraries** | Search the registry; install/remove; browse library examples | -| **Formatting** | Format Arduino/C++ with clang-format | -| **Config** | Sketchbook location, board-manager URLs, IDE settings | +1. Instala OpenCode y configura un proveedor de modelos si todavía no lo has hecho. +2. Abre Open Duino. +3. Elige **Help > Connect an AI Agent...**. También puedes pulsar el estado MCP en la esquina inferior izquierda. +4. Copia el comando de **OpenCode** que aparece en el diálogo y ejecútalo en una terminal. El comando se genera con las rutas de esa instalación. +5. Elige **Help > OpenCode Chat** para abrir OpenCode en la terminal integrada, junto al editor. +6. Pídele, por ejemplo: “Crea un sketch Blink para la placa seleccionada, compílalo y explícame el resultado”. -By default the tools are exposed through a **router pattern** — 4 meta-tools -(`list_tool_categories`, `get_category_tools`, `execute_tool`, `search_tools`) -so the agent discovers tools on demand instead of loading every definition into -its context. A **direct mode** exposes all tools individually if you prefer. +La conexión recomendada usa el puente MCP incluido con Open Duino. Cuando OpenCode solicita una herramienta, el puente puede abrir la aplicación si está cerrada. Se necesita Node.js 18 o posterior. También puedes copiar la configuración manual que muestra el diálogo. -## How it works +El servidor MCP escucha por defecto en `http://127.0.0.1:3847`. La opción de conexión directa solo funciona mientras Open Duino está abierto; el puente incluido puede iniciar el IDE cuando el agente necesite una herramienta. -``` -+------------------+ HTTP (Bearer auth) +---------------------------+ -| AI agent | <------------------------> | Arduino Agent | -| (MCP client) | http://127.0.0.1:3847 | (Theia/Electron + MCP) | -+------------------+ /mcp +------------------------------+ - | - Arduino toolchain - (arduino-cli daemon, clang-format, - pluggable serial monitor) -``` +### Otros clientes MCP -The MCP server is embedded in the IDE's backend and speaks the modern -**Streamable HTTP** transport (plus legacy SSE for older clients), supporting -multiple simultaneous sessions. +En **Help > Connect an AI Agent...** encontrarás instrucciones para Claude Code, Claude Desktop, Cursor y clientes compatibles con MCP. El diálogo genera las rutas de la instalación actual; usa esas instrucciones en lugar de copiar rutas de ejemplo. -**Security is on by default:** -- Binds to `127.0.0.1` only. -- Requires a bearer token (generated on first launch, stored in - `~/.arduinoIDE/mcp-token`). -- Rejects browser-originated requests and sends no CORS headers, so a web page - can't reach it. -- Confines file access to your sketchbook and the built-in examples. +## Funciones -The only unauthenticated endpoint is a health check: +- Crear, abrir, leer y editar sketches Arduino desde el editor. +- Consultar placas y puertos conectados, seleccionar una placa e instalar núcleos. +- Compilar y cargar sketches usando el mismo toolchain del IDE. +- Leer y enviar datos por el monitor serial, esperar texto específico y detectar reinicios o errores comunes. +- Buscar, instalar y quitar bibliotecas; abrir ejemplos y formatear código Arduino/C++. +- Trabajar con un agente desde OpenCode integrado en una terminal de la aplicación. +- Usar el servidor MCP desde otros clientes compatibles, con herramientas en modo **Router** o **Direct**. +- Cambiar entre los temas claro y oscuro. -```bash -curl http://127.0.0.1:3847/health -``` +Los archivos que modifica el agente se reflejan en el editor. La selección de la placa y los resultados de compilación y carga corresponden al IDE y a las herramientas instaladas en el equipo. + +## Privacidad y seguridad de la conexión MCP + +- El servidor se enlaza a `127.0.0.1`; no se publica en la red local. +- Las solicitudes requieren un token Bearer generado por la aplicación. +- El token se guarda localmente en `~/.arduinoIDE/mcp-token` (en Windows, dentro de la carpeta del usuario). +- El puente incluido lee el token por ti. No lo compartas ni lo publiques. +- El acceso a archivos del servidor está limitado al sketchbook y a los ejemplos integrados. + +## Solución de problemas + +### OpenCode muestra `Session failed` o no carga `core/instructions` -## Settings +1. Cierra la sesión de OpenCode abierta en la terminal integrada. +2. Actualiza Open Duino a la compilación de este repositorio, que incluye el ajuste de ruta de Windows. +3. Vuelve a abrirla desde **Help > OpenCode Chat**. +4. Si el problema continúa, cierra y vuelve a abrir Open Duino y comprueba que OpenCode pueda iniciarse desde una terminal de Windows. -**File → Preferences → MCP:** +### El agente no encuentra la placa -| Setting | Description | Default | -|---------|-------------|---------| -| Enable MCP server | Turn the integration on/off | `true` | -| Start automatically | Launch the server with the IDE | `true` | -| Server port | HTTP port (1024–65535) | `3847` | -| Require auth | Require the bearer token | `true` | -| Log level | none / error / info / debug | `info` | -| Tool mode | Router (4 meta-tools) or Direct (all tools) | `router` | +Comprueba el cable USB, el puerto y la selección de placa. Algunas placas, incluidos ciertos modelos ESP32-S3, no se pueden identificar automáticamente. Pide al agente que use `suggest_fqbn` o selecciona el modelo y el FQBN correctos en el IDE. Puede ser necesario instalar el núcleo desde el gestor de placas. -## Made for learning, too +### La carga falla o el puerto está ocupado -Arduino Agent ships extras aimed at STEM and classroom use: +Cierra otras aplicaciones que estén usando el puerto serial. Si el monitor serial del IDE ya está conectado, desconéctalo antes de cargar. Algunas placas que usan USB nativo requieren entrar manualmente en modo bootloader. -- **Example browser** — every built-in Arduino example, with descriptions. -- **Hardware reference** — ask for a board's pin map, PWM/I2C/SPI pins, memory. -- **Beginner-friendly errors** — compiler errors returned with plain-language - explanations and suggested fixes. +### OpenCode no aparece en el menú o no inicia -It also carries a modernized UI (refined buttons, dialogs, board selector, -progress bars, and serial monitor) that respects both light and dark themes. +Instala OpenCode, comprueba que el comando `opencode` esté disponible en una terminal y vuelve a iniciar Open Duino. En **Help > Connect an AI Agent...**, revisa que la conexión aparezca configurada. El puente requiere Node.js 18 o posterior. -## Building from source +## Desarrollo y ejecución desde el código -**Prerequisites:** Node.js 18+, Yarn 4 (via Corepack), Python 3.11, Go 1.21, and -a C/C++ toolchain (VS 2022 Build Tools on Windows). +Para probar cambios de código sin generar un instalador en cada iteración, ejecuta el IDE de desarrollo desde el repositorio. -```bash -git clone https://github.com/mixelpixx/arduino-mcp.git -cd arduino-mcp +### Requisitos +- Windows 10/11 de 64 bits. +- Node.js `18.17` o posterior, pero anterior a la versión 21. +- Yarn 4 mediante Corepack (el repositorio fija Yarn `4.5.3`). +- Python 3.9–3.12 y Go 1.21 o posterior. +- Visual Studio 2022 Build Tools con la carga **Desktop development with C++**. +- Git. + +### Primera ejecución + +Abre PowerShell en la carpeta del repositorio y ejecuta: + +```powershell corepack enable yarn install -yarn prepare:shims # create the launcher shims Theia's build expects - -yarn build:dev # build all packages, including the MCP extension -cd electron-app && yarn start +yarn prepare:shims +npm run dev ``` -Windows has a few extra native-module notes (mostly automated now) — see -[**docs/BUILDING-WINDOWS.md**](docs/BUILDING-WINDOWS.md). Full extension -documentation lives in -[**arduino-mcp-extension/README.md**](arduino-mcp-extension/README.md). +El primer arranque prepara dependencias nativas y compila los componentes de desarrollo, por lo que puede tardar varios minutos. Después abrirá Open Duino directamente desde el código. + +### Iteraciones rápidas -## Project status +Para dejar los compiladores en modo de observación y reiniciar la aplicación por separado: -Actively developed, and **verified against real hardware** — an ESP32-S3 -(N16R8) driven end to end through the MCP tools alone: +1. Abre una primera terminal en la raíz del repositorio y ejecuta: -> install the board core → create a sketch → write the code → compile → -> upload → read the board's own serial output + ```powershell + npm run watch + ``` + +2. Abre una segunda terminal en la misma carpeta y ejecuta: -Every step ran as an MCP call, with no manual work in the IDE. In one pass the -agent wrote a WiFi scanner, flashed it, and read 21 access points back off the -board. + ```powershell + npm run start --prefix electron-app + ``` -Those sessions shaped the tooling itself. v0.6.0 turned every pain point they -surfaced into a feature: `wait:true` replaces polling loops, serial reads are -lossless and cursor-based, crashes/resets/watchdogs are detected and reported -as events, upload failures come back explained, and `suggest_fqbn` identifies -boards arduino-cli can't. The server also now teaches connected agents its own -workflow (instructions at connect, `/bringup`-style prompts, a Claude Code -skill). +Los cambios de frontend se actualizan al recargar la ventana. Para cambios de backend, reinicia la aplicación desde la segunda terminal. Este modo sirve para desarrollo; no crea un instalador. -Release builds for **Windows, macOS and Linux** are produced by CI and attached -to every tagged release. +### Crear el instalador de Windows -**Known limits:** artifacts are unsigned. Boards whose USB VID/PID appear in no -`boards.txt` (many ESP32-S3 devkits) can't be auto-identified — use -`arduino_board suggest_fqbn`, then pass the FQBN explicitly. Uploading over a -board's *native* USB port can require manual bootloader entry; a UART bridge -port works without it. +Con las dependencias instaladas y los módulos nativos compilados para la versión de Electron del proyecto, genera los bundles de producción y ejecuta el empaquetador desde PowerShell: + +```powershell +npm run build --prefix arduino-ide-extension +npm run build --prefix arduino-mcp-extension +node scripts/copy-dev-resources.js +npm run build --prefix electron-app +npm run package --prefix electron-app -- --win nsis +``` -Contributions and bug reports are welcome via -[Issues](https://github.com/mixelpixx/arduino-mcp/issues) and pull requests. +El instalador se guarda en `electron-app/dist/`. Los pasos de compilación de producción pueden requerir que los módulos nativos se reconstruyan para Electron. Consulta [la guía técnica de compilación para Windows](docs/BUILDING-WINDOWS.md) si necesitas preparar un entorno desde cero. -## Relationship to the Arduino IDE +## Contribuir -Arduino Agent is a fork of the open-source -**[Arduino IDE 2.x](https://github.com/arduino/arduino-ide)** (a -[Theia](https://theia-ide.org/)/[Electron](https://www.electronjs.org/) -application that drives the [arduino-cli](https://github.com/arduino/arduino-cli)). -All of the core IDE work is theirs; this project adds the embedded MCP server, -the AI-collaboration features, and the UI refinements on top. +Las contribuciones y los informes de errores son bienvenidos. Abre un [issue](https://github.com/lanuzadev/Open-Duino/issues) para comunicar un problema o crea un pull request con tu propuesta. Indica la versión, el sistema operativo y los pasos para reproducir cualquier error. -**Arduino® is a trademark of Arduino SA.** Arduino Agent is an independent, -community project and is **not affiliated with, sponsored by, or endorsed by -Arduino SA.** The name describes this project's purpose — an agent-driven Arduino -development environment — and implies no official connection. +## Proyecto y licencia -## License +Open Duino es una versión comunitaria basada en el código abierto de **[Arduino IDE 2.x](https://github.com/arduino/arduino-ide)**. Conserva los avisos de copyright y las marcas de sus autores originales. **Arduino® es una marca registrada de Arduino SA.** Este proyecto es independiente y no está afiliado, patrocinado ni respaldado por Arduino SA. -Licensed under the **GNU AGPL-3.0-or-later**, the same license as the upstream -Arduino IDE. Distributions include third-party components under compatible -licenses (GPLv2, MIT, BSD-3). See [LICENSE.txt](LICENSE.txt). +El repositorio está publicado bajo la licencia **GNU AGPL-3.0-or-later**. Consulta [LICENSE.txt](LICENSE.txt) y los avisos de terceros incluidos con la aplicación. diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 61f60a95068..7e0844eb03f 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -16,8 +16,8 @@ "lint": "eslint .", "prebuild": "rimraf lib", "build": "tsc", - "build:dev": "yarn build", - "postbuild": "ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/", + "build:dev": "npm run build", + "postbuild": "node ./scripts/copy-protocol.js", "watch": "tsc -w", "test": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.test.js\"", "test:slow": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.slow-test.js\" --slow 5000", diff --git a/arduino-ide-extension/scripts/copy-protocol.js b/arduino-ide-extension/scripts/copy-protocol.js new file mode 100644 index 00000000000..50839c85939 --- /dev/null +++ b/arduino-ide-extension/scripts/copy-protocol.js @@ -0,0 +1,18 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +const source = path.resolve(__dirname, '../src/node/cli-protocol'); +const destination = path.resolve(__dirname, '../lib/node/cli-protocol'); + +if (!fs.existsSync(source)) { + throw new Error(`Arduino CLI protocol source is missing: ${source}`); +} + +fs.rmSync(destination, { recursive: true, force: true }); +fs.mkdirSync(path.dirname(destination), { recursive: true }); +fs.cpSync(source, destination, { recursive: true }); + +const requiredModule = path.join(destination, 'cc/arduino/cli/commands/v1/upload_pb.js'); +if (!fs.existsSync(requiredModule)) { + throw new Error(`Arduino CLI protocol copy did not produce ${requiredModule}`); +} diff --git a/arduino-ide-extension/scripts/generate-protocol.js b/arduino-ide-extension/scripts/generate-protocol.js index 7e0cf8a555b..d443ad57097 100644 --- a/arduino-ide-extension/scripts/generate-protocol.js +++ b/arduino-ide-extension/scripts/generate-protocol.js @@ -247,42 +247,91 @@ // rmSync(out, { recursive: true, maxRetries: 5, force: true }); mkdirSync(out, { recursive: true }); + let pluginLauncherDir; try { + let tsPluginPath = path.resolve( + __dirname, + '..', + 'node_modules', + '.bin', + `protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}` + ); + + // protoc cannot launch a Windows .cmd shim directly, so use a native + // launcher that forwards its stdin and stdout to the Node-based plugin. + if (platform === 'win32') { + pluginLauncherDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'arduino-protoc-gen-ts-') + ); + tsPluginPath = path.join(pluginLauncherDir, 'protoc-gen-ts.exe'); + exec( + 'go', + [ + 'build', + '-o', + tsPluginPath, + path.join(__dirname, 'protoc-plugin-launcher.go'), + ], + { + cwd: pluginLauncherDir, + env: { ...process.env, GO111MODULE: 'off' }, + logStdout: true, + } + ); + } + // Generate JS code from the `.proto` files. exec( - 'grpc_tools_node_protoc', + process.execPath, [ + require.resolve('grpc-tools/bin/protoc.js'), `--js_out=import_style=commonjs,binary:${out}`, `--grpc_out=generate_package_definition:${out}`, '-I', protosFolder, ...protos, ], - { logStdout: true } + { + env: { + ...process.env, + OPENDUINO_NODE_EXE: process.execPath, + OPENDUINO_TS_PLUGIN: require.resolve( + 'grpc_tools_node_protoc_ts/bin/protoc-gen-ts' + ), + }, + logStdout: true, + } ); // Generate the `.d.ts` files for JS. exec( path.join(protoc, `protoc${platform === 'win32' ? '.exe' : ''}`), [ - `--plugin=protoc-gen-ts=${path.resolve( - __dirname, - '..', - 'node_modules', - '.bin', - `protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}` - )}`, + `--plugin=protoc-gen-ts=${tsPluginPath}`, `--ts_out=generate_package_definition:${out}`, '-I', protosFolder, ...protos, ], - { logStdout: true } + { + env: { + ...process.env, + OPENDUINO_NODE_EXE: process.execPath, + OPENDUINO_TS_PLUGIN: require.resolve( + 'grpc_tools_node_protoc_ts/bin/protoc-gen-ts' + ), + }, + logStdout: true, + } ); } catch (error) { - console.log(error); + console.error(error); + process.exitCode = 1; } finally { rmSync(protosFolder, { recursive: true, maxRetries: 5, force: true }); + if (pluginLauncherDir) { + rmSync(pluginLauncherDir, { recursive: true, maxRetries: 5, force: true }); + } } console.log('<<< Generation was successful.'); diff --git a/arduino-ide-extension/scripts/protoc-plugin-launcher.go b/arduino-ide-extension/scripts/protoc-plugin-launcher.go new file mode 100644 index 00000000000..c7d7a3d5b16 --- /dev/null +++ b/arduino-ide-extension/scripts/protoc-plugin-launcher.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +func main() { + cmd := exec.Command(os.Getenv("OPENDUINO_NODE_EXE"), os.Getenv("OPENDUINO_TS_PLUGIN")) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintln(os.Stderr, err) + if exitError, ok := err.(*exec.ExitError); ok { + os.Exit(exitError.ExitCode()) + } + os.Exit(1) + } +} diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx index 203c586fe76..fbcda0db965 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx @@ -538,7 +538,7 @@ export class SettingsComponent extends React.Component<

{nls.localize( 'arduino/preferences/mcp.description', - 'MCP enables AI assistants like Claude Code to work in Arduino Agent with you - compiling sketches, uploading code, managing libraries, and more.' + 'MCP lets OpenCode and other AI assistants work with you in Open Duino: editing sketches, compiling, uploading, and managing libraries.' )}

diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 234ff55d591..9b84cd0e116 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -84,7 +84,7 @@ function enableFileLogger() { process.env['XDG_CONFIG_HOME'] = xdgConfigHome; } setupFileLog({ - appName: 'Arduino Agent', // Arduino Agent: own log folder, not the official IDE's + appName: 'Open Duino', // Open Duino: own log folder, not the official IDE's maxSize: 10 * 1024 * 1024, }); for (const name of consoleLogFunctionNames) { @@ -893,7 +893,7 @@ async function updateFrontendApplicationConfigFromPackageJson( } const fallbackFrontendAppConfig: FrontendApplicationConfig = { - applicationName: 'Arduino Agent', // Arduino Agent + applicationName: 'Open Duino', // Open Duino defaultTheme: { light: 'arduino-theme', dark: 'arduino-theme-dark', @@ -903,7 +903,7 @@ const fallbackFrontendAppConfig: FrontendApplicationConfig = { defaultLocale: '', electron: { showWindowEarly: true, - uriScheme: 'arduino-agent', // Arduino Agent + uriScheme: 'openduino', // Open Duino }, reloadOnReconnect: true, }; diff --git a/arduino-mcp-extension/README.md b/arduino-mcp-extension/README.md index d60fc28ec3d..7cf262a349f 100644 --- a/arduino-mcp-extension/README.md +++ b/arduino-mcp-extension/README.md @@ -1,6 +1,6 @@ -# Arduino MCP Extension +# Open Duino MCP Extension -MCP (Model Context Protocol) server **embedded directly** in Arduino IDE 2.x, enabling AI assistants like Claude Code to programmatically interact with the IDE. +MCP (Model Context Protocol) server **embedded directly** in Open Duino, enabling OpenCode and other AI assistants to work with the IDE and attached boards. Designed with **STEM education** in mind - includes built-in example browsing, hardware reference data, and beginner-friendly error explanations. @@ -11,7 +11,7 @@ The extension embeds the MCP server directly in the IDE using HTTP transport ``` +------------------+ HTTP (Bearer auth) +------------------+ -| Claude Code |<----------------------->| Arduino IDE | +| OpenCode / agent |<----------------------->| Open Duino | | (MCP Client) | http://127.0.0.1:3847 | (MCP Server) | +------------------+ /mcp +------------------+ ↓ @@ -22,7 +22,7 @@ The extension embeds the MCP server directly in the IDE using HTTP transport Config) ``` -**The Arduino IDE IS the MCP server** - no sidecar process, no abstraction layer. +**Open Duino is the MCP server** - no sidecar process, no abstraction layer. ## Security @@ -253,7 +253,7 @@ Expected response: ```json { "status": "ok", - "server": "arduino-agent", + "server": "open-duino", "version": "0.5.0", "uptime": 123, "auth": true diff --git a/arduino-mcp-extension/bridge/README.md b/arduino-mcp-extension/bridge/README.md index 5cf3d8e6b48..c741c1bbb78 100644 --- a/arduino-mcp-extension/bridge/README.md +++ b/arduino-mcp-extension/bridge/README.md @@ -1,6 +1,6 @@ -# Arduino Agent MCP bridge +# Open Duino MCP bridge -A tiny stdio ⇄ HTTP bridge for the MCP server embedded in Arduino Agent. +A tiny stdio ⇄ HTTP bridge for the MCP server embedded in Open Duino. ## Why @@ -30,7 +30,23 @@ is closed: "arduino": { "command": "node", "args": ["/path/to/mcp-bridge/bridge/arduino-agent-bridge.js"], - "env": { "ARDUINO_AGENT_PATH": "/path/to/Arduino Agent executable" } + "env": { "ARDUINO_AGENT_PATH": "/path/to/Open Duino executable" } + } + } +} +``` + +OpenCode uses the same bridge in its local MCP configuration: + +```jsonc +{ + "mcp": { + "servers": { + "open-duino": { + "type": "local", + "command": ["node", "/path/to/arduino-agent-bridge.js"], + "environment": { "ARDUINO_AGENT_PATH": "/path/to/Open Duino executable" } + } } } } @@ -41,7 +57,7 @@ Where to find it: | Running from | Bridge path | |--------------|-------------| | Release, Windows / Linux | `/resources/mcp-bridge/bridge/arduino-agent-bridge.js` | -| Release, macOS | `Arduino Agent.app/Contents/Resources/mcp-bridge/bridge/arduino-agent-bridge.js` (`Arduino IDE.app` in 0.6.2) | +| Release, macOS | `Open Duino.app/Contents/Resources/mcp-bridge/bridge/arduino-agent-bridge.js` | | Source checkout | `arduino-mcp-extension/bridge/arduino-agent-bridge.js`, after `yarn build` in `arduino-mcp-extension` | Releases after v0.6.1 include the bridge; earlier zips don't, so use a checkout @@ -60,7 +76,7 @@ All optional, set as environment variables: |----------|---------|---------| | `ARDUINO_MCP_URL` | `http://127.0.0.1:3847/mcp` | MCP endpoint to forward to | | `ARDUINO_MCP_TOKEN` | *(reads the token file)* | Override the auth token | -| `ARDUINO_AGENT_PATH` | *(unset — never launches)* | Path to the IDE executable. When set, a tool call that needs the IDE while it is closed starts it, waits for its MCP server and then runs the call. Concurrent calls share one launch; if the IDE never answers, the bridge doesn't start it again until it has been seen running. A path written for a pre-0.7.0 release (`Arduino IDE.exe`, `Arduino IDE.app/...`) that no longer exists falls back to the renamed `Arduino Agent` executable. | +| `ARDUINO_AGENT_PATH` | *(unset — never launches)* | Path to the IDE executable. When set, a tool call that needs the IDE while it is closed starts it, waits for its MCP server and then runs the call. Concurrent calls share one launch; if the IDE never answers, the bridge doesn't start it again until it has been seen running. Paths from older Arduino IDE and Arduino Agent builds are migrated to Open Duino when possible. | | `ARDUINO_MCP_LAUNCH_TIMEOUT` | `120` | Seconds to wait for a started IDE's MCP server | | `ARDUINO_MCP_WATCH_INTERVAL` | `5` | Seconds between checks for the IDE coming up (a local TCP connect) | | `ARDUINO_MCP_DEBUG` | *(off)* | Set to `1` for verbose logging on stderr | @@ -72,8 +88,8 @@ Example on Windows: "mcpServers": { "arduino": { "command": "node", - "args": ["C:/Arduino Agent/resources/mcp-bridge/bridge/arduino-agent-bridge.js"], - "env": { "ARDUINO_AGENT_PATH": "C:/Arduino Agent/Arduino Agent.exe" } + "args": ["C:/Open Duino/resources/mcp-bridge/bridge/arduino-agent-bridge.js"], + "env": { "ARDUINO_AGENT_PATH": "C:/Open Duino/Open Duino.exe" } } } } diff --git a/arduino-mcp-extension/bridge/arduino-agent-bridge.js b/arduino-mcp-extension/bridge/arduino-agent-bridge.js index 598a521884c..6cb918a3e5a 100644 --- a/arduino-mcp-extension/bridge/arduino-agent-bridge.js +++ b/arduino-mcp-extension/bridge/arduino-agent-bridge.js @@ -90,7 +90,7 @@ const PROBE_TIMEOUT_MS = 1000; * KEEP IN SYNC with src/common/mcp-instructions.ts - the manual smoke test * compares the bridge's initialize.instructions with the HTTP server's. */ -const FALLBACK_INSTRUCTIONS = `Arduino Agent - an Arduino IDE with this MCP server embedded. You share one editor, one board and one serial monitor with the user; prefer these tools over asking the user to click in the IDE. +const FALLBACK_INSTRUCTIONS = `Open Duino - an Arduino IDE with this MCP server embedded. You share one editor, one board and one serial monitor with the user; prefer these tools over asking the user to click in the IDE. Recommended workflow: 1. arduino_context for current state (open sketch, selected board/port, connected boards with USB vid/pid). @@ -143,30 +143,31 @@ function sleep(ms) { } /** - * ARDUINO_AGENT_PATH, following the 0.7.0 rename of the executable from - * `Arduino IDE` to `Arduino Agent` (`Arduino IDE.exe`, or on macOS - * `Arduino IDE.app/Contents/MacOS/Arduino IDE`). When a path written for an - * older release no longer exists but the renamed one does, use that, so - * existing MCP configs keep working after an upgrade. + * ARDUINO_AGENT_PATH is retained for compatibility with existing MCP configs. + * If it names an old Arduino IDE or Arduino Agent executable, try the matching + * Open Duino path before giving up. */ function resolveAgentExecutable(exe) { - if (!exe || fs.existsSync(exe) || !exe.includes('Arduino IDE')) return exe; - const candidates = [ - path.join(path.dirname(exe), path.basename(exe).split('Arduino IDE').join('Arduino Agent')), - exe.split('Arduino IDE').join('Arduino Agent'), - ]; + if (!exe || fs.existsSync(exe)) return exe; + const candidates = []; + for (const oldName of ['Arduino IDE', 'Arduino Agent']) { + for (const newName of ['Open Duino', 'Arduino Agent']) { + const candidate = exe.split(oldName).join(newName); + if (candidate !== exe) candidates.push(candidate); + } + } const renamed = candidates.find((p) => fs.existsSync(p)); if (!renamed) return exe; - warn(`ARDUINO_AGENT_PATH points at ${exe}, which no longer exists; using ${renamed} instead (the executable was renamed in Arduino Agent 0.7.0 - update the setting)`); + warn(`ARDUINO_AGENT_PATH points at ${exe}, which no longer exists; using ${renamed} instead. Update the setting when convenient.`); return renamed; } /** The message shown when a tool needs the IDE and it is not reachable. */ function offlineMessage(detail) { const lines = [ - 'Arduino Agent is not running, so this Arduino tool cannot run right now.', + 'Open Duino is not running, so this Arduino tool cannot run right now.', '', - 'Open the Arduino Agent IDE and call the tool again - the connection ' + + 'Open the Open Duino IDE and call the tool again - the connection ' + 'recovers on its own; there is no need to restart this client.', ]; if (detail) { @@ -185,12 +186,12 @@ function offlineMessage(detail) { function upstreamErrorMessage(err) { if (err && err.status === 401) { return ( - 'Arduino Agent rejected the bridge\'s auth token. The IDE writes a fresh ' + + 'Open Duino rejected the bridge\'s auth token. The IDE writes a fresh ' + `token to ${TOKEN_FILE} when it starts; restart the IDE, or set ` + 'ARDUINO_MCP_TOKEN in this MCP server\'s config if you use a custom token.' ); } - return `Arduino Agent bridge error: ${err ? err.message : 'unknown error'}`; + return `Open Duino bridge error: ${err ? err.message : 'unknown error'}`; } // --------------------------------------------------------------------------- @@ -398,7 +399,7 @@ class Upstream { spawnError = err; }); child.unref(); - warn(`starting Arduino Agent: ${exe}`); + warn(`starting Open Duino: ${exe}`); } catch (err) { spawnError = err; } @@ -606,7 +607,7 @@ async function callTool(id, msg) { progressToken, progress: Math.round(elapsedMs / 1000), total: LAUNCH_TIMEOUT_MS / 1000, - message: 'Starting Arduino Agent...', + message: 'Starting Open Duino...', }, }); const launch = await upstream.launchAndWait(onWaiting); @@ -710,7 +711,7 @@ async function watchTick() { const tools = response?.result?.tools; if (!Array.isArray(tools)) return; upstreamTools = tools; - log('Arduino Agent is up'); + log('Open Duino is up'); if (advertised !== null && JSON.stringify(tools) !== advertised) { advertised = JSON.stringify(tools); log('tool list changed; notifying client'); @@ -744,7 +745,7 @@ process.stdin.on('data', (chunk) => { .catch((err) => { warn(`unhandled error: ${err.stack || err.message}`); if (msg && msg.id !== undefined && msg.id !== null) { - respondError(msg.id, -32603, `Arduino Agent bridge error: ${err.message}`); + respondError(msg.id, -32603, `Open Duino bridge error: ${err.message}`); } }); } diff --git a/arduino-mcp-extension/package.json b/arduino-mcp-extension/package.json index 0f626dece6f..c7826cf2a18 100644 --- a/arduino-mcp-extension/package.json +++ b/arduino-mcp-extension/package.json @@ -1,7 +1,7 @@ { "name": "arduino-mcp-extension", "version": "0.7.1", - "description": "MCP (Model Context Protocol) server embedded in Arduino IDE - HTTP/SSE transport", + "description": "Open Duino MCP server with OpenCode integration - HTTP/SSE transport", "license": "AGPL-3.0-or-later", "main": "lib/node/arduino-mcp-backend-module", "types": "lib/node/arduino-mcp-backend-module", @@ -10,6 +10,7 @@ "arduino", "mcp", "claude", + "opencode", "http", "sse" ], @@ -36,7 +37,7 @@ "build": "tsc", "test:bridge": "node --test test/bridge/bridge.test.js", "test:palette": "node --test test/theme/agent-palette.test.js", - "build:dev": "yarn build", + "build:dev": "npm run build", "watch": "tsc -w" } } diff --git a/arduino-mcp-extension/src/browser/agent-branding.ts b/arduino-mcp-extension/src/browser/agent-branding.ts index 7a8bcfdffb8..d631cd90318 100644 --- a/arduino-mcp-extension/src/browser/agent-branding.ts +++ b/arduino-mcp-extension/src/browser/agent-branding.ts @@ -1,5 +1,5 @@ /** - * Arduino Agent identity for the vendored Arduino IDE shell. + * Open Duino identity for the vendored Arduino IDE shell. * * arduino-ide-extension is upstream code synced by file copy, so the fork's * identity lives here as rebinds and contributions rather than as edits there. @@ -8,7 +8,7 @@ * so it would offer the official Arduino IDE as an "update" and replace this * app, MCP server included. Board and library update checks stay on: they * share the `arduino.checkForUpdates` preference, so that is left alone. - * - The About dialog names Arduino Agent, its own version and the Arduino IDE + * - The About dialog names Open Duino, its own version and the Arduino IDE * version it is based on, and says it is not an Arduino product. * * arduino-ide-extension ships no type declarations, so its classes are `any` @@ -89,7 +89,10 @@ export class AgentAbout extends About { `CLI Version: ${cliVersion}`, `Based on Arduino IDE ${versionOf('arduino-ide-extension') ?? ''}`, '', - 'Arduino Agent is an independent fork of the Arduino IDE. It is not affiliated with or endorsed by Arduino SA.', + 'Created by Lanuzadev', + 'Repository: https://github.com/lanuzadev/Open-Duino', + '', + 'Open Duino is an independent fork of the Arduino IDE. It is not affiliated with or endorsed by Arduino SA.', `Arduino IDE: Copyright © ${new Date().getFullYear()} Arduino s.r.l. and/or its affiliated companies`, ].join('\n'); @@ -97,8 +100,8 @@ export class AgentAbout extends About { const copy = 'Copy'; const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy]; const { response } = await this.agentDialogService.showMessageBox({ - message: 'Arduino Agent', - title: 'Arduino Agent', + message: 'Open Duino', + title: 'Open Duino', type: 'info', detail, buttons, diff --git a/arduino-mcp-extension/src/browser/agent-theme.ts b/arduino-mcp-extension/src/browser/agent-theme.ts index 7687ac71c25..da4423a3f1c 100644 --- a/arduino-mcp-extension/src/browser/agent-theme.ts +++ b/arduino-mcp-extension/src/browser/agent-theme.ts @@ -1,6 +1,6 @@ /** - * Arduino Agent's colors: Arduino's light and dark themes with their teal moved - * to the indigo of the app icon, so the IDE looks like its own app. + * Open Duino's colors: adapted light and dark themes with the app's accent color, + * keeping familiar IDE colors while giving the shell its own identity. * * The themes keep Arduino's ids (`arduino-theme`, `arduino-theme-dark`): the * vendored settings dialog lists exactly those as the built-in Light/Dark, and @@ -40,13 +40,13 @@ export class AgentMonacoThemingService extends MonacoThemingService { const registry = this as unknown as ParsedThemeRegistry; registry.registerParsedTheme({ id: light.id, - label: light.label, + label: 'Open Duino Light', uiTheme: 'vs', json: toAgentPalette(require('arduino-ide-extension/src/browser/data/default.color-theme.json')), }); registry.registerParsedTheme({ id: dark.id, - label: dark.label, + label: 'Open Duino Dark', uiTheme: 'vs-dark', json: toAgentPalette(require('arduino-ide-extension/src/browser/data/dark.color-theme.json')), }); @@ -54,7 +54,7 @@ export class AgentMonacoThemingService extends MonacoThemingService { } /** - * Registers `arduino.branding.primary`/`secondary`. Arduino's themes set them and + * Registers `arduino.branding.primary`/`secondary`. The themes set them and * its CSS uses `var(--theia-arduino-branding-primary)` (boards dialog, library and * board lists, serial monitor scrollbar, progress bar), but upstream never * registers them, so the variable was undefined and those accents never showed. @@ -68,12 +68,12 @@ export class AgentThemeStyles implements FrontendApplicationContribution, ColorC { id: 'arduino.branding.primary', defaults: { light: '#5d64c6', dark: '#757ff4', hcLight: 'focusBorder', hcDark: 'focusBorder' }, - description: 'Arduino Agent accent color (links, highlights, the serial monitor scrollbar).', + description: 'Open Duino accent color (links, highlights, the serial monitor scrollbar).', }, { id: 'arduino.branding.secondary', defaults: { light: '#bfc3d2', dark: '#bfc3d2', hcLight: 'foreground', hcDark: 'foreground' }, - description: 'Arduino Agent secondary branding color.', + description: 'Open Duino secondary branding color.', } ); } @@ -92,6 +92,95 @@ const brand = (percent: number) => const AGENT_THEME_CSS = ` :root { --arduino-shadow-focus: 0 0 0 3px ${brand(25)}; + --openduino-control-height: 28px; + --openduino-panel-divider: var(--theia-sideBar-border, var(--theia-editorWidget-border)); + --arduino-radius-sm: 3px; + --arduino-radius-md: 4px; + --arduino-radius-lg: 6px; + --arduino-radius-pill: 4px; + --arduino-shadow-sm: none; + --arduino-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08); + --arduino-shadow-lg: 0 6px 20px rgba(0, 0, 0, 0.12); +} +body { + font-family: var(--theia-ui-font-family, system-ui, sans-serif); + font-size: 12px; + letter-spacing: 0; +} +#theia-top-panel { + border-bottom: 1px solid var(--openduino-panel-divider); +} +#theia-statusBar { + min-height: 27px; + border-top: 1px solid var(--openduino-panel-divider); + font-size: 10px; +} +#theia-left-content-panel, +#theia-right-content-panel { + background: var(--theia-sideBar-background, var(--theia-editor-background)); +} +.p-Widget.p-DockPanel-tabBar { + border-bottom: 1px solid var(--openduino-panel-divider); + background: var(--theia-sideBar-background, var(--theia-editor-background)); +} +.p-TabBar-tab { + min-height: 34px; + border-radius: 0; + font-size: 11px; +} +.p-TabBar-tab.p-mod-current { + box-shadow: inset 0 -2px var(--theia-arduino-branding-primary); +} +.p-TabBar-tab:hover:not(.p-mod-current) { + background: ${brand(7)}; +} +.theia-sidepanel-title, +.theia-sidepanel-toolbar { + font-size: 11px; +} +.theia-sidepanel-title { + font-weight: 600; + letter-spacing: .02em; +} +.theia-tree .theia-TreeNode, +.theia-TreeNode { + min-height: 25px; +} +.theia-tree .theia-TreeNode:hover, +.theia-TreeNode:hover { + background: ${brand(7)}; +} +.theia-tree .theia-TreeNode.theia-mod-selected, +.theia-TreeNode.theia-mod-selected { + background: ${brand(13)}; +} +.theia-input, +input[type="text"], +input[type="number"], +select, +textarea { + min-height: var(--openduino-control-height); + border-radius: var(--arduino-radius-md); + box-shadow: none; +} +.p-Widget.dialogOverlay .dialogBlock { + border-radius: var(--arduino-radius-lg); + box-shadow: var(--arduino-shadow-lg); +} +.theia-dialog-title { + font-size: 13px; + font-weight: 600; +} +::-webkit-scrollbar { width: 9px; height: 9px; } +::-webkit-scrollbar-thumb { + border: 2px solid transparent; + border-radius: 8px; + background: color-mix(in srgb, var(--theia-foreground) 18%, transparent); + background-clip: padding-box; +} +::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--theia-foreground) 30%, transparent); + background-clip: padding-box; } #theia-statusBar .area .element.arduino-selected-port, #theia-statusBar .area .element.arduino-selected-board { @@ -107,4 +196,29 @@ const AGENT_THEME_CSS = ` .component-list-item { border-bottom-color: ${brand(8)}; } +button.theia-button, +.theia-button { + border-width: 1px; + border-radius: var(--arduino-radius-md); + box-shadow: none; + font-family: var(--theia-ui-font-family); + font-size: 12px; + font-weight: 500; + letter-spacing: 0; + text-transform: none; +} +button.theia-button:hover:not([disabled]) { + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); + transform: none; +} +.theia-input, +input[type="text"], +input[type="number"] { + border-radius: var(--arduino-radius-md); +} +body.theia-dark { + --arduino-shadow-sm: none; + --arduino-shadow-md: 0 2px 10px rgba(0, 0, 0, 0.24); + --arduino-shadow-lg: 0 6px 24px rgba(0, 0, 0, 0.32); +} `; diff --git a/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts b/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts index b50bdb97675..a045b31d1e7 100644 --- a/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts +++ b/arduino-mcp-extension/src/browser/arduino-mcp-frontend-module.ts @@ -27,14 +27,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Bind MCP preferences to add settings to the IDE preferences panel bindMCPPreferences(bind); - // Arduino Agent identity: no Arduino IDE updater, fork-specific About dialog + // Open Duino identity: no Arduino IDE updater, fork-specific About dialog rebind(CheckForIDEUpdates).to(DisabledIDEUpdateCheck).inSingletonScope(); rebind(About).to(AgentAbout).inSingletonScope(); bind(RemoveIDEUpdaterCommand).toSelf().inSingletonScope(); bind(CommandContribution).toService(RemoveIDEUpdaterCommand); bind(MenuContribution).toService(RemoveIDEUpdaterCommand); - // Arduino Agent colors: Arduino's themes recolored teal -> indigo (agent-theme.ts) + // Open Duino colors: adapted light and dark themes (agent-theme.ts) rebind(MonacoThemingService).to(AgentMonacoThemingService).inSingletonScope(); bind(AgentThemeStyles).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(AgentThemeStyles); diff --git a/arduino-mcp-extension/src/browser/connect-agent.ts b/arduino-mcp-extension/src/browser/connect-agent.ts index fe8b7e8c241..1b9178919b1 100644 --- a/arduino-mcp-extension/src/browser/connect-agent.ts +++ b/arduino-mcp-extension/src/browser/connect-agent.ts @@ -1,6 +1,6 @@ /** - * "Connect an AI Agent": the in-IDE way to hook up Claude Code, Claude Desktop - * or any MCP client. + * "Connect an AI Agent": the in-IDE way to hook up OpenCode, Claude Code, + * Claude Desktop or any MCP client. * * - Help > Connect an AI Agent... opens a dialog with copy-ready setups for this * installation (real paths of the bundled bridge and of the app, the token). @@ -19,6 +19,12 @@ import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/c import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MessageService } from '@theia/core/lib/common/message-service'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import URI from '@theia/core/lib/common/uri'; +import { + CurrentSketch, + SketchesServiceClientImpl, +} from 'arduino-ide-extension/lib/browser/sketches-service-client-impl'; import { MCPConnectionGuide, MCPConnectionSnippet, @@ -31,7 +37,12 @@ export namespace ConnectAgentCommands { export const OPEN: Command = { id: 'arduino-agent.connect-ai-agent', label: 'Connect an AI Agent...', - category: 'Arduino Agent', + category: 'Open Duino', + }; + export const OPEN_OPENCODE: Command = { + id: 'open-duino.open-opencode-chat', + label: 'OpenCode Chat', + category: 'Open Duino', }; } @@ -61,10 +72,19 @@ export class ConnectAgentContribution @inject(StorageService) private readonly storage!: StorageService; + @inject(TerminalService) + private readonly terminalService!: TerminalService; + + @inject(SketchesServiceClientImpl) + private readonly sketchesServiceClient!: SketchesServiceClientImpl; + registerCommands(registry: CommandRegistry): void { registry.registerCommand(ConnectAgentCommands.OPEN, { execute: () => this.openDialog(), }); + registry.registerCommand(ConnectAgentCommands.OPEN_OPENCODE, { + execute: () => this.openOpenCodeChat(), + }); } registerMenus(registry: MenuModelRegistry): void { @@ -74,6 +94,11 @@ export class ConnectAgentContribution label: ConnectAgentCommands.OPEN.label, order: '0', }); + registry.registerMenuAction([...CommonMenus.HELP, '0_agent'], { + commandId: ConnectAgentCommands.OPEN_OPENCODE.id, + label: ConnectAgentCommands.OPEN_OPENCODE.label, + order: '1', + }); } onStart(): void { @@ -109,8 +134,8 @@ export class ConnectAgentContribution // last 5 minutes (each Claude session's bridge counts once it fetches tools). text = n === 1 ? '$(hubot) Agent active' : `$(hubot) ${n} agents active`; tooltip = - `${n} MCP client${n === 1 ? '' : 's'} used Arduino Agent in the last 5 minutes ` + - `(each AI session with Arduino Agent configured counts once) on ${url}. Click for connection setup.`; + `${n} MCP client${n === 1 ? '' : 's'} used Open Duino in the last 5 minutes ` + + `(each AI session with Open Duino configured counts once) on ${url}. Click for connection setup.`; } else { text = '$(hubot) MCP ready'; tooltip = `MCP server listening on ${url}. Click to connect an AI agent.`; @@ -132,7 +157,7 @@ export class ConnectAgentContribution await this.storage.setData(HINT_SHOWN_KEY, true); const show = 'Show me how'; const choice = await this.messageService.info( - 'Arduino Agent is ready for your AI assistant. Connect Claude Code, Claude Desktop or another MCP client in one step.', + 'Open Duino is ready for your AI assistant. Connect OpenCode, Claude Code or another MCP client in one step.', show ); if (choice === show) { @@ -145,6 +170,51 @@ export class ConnectAgentContribution this.showStatus(guide.status); await new ConnectAgentDialog(guide, this.clipboard).open(); } + + /** Opens OpenCode's TUI in the integrated terminal beside the sketch. */ + private async openOpenCodeChat(): Promise { + try { + const sketch = this.sketchesServiceClient.tryGetCurrentSketch(); + const sketchDir = CurrentSketch.isValid(sketch) + ? new URI(sketch.uri).parent + : undefined; + const isWindows = /^win/i.test(navigator.platform); + const windowsCwd = sketchDir ? windowsPathFromFileUri(sketchDir.toString()) : undefined; + const terminal = await this.terminalService.newTerminal({ + title: 'OpenCode · Open Duino', + ...(isWindows + ? { shellPath: 'cmd.exe', shellArgs: ['/d', '/k'] } + : { cwd: sketchDir?.toString() }), + }); + await terminal.start(); + this.terminalService.open(terminal); + terminal.sendText( + isWindows + ? `cd /d "${windowsCwd ?? '%USERPROFILE%'}" && opencode\r` + : 'opencode\r' + ); + } catch (error) { + this.messageService.error(`No se pudo iniciar OpenCode en la terminal integrada: ${String(error)}`); + } + } + +} + +/** OpenCode's core/instructions source recurses when Windows path casing + * differs from Git's project root. Start cmd explicitly and `cd /d` using the + * canonical uppercase drive before launching OpenCode. */ +function windowsPathFromFileUri(uri: string): string | undefined { + let decoded: string; + try { + decoded = decodeURIComponent(uri); + } catch { + return undefined; + } + const match = decoded.match(/^file:\/\/\/([a-z]):\/(.*)$/i); + if (!match) { + return undefined; + } + return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, '\\')}`; } class ConnectAgentDialog extends AbstractDialog { diff --git a/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts b/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts index 3f37e47d51d..30f5269f059 100644 --- a/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts +++ b/arduino-mcp-extension/src/browser/mcp-frontend-contribution.ts @@ -75,7 +75,7 @@ export class MCPFrontendContribution break; case 'arduino.mcp.requireAuth': this.messageService.info( - 'The MCP authentication setting takes effect after restarting Arduino Agent.', + 'The MCP authentication setting takes effect after restarting Open Duino.', { timeout: 8000 } ); break; @@ -193,7 +193,7 @@ export class MCPFrontendContribution const fileName = event.uri.split('/').pop() || event.uri; const action = event.type === 'created' ? 'Created' : 'Updated'; this.messageService.info( - `${action} by Claude: ${fileName}`, + `${action} by an AI agent: ${fileName}`, { timeout: 3000 } ); } @@ -237,9 +237,8 @@ export class MCPFrontendContribution await this.mcpService.setEnabled(enabled); if (enabled) { - const config = await this.mcpService.getClientConfig(); this.messageService.info( - `MCP server enabled. Claude Code configuration:\n${config}`, + 'MCP server enabled. Use Help > Connect an AI Agent to link a client.', { timeout: 15000 } ); } else { @@ -260,7 +259,7 @@ export class MCPFrontendContribution ? 'Router mode (4 meta-tools)' : 'Direct mode (all tools)'; this.messageService.info( - `MCP tool mode changed to: ${modeDesc}. Reconnect Claude Code to apply.`, + `MCP tool mode changed to: ${modeDesc}. Reconnect your AI client to apply.`, { timeout: 5000 } ); } catch (error) { @@ -275,9 +274,8 @@ export class MCPFrontendContribution } try { await this.mcpService.setPort(port); - const config = await this.mcpService.getClientConfig(); this.messageService.info( - `MCP server moved to port ${port}. Updated Claude Code configuration:\n${config}`, + `MCP server moved to port ${port}. Refresh the client setup from Help > Connect an AI Agent.`, { timeout: 15000 } ); } catch (error) { diff --git a/arduino-mcp-extension/src/browser/mcp-preferences.ts b/arduino-mcp-extension/src/browser/mcp-preferences.ts index 20ed4074eb0..3b976b8444f 100644 --- a/arduino-mcp-extension/src/browser/mcp-preferences.ts +++ b/arduino-mcp-extension/src/browser/mcp-preferences.ts @@ -50,7 +50,7 @@ export const mcpPreferenceSchema: PreferenceSchema = { type: 'boolean', description: nls.localize( 'arduino/mcp/preferences.enabled', - 'Enable MCP (Model Context Protocol) server integration. When enabled, AI assistants like Claude Code can interact with Arduino Agent programmatically - compiling, uploading, managing libraries, and more.' + 'Enable MCP (Model Context Protocol) server integration. When enabled, AI assistants like OpenCode can interact with Open Duino - compiling, uploading, managing libraries, and more.' ), default: MCPConfigurationDefaults['arduino.mcp.enabled'], order: 1, @@ -59,7 +59,7 @@ export const mcpPreferenceSchema: PreferenceSchema = { type: 'boolean', description: nls.localize( 'arduino/mcp/preferences.autoConnect', - 'Automatically start the MCP server when Arduino Agent launches. Disable this if you want to manually control when the MCP server is available.' + 'Automatically start the MCP server when Open Duino launches. Disable this if you want to manually control when the MCP server is available.' ), default: MCPConfigurationDefaults['arduino.mcp.autoConnect'], order: 2, @@ -68,7 +68,7 @@ export const mcpPreferenceSchema: PreferenceSchema = { type: 'number', description: nls.localize( 'arduino/mcp/preferences.port', - 'HTTP port for the MCP server. Claude Code connects to http://127.0.0.1:{port}/mcp. Change this if port 3847 is already in use. The server restarts on the new port immediately.' + 'HTTP port for the MCP server. MCP clients connect to http://127.0.0.1:{port}/mcp. Change this if port 3847 is already in use. The server restarts on the new port immediately.' ), default: MCPConfigurationDefaults['arduino.mcp.port'], minimum: 1024, diff --git a/arduino-mcp-extension/src/common/mcp-instructions.ts b/arduino-mcp-extension/src/common/mcp-instructions.ts index b17fd88e23e..073b2aa451b 100644 --- a/arduino-mcp-extension/src/common/mcp-instructions.ts +++ b/arduino-mcp-extension/src/common/mcp-instructions.ts @@ -10,7 +10,7 @@ * (the stdio bridge must answer initialize locally when the IDE is closed). * The manual smoke test asserts the two stay identical. */ -export const MCP_SERVER_INSTRUCTIONS = `Arduino Agent - an Arduino IDE with this MCP server embedded. You share one editor, one board and one serial monitor with the user; prefer these tools over asking the user to click in the IDE. +export const MCP_SERVER_INSTRUCTIONS = `Open Duino - an Arduino IDE with this MCP server embedded. You share one editor, one board and one serial monitor with the user; prefer these tools over asking the user to click in the IDE. Recommended workflow: 1. arduino_context for current state (open sketch, selected board/port, connected boards with USB vid/pid). diff --git a/arduino-mcp-extension/src/common/mcp-service.ts b/arduino-mcp-extension/src/common/mcp-service.ts index 3b5201117cd..5b0440d2e73 100644 --- a/arduino-mcp-extension/src/common/mcp-service.ts +++ b/arduino-mcp-extension/src/common/mcp-service.ts @@ -33,7 +33,7 @@ export const ACTIVE_CLIENT_WINDOW_MS = 5 * 60 * 1000; /** One ready-to-paste way of connecting a client. */ export interface MCPConnectionSnippet { - id: 'claude-code' | 'bridge-config' | 'http-config'; + id: 'claude-code' | 'opencode-cli' | 'opencode-config' | 'bridge-config' | 'http-config'; title: string; description: string; text: string; diff --git a/arduino-mcp-extension/src/node/mcp-server.ts b/arduino-mcp-extension/src/node/mcp-server.ts index 7ea00e3e48a..48b510ef5ff 100644 --- a/arduino-mcp-extension/src/node/mcp-server.ts +++ b/arduino-mcp-extension/src/node/mcp-server.ts @@ -492,7 +492,7 @@ export class ArduinoMCPServer { `Port ${this.port} is in use. Change the arduino.mcp.port preference or set the ARDUINO_MCP_PORT env var.` ); this.startError = - `Port ${this.port} is already in use, probably by another running copy of Arduino Agent. ` + + `Port ${this.port} is already in use, probably by another running copy of Open Duino. ` + 'Close it, or choose another port in Preferences > MCP.'; } else { this.startError = `The MCP server could not start: ${err.message}`; @@ -509,7 +509,7 @@ export class ArduinoMCPServer { this.requireAuth ? 'required' : 'DISABLED' })` ); - mcpLog.info('Claude Code config:\n' + this.buildClientConfig()); + mcpLog.info('MCP client config:\n' + this.buildClientConfig()); resolve(); }); }); @@ -705,7 +705,7 @@ export class ArduinoMCPServer { res.end( JSON.stringify({ status: 'ok', - server: 'arduino-agent', + server: 'open-duino', version: EXTENSION_VERSION, uptime: Math.floor((Date.now() - this.startTime) / 1000), auth: this.requireAuth, @@ -719,7 +719,7 @@ export class ArduinoMCPServer { res.end( JSON.stringify({ error: - 'Unauthorized. Pass the MCP token as an "Authorization: Bearer " header. The token is shown in Arduino Agent (Preferences > MCP) and stored in ~/.arduinoIDE/mcp-token.', + 'Unauthorized. Pass the MCP token as an "Authorization: Bearer " header. The token is shown in Open Duino (Preferences > MCP) and stored in ~/.arduinoIDE/mcp-token.', }) ); return; @@ -847,7 +847,7 @@ export class ArduinoMCPServer { */ private createMCPServerInstance(): Server { const server = new Server( - { name: 'arduino-agent', version: EXTENSION_VERSION }, + { name: 'open-duino', version: EXTENSION_VERSION }, { capabilities: { tools: {}, prompts: {} }, // Workflow guidance delivered to every client at initialize. diff --git a/arduino-mcp-extension/src/node/mcp-service-impl.ts b/arduino-mcp-extension/src/node/mcp-service-impl.ts index aa346ac4ebf..1fa5c8df978 100644 --- a/arduino-mcp-extension/src/node/mcp-service-impl.ts +++ b/arduino-mcp-extension/src/node/mcp-service-impl.ts @@ -46,7 +46,8 @@ function findBridge(): string | null { /** The app executable (for ARDUINO_AGENT_PATH), or null when running from a dev Electron. */ function findAppExecutable(): string | null { - return /^arduino[ -](agent|ide)(\.exe)?$/i.test(path.basename(process.execPath)) + const executableName = path.basename(process.execPath); + return /^(?:arduino[ -](?:agent|ide)|open[ -]?duino)(?:\.exe)?$/i.test(executableName) ? process.execPath : null; } @@ -121,11 +122,38 @@ export class MCPServiceImpl implements MCPService { if (bridgePath) { const env = appExecutable ? ` --env ARDUINO_AGENT_PATH=${quote(appExecutable)}` : ''; + const openCodeEnvironment = appExecutable ? { ARDUINO_AGENT_PATH: appExecutable } : undefined; + const openCodeServer: Record = { + type: 'local', + command: ['node', bridgePath], + }; + if (openCodeEnvironment) { + openCodeServer.environment = openCodeEnvironment; + } + snippets.push({ + id: 'opencode-cli', + title: 'OpenCode command', + description: + 'Run this in a terminal to add Open Duino to your global OpenCode MCP configuration. ' + + 'The bundled bridge can start the IDE when a tool is called (requires Node.js 18 or newer).', + text: `opencode mcp add open-duino --global${env} -- node ${quote(bridgePath)}`, + }); + snippets.push({ + id: 'opencode-config', + title: 'OpenCode configuration', + description: + 'Merge this MCP entry into opencode.json or opencode.jsonc. The bridge keeps Open Duino and OpenCode connected.', + text: JSON.stringify( + { mcp: { servers: { 'open-duino': openCodeServer } } }, + null, + 2 + ), + }); snippets.push({ id: 'claude-code', title: 'Claude Code', description: - 'Run this in a terminal. It adds Arduino Agent to Claude Code for all your projects, ' + + 'Run this in a terminal. It adds Open Duino to Claude Code for all your projects, ' + 'through the bundled bridge (needs Node.js 18 or newer).', text: `claude mcp add arduino --scope user${env} -- node ${quote(bridgePath)}`, }); @@ -138,24 +166,53 @@ export class MCPServiceImpl implements MCPService { title: 'Claude Desktop, Cursor and other MCP clients', description: 'Add this to the client\'s MCP configuration (claude_desktop_config.json, .mcp.json, ...). ' + - 'The bundled bridge stays connected while Arduino Agent is closed and starts it when a ' + + 'The bundled bridge stays connected while Open Duino is closed and starts it when a ' + 'tool needs it. Needs Node.js 18 or newer.', text: JSON.stringify({ mcpServers: { arduino: server } }, null, 2), }); + } else { const header = token ? ` --header ${quote(`Authorization: Bearer ${token}`)}` : ''; snippets.push({ id: 'claude-code', title: 'Claude Code', - description: 'Run this in a terminal. It connects while Arduino Agent is running.', + description: 'Run this in a terminal. It connects while Open Duino is running.', text: `claude mcp add arduino --scope user --transport http ${url}${header}`, }); + snippets.push({ + id: 'opencode-cli', + title: 'OpenCode command', + description: 'Run this in a terminal to add the running Open Duino MCP server to OpenCode globally.', + text: + `opencode mcp add open-duino --global --url ${url}` + + (token ? ` --header ${quote(`Authorization=Bearer ${token}`)}` : ''), + }); + snippets.push({ + id: 'opencode-config', + title: 'OpenCode configuration', + description: 'Merge this entry under mcp.servers in opencode.json or opencode.jsonc.', + text: JSON.stringify( + { + mcp: { + servers: { + 'open-duino': { + type: 'remote', + url, + ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}), + }, + }, + }, + }, + null, + 2 + ), + }); } snippets.push({ id: 'http-config', title: 'Direct HTTP', description: - 'For clients that speak Streamable HTTP. Only connects while Arduino Agent is running' + + 'For clients that speak Streamable HTTP. Only connects while Open Duino is running' + (token ? '; the token is stored in ~/.arduinoIDE/mcp-token.' : '.'), text: this.mcpServer.buildClientConfig(), }); diff --git a/docs/BUILDING-WINDOWS.md b/docs/BUILDING-WINDOWS.md index 94221bc1b00..6be796dcf8b 100644 --- a/docs/BUILDING-WINDOWS.md +++ b/docs/BUILDING-WINDOWS.md @@ -44,8 +44,8 @@ Studio Installer (Individual Components tab). ## Build steps ```bash -git clone https://github.com/mixelpixx/arduino-mcp.git -cd arduino-mcp +git clone https://github.com/lanuzadev/Open-Duino.git +cd Arduino-Agent corepack enable yarn install # applies the bundled node-pty patch automatically @@ -74,12 +74,12 @@ yarn exec theia build --config webpack.config.js --mode production # wrong user-data folder and skips its bundled plugins. node ../node_modules/electron-builder/cli.js --dir --publish never \ -c.electronVersion=30.1.2 \ - -c.extraMetadata.name=arduino-agent \ + -c.extraMetadata.name=open-duino \ -c.extraMetadata.main=./arduino-ide-electron-main.js \ -c.extraMetadata.theia.frontend.config.buildDate="$(date -u +%Y-%m-%dT%H:%M:%SZ)" ``` -The MCP server starts automatically at `http://127.0.0.1:3847` when the IDE +Open Duino's MCP server starts automatically at `http://127.0.0.1:3847` when the IDE launches; the console prints a ready-to-paste client configuration including the auth token (stored in `~/.arduinoIDE/mcp-token`). diff --git a/docs/development.md b/docs/development.md index b6870e8912d..9be572f0710 100644 --- a/docs/development.md +++ b/docs/development.md @@ -200,6 +200,6 @@ This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide [Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at: - - Windows: `C:\path\to\Arduino Agent\resources\app\lib\backend\resources\arduino-cli.exe`, - - macOS: `/path/to/Arduino Agent.app/Contents/Resources/app/lib/backend/resources/arduino-cli`, and - - Linux: `/path/to/Arduino Agent/resources/app/lib/backend/resources/arduino-cli`. + - Windows: `C:\path\to\Open Duino\resources\app\lib\backend\resources\arduino-cli.exe`, + - macOS: `/path/to/Open Duino.app/Contents/Resources/app/lib/backend/resources/arduino-cli`, and + - Linux: `/path/to/Open Duino/resources/app/lib/backend/resources/arduino-cli`. diff --git a/docs/images/open-duino-neon.png b/docs/images/open-duino-neon.png new file mode 100644 index 00000000000..a0503e4b915 Binary files /dev/null and b/docs/images/open-duino-neon.png differ diff --git a/electron-app/arduino-ide-electron-main.js b/electron-app/arduino-ide-electron-main.js index eca93548e3d..a88e97604f2 100644 --- a/electron-app/arduino-ide-electron-main.js +++ b/electron-app/arduino-ide-electron-main.js @@ -7,8 +7,8 @@ const path = require('path'); const config = require('./package.json').theia.frontend.config; /** - * Since 0.7.0 Arduino Agent keeps its Electron user data in its own folder - * (`arduino-agent`, from the package name) instead of the official Arduino + * Open Duino keeps its Electron user data in its own folder + * (`open-duino`, from the package name) instead of the official Arduino * IDE's (`arduino-ide`), so the two no longer share a single-instance lock or * window state. On first start, carry over what lived in the shared folder: * the shell layout and per-sketch board selections (Local Storage, IndexedDB) diff --git a/electron-app/package.json b/electron-app/package.json index 70b9b6a567f..be0bf028cce 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -46,7 +46,7 @@ "prepare": "theia download:plugins", "prebuild": "rimraf lib", "build": "theia build", - "prebuild:dev": "yarn prebuild", + "prebuild:dev": "npm run prebuild", "build:dev": "theia build --config webpack.dev.js --mode development", "test": "mocha \"./test/**/*.test.js\"", "start": "theia start --plugins=local-dir:../plugins", @@ -60,7 +60,7 @@ "target": "electron", "frontend": { "config": { - "applicationName": "Arduino Agent", + "applicationName": "Open Duino", "defaultTheme": { "light": "arduino-theme", "dark": "arduino-theme-dark" @@ -69,7 +69,7 @@ "validatePreferencesSchema": false, "electron": { "showWindowEarly": true, - "uriScheme": "arduino-agent" + "uriScheme": "openduino" }, "reloadOnReconnect": true, "preferences": { @@ -103,11 +103,15 @@ } } }, - "author": "mixelpixx", - "description": "Arduino Agent: the Arduino IDE with a built-in MCP server", + "author": "Lanuzadev", + "repository": { + "type": "git", + "url": "https://github.com/lanuzadev/Open-Duino.git" + }, + "description": "Open Duino: an AI-assisted Arduino development environment", "build": { - "appId": "io.github.mixelpixx.arduino-agent", - "productName": "Arduino Agent", + "appId": "org.openduino.desktop", + "productName": "Open Duino", "asar": false, "detectUpdateChannel": false, "generateUpdatesFilesForAllChannels": true, @@ -152,6 +156,9 @@ } ], "win": { + "icon": "resources/icon.ico", + "publisherName": "Lanuzadev", + "signAndEditExecutable": false, "target": [ "msi", "nsis", @@ -210,14 +217,7 @@ } ] }, - "afterSign": "scripts/notarize.js", - "publish": [ - { - "provider": "s3", - "bucket": "arduino-downloads-prod-beagle", - "path": "arduino-ide/nightly" - } - ] + "afterSign": "scripts/notarize.js" }, "theiaPluginsDir": "plugins", "theiaPlugins": { diff --git a/electron-app/resources/icon.ico b/electron-app/resources/icon.ico index 0b660545cc1..d489c12d581 100644 Binary files a/electron-app/resources/icon.ico and b/electron-app/resources/icon.ico differ diff --git a/electron-app/resources/icons/512x512.png b/electron-app/resources/icons/512x512.png index 2ceb017d3dc..afbb6441eba 100644 Binary files a/electron-app/resources/icons/512x512.png and b/electron-app/resources/icons/512x512.png differ diff --git a/electron-app/resources/installerSidebar.bmp b/electron-app/resources/installerSidebar.bmp index ef5e2a6a009..8dbe4022e64 100644 Binary files a/electron-app/resources/installerSidebar.bmp and b/electron-app/resources/installerSidebar.bmp differ diff --git a/electron-app/resources/open-duino-icon.png b/electron-app/resources/open-duino-icon.png new file mode 100644 index 00000000000..3ed878e18ec Binary files /dev/null and b/electron-app/resources/open-duino-icon.png differ diff --git a/electron-app/resources/preload.html b/electron-app/resources/preload.html index f7b272c210b..59e6ae3239c 100644 --- a/electron-app/resources/preload.html +++ b/electron-app/resources/preload.html @@ -3,14 +3,14 @@ /* The colors are hard-coded and based on the `editor.background` and `editor.foreground` values from `./arduino-ide-extension/src/browser/data/default.color-theme.json` */ @media (prefers-color-scheme: light) { html { - background: #ffffff; + background: #0a1220; } } /* The colors are hard-coded and based on the `editor.background` and `editor.foreground` values from `./arduino-ide-extension/src/browser/data/dark.color-theme.json` */ @media (prefers-color-scheme: dark) { html { - background: #1f272a; + background: #0a1220; } } @@ -74,17 +74,17 @@
- - - - - - - - - - - + + + + + + + + + + +
diff --git a/electron-app/scripts/package.js b/electron-app/scripts/package.js index 4e036e4a03d..fe18e88e273 100644 --- a/electron-app/scripts/package.js +++ b/electron-app/scripts/package.js @@ -27,9 +27,9 @@ async function run() { electronVersion, '-c.extraMetadata.version', version, - // Arduino Agent: its own `name`, so its userData (localStorage, single-instance lock) is separate from the official IDE. arduino-ide-electron-main.js migrates the old data once. + // Open Duino: its own app data folder and single-instance lock, separate from the official Arduino IDE. '-c.extraMetadata.name', - 'arduino-agent', + 'open-duino', `-c.${platform}.artifactName`, artifactName, '-c.extraMetadata.theia.frontend.config.appVersion', @@ -51,6 +51,7 @@ async function run() { // updateChannel // ); } + args.push(...process.argv.slice(2)); const cp = exec('electron-builder', args, { stdio: 'inherit' }); await cp; } @@ -102,7 +103,7 @@ async function getVersion() { async function getArtifactName(version) { const { platform, arch } = process; version = isNightly ? `nightly-${await timestamp()}` : version; - const name = 'arduino-agent'; + const name = 'open-duino'; switch (platform) { case 'win32': { if (arch === 'x64') { diff --git a/electron-app/scripts/post-package.js b/electron-app/scripts/post-package.js index fd90a7f33f3..15e0c4b8a3f 100644 --- a/electron-app/scripts/post-package.js +++ b/electron-app/scripts/post-package.js @@ -131,7 +131,7 @@ async function copyFilesToBuildArtifacts() { case 'linux': { filesToCopy.push( ...glob - .sync('**/arduino-agent*.{zip,AppImage}', { cwd }) + .sync('**/open-duino*.{zip,AppImage}', { cwd }) .map((p) => path.join(cwd, p)) ); break; @@ -139,7 +139,7 @@ async function copyFilesToBuildArtifacts() { case 'win32': { filesToCopy.push( ...glob - .sync('**/arduino-agent*.{exe,msi,zip}', { cwd }) + .sync('**/open-duino*.{exe,msi,zip}', { cwd }) .map((p) => path.join(cwd, p)) ); break; @@ -147,7 +147,7 @@ async function copyFilesToBuildArtifacts() { case 'darwin': { filesToCopy.push( ...glob - .sync('**/arduino-agent*.{dmg,zip}', { cwd }) + .sync('**/open-duino*.{dmg,zip}', { cwd }) .map((p) => path.join(cwd, p)) ); break; diff --git a/electron-app/scripts/windowsCustomSign.js b/electron-app/scripts/windowsCustomSign.js index d7600509b5c..0525ffff0fd 100644 --- a/electron-app/scripts/windowsCustomSign.js +++ b/electron-app/scripts/windowsCustomSign.js @@ -18,7 +18,7 @@ exports.default = async function (configuration) { CONTAINER_NAME ) { childProcess.execSync( - `"${SIGNTOOL_PATH}" sign -d "Arduino Agent" -f "${INSTALLER_CERT_WINDOWS_CER}" -csp "eToken Base Cryptographic Provider" -k "[{{${CERT_PASSWORD}}}]=${CONTAINER_NAME}" -fd sha256 -tr http://timestamp.digicert.com -td SHA256 -v "${filePath}"`, + `"${SIGNTOOL_PATH}" sign -d "Open Duino" -f "${INSTALLER_CERT_WINDOWS_CER}" -csp "eToken Base Cryptographic Provider" -k "[{{${CERT_PASSWORD}}}]=${CONTAINER_NAME}" -fd sha256 -tr http://timestamp.digicert.com -td SHA256 -v "${filePath}"`, { stdio: 'inherit' } ); } else { diff --git a/electron-app/webpack.dev.js b/electron-app/webpack.dev.js index a4f7b06beab..488db8726fb 100644 --- a/electron-app/webpack.dev.js +++ b/electron-app/webpack.dev.js @@ -1,22 +1,6 @@ // When running in development mode, do not webpack the backend and electron main modules. // It does not work in watch mode: https://github.com/eclipse-theia/theia/issues/12793. -const path = require('node:path'); const configs = require('./webpack.config'); -const { createCopyArduinoResourcesPlugins } = require('./webpack.base'); const [mainWindowConfig, preloadConfig] = configs; -// Use the frontend's webpack config to copy the required resources to the `./arduino-ide-extension/lib/node/resources` folder. -mainWindowConfig.plugins?.push( - ...createCopyArduinoResourcesPlugins( - path.join( - __dirname, - '..', - 'arduino-ide-extension', - 'lib', - 'node', - 'resources' - ) - ) -); - module.exports = [mainWindowConfig, preloadConfig]; diff --git a/lerna.json b/lerna.json index 13216973df4..0acebe0c341 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { "lerna": "2.4.0", "version": "independent", - "npmClient": "yarn", + "npmClient": "npm", "command": { "run": { "stream": true diff --git a/package.json b/package.json index bab5aa04848..c8da7b28196 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "arduino-ide", + "name": "open-duino", "version": "2.3.10", - "description": "Arduino IDE", - "repository": "https://github.com/arduino/arduino-ide.git", - "author": "Arduino SA", + "description": "Open Duino: an AI-assisted Arduino development environment", + "repository": "https://github.com/lanuzadev/Open-Duino.git", + "author": "Lanuzadev", "license": "AGPL-3.0-or-later", "private": true, "packageManager": "yarn@4.5.3", @@ -48,7 +48,8 @@ "rebuild:browser": "theia rebuild:browser", "rebuild:electron": "theia rebuild:electron", "start": "yarn --cwd ./electron-app start", - "watch": "lerna run watch --parallel", + "dev": "node scripts/dev.js", + "watch": "node scripts/watch.js", "build": "lerna run build", "build:dev": "lerna run build:dev", "test": "lerna run test", diff --git a/scripts/copy-dev-resources.js b/scripts/copy-dev-resources.js new file mode 100644 index 00000000000..dcaf6f1203d --- /dev/null +++ b/scripts/copy-dev-resources.js @@ -0,0 +1,15 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +const root = path.resolve(__dirname, '..'); +const resourcesSource = path.join(root, 'arduino-ide-extension', 'src', 'node', 'resources'); +const resourcesTarget = path.join(root, 'arduino-ide-extension', 'lib', 'node', 'resources'); +const plotterSource = path.join(root, 'node_modules', 'arduino-serial-plotter-webapp', 'build'); +const plotterTarget = path.join(resourcesTarget, 'arduino-serial-plotter-webapp'); + +fs.cpSync(resourcesSource, resourcesTarget, { + recursive: true, + filter: (source) => !source.split(path.sep).includes('i18n'), +}); +fs.cpSync(plotterSource, plotterTarget, { recursive: true }); +console.log('[copy-dev-resources] Arduino CLI assets and plotter copied.'); diff --git a/scripts/dev.js b/scripts/dev.js new file mode 100644 index 00000000000..5c123c61fb1 --- /dev/null +++ b/scripts/dev.js @@ -0,0 +1,102 @@ +const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +const root = path.resolve(__dirname, '..'); +const cacheRoot = path.join(root, '.dev-cache'); +const tempDir = path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Temp', 'OpenDuinoDev'); +fs.mkdirSync(tempDir, { recursive: true }); + +const env = { + ...process.env, + TMP: tempDir, + TEMP: tempDir, + ELECTRON_CACHE: path.join(cacheRoot, 'electron'), + ELECTRON_BUILDER_CACHE: path.join(cacheRoot, 'electron-builder'), + PYTHONPATH: [ + path.join(__dirname, 'python-bootstrap'), + path.join(cacheRoot, 'python-packages'), + process.env.PYTHONPATH, + ] + .filter(Boolean) + .join(path.delimiter), + PYTHON: process.env.OPENDUINO_PYTHON || path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Python', 'bin', 'python.exe'), + SETUPTOOLS_USE_DISTUTILS: 'local', +}; + +const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + +function run(args, cwd = root) { + const result = spawnSync(npm, args, { + cwd, + env, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + if (result.error) throw result.error; + if (result.status !== 0) process.exit(result.status ?? 1); +} + +function runNode(script) { + const result = spawnSync(process.execPath, [script], { + cwd: root, + env, + stdio: 'inherit', + }); + if (result.error) throw result.error; + if (result.status !== 0) process.exit(result.status ?? 1); +} + +run(['run', 'prepare:shims']); +const electronRuntime = path.join(root, 'node_modules', 'electron', 'dist', 'electron.exe'); +if (!fs.existsSync(electronRuntime)) { + runNode(path.join(root, 'node_modules', 'electron', 'install.js')); +} +const nativeBuildMarker = path.join(cacheRoot, 'electron-30.1.2-native-build.done'); +if (!fs.existsSync(nativeBuildMarker)) { + run(['run', 'rebuild', '--prefix', 'electron-app']); + fs.writeFileSync(nativeBuildMarker, 'Electron 30.1.2 native modules rebuilt.\n'); +} +const ffmpegBuildMarker = path.join(cacheRoot, 'theia-ffmpeg-native-build.done'); +if (!fs.existsSync(ffmpegBuildMarker)) { + const globalNodeHeaders = path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'node-gyp', 'Cache', process.versions.node); + const nodeHeaders = path.join(cacheRoot, 'node-headers', process.versions.node); + if (!fs.existsSync(path.join(nodeHeaders, 'include', 'node', 'common.gypi')) && fs.existsSync(path.join(globalNodeHeaders, 'include', 'node', 'common.gypi'))) { + fs.cpSync(globalNodeHeaders, nodeHeaders, { recursive: true }); + const architectureLib = path.join(nodeHeaders, process.arch === 'x64' ? 'x64' : process.arch, 'node.lib'); + if (fs.existsSync(architectureLib)) { + for (const configuration of ['Debug', 'Release']) { + const configurationDir = path.join(nodeHeaders, configuration); + fs.mkdirSync(configurationDir, { recursive: true }); + fs.copyFileSync(architectureLib, path.join(configurationDir, 'node.lib')); + } + } + } + const nodeGypArgs = ['exec', '--', 'node-gyp', 'rebuild', '--debug', '--directory', 'node_modules/@theia/ffmpeg']; + if (fs.existsSync(path.join(nodeHeaders, 'include', 'node', 'common.gypi'))) { + nodeGypArgs.push(`--nodedir=${nodeHeaders}`); + } + run(nodeGypArgs); + fs.writeFileSync(ffmpegBuildMarker, 'Theia FFmpeg native module built.\n'); +} +run(['run', 'build', '--prefix', 'arduino-ide-extension']); +run(['run', 'build', '--prefix', 'arduino-mcp-extension']); +runNode(path.join(__dirname, 'copy-dev-resources.js')); +run(['run', 'build:dev', '--prefix', 'electron-app']); +const devUserData = path.join(cacheRoot, 'user-data'); +fs.mkdirSync(devUserData, { recursive: true }); +const electronApp = path.join(root, 'electron-app'); +const electronExecutable = require(require.resolve('electron', { paths: [electronApp] })); +const appArgs = ['--user-data-dir=' + devUserData, '--disable-gpu', '--no-sandbox', electronApp, '--plugins=local-dir:../plugins']; +const app = spawnSync(electronExecutable, [ + ...appArgs, +], { + cwd: electronApp, + env, + stdio: 'inherit', +}); +if (app.error) throw app.error; +if (app.status !== 0) { + console.error(`Electron exited with status ${app.status ?? app.signal}.`); + process.exit(app.status ?? 1); +} diff --git a/scripts/python-bootstrap/sitecustomize.py b/scripts/python-bootstrap/sitecustomize.py new file mode 100644 index 00000000000..8341b9d8fee --- /dev/null +++ b/scripts/python-bootstrap/sitecustomize.py @@ -0,0 +1,3 @@ +"""Load setuptools' distutils compatibility shim for node-gyp on Python 3.12+.""" + +import setuptools # noqa: F401 diff --git a/scripts/watch.js b/scripts/watch.js new file mode 100644 index 00000000000..d022bc317fb --- /dev/null +++ b/scripts/watch.js @@ -0,0 +1,39 @@ +const { spawn } = require('node:child_process'); +const path = require('node:path'); + +const root = path.resolve(__dirname, '..'); +const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +const packages = ['arduino-ide-extension', 'arduino-mcp-extension', 'electron-app']; +const children = []; +let shuttingDown = false; + +function stop(code = 0) { + if (shuttingDown) return; + shuttingDown = true; + for (const child of children) { + if (child.exitCode === null) child.kill('SIGTERM'); + } + process.exitCode = code; +} + +for (const packageDir of packages) { + const child = spawn(npm, ['run', 'watch', '--prefix', packageDir], { + cwd: root, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + children.push(child); + child.on('error', (error) => { + console.error(`[watch:${packageDir}] ${error.message}`); + stop(1); + }); + child.on('exit', (code, signal) => { + if (!shuttingDown && (code !== 0 || signal)) { + console.error(`[watch:${packageDir}] stopped (${signal || code}).`); + stop(code || 1); + } + }); +} + +process.on('SIGINT', () => stop(0)); +process.on('SIGTERM', () => stop(0)); diff --git a/yarn.lock b/yarn.lock index c6a45699bc4..9c58c3adf95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5372,37 +5372,6 @@ __metadata: languageName: unknown linkType: soft -"arduino-ide@workspace:.": - version: 0.0.0-use.local - resolution: "arduino-ide@workspace:." - dependencies: - "@theia/cli": "npm:1.57.0" - "@typescript-eslint/eslint-plugin": "npm:^5.59.0" - "@typescript-eslint/parser": "npm:^5.59.0" - "@xhmikosr/downloader": "npm:^13.0.1" - eslint: "npm:^8.39.0" - eslint-config-prettier: "npm:^8.8.0" - eslint-plugin-prettier: "npm:^4.2.1" - eslint-plugin-react: "npm:^7.32.2" - eslint-plugin-react-hooks: "npm:^4.6.0" - eslint-plugin-unused-imports: "npm:^2.0.0" - husky: "npm:^6.0.0" - ignore-styles: "npm:^5.0.1" - js-yaml: "npm:4.1.0" - lerna: "npm:^7.1.4" - lint-staged: "npm:^11.0.0" - node-fetch: "npm:^2.6.1" - node-gyp: "npm:^9.3.0" - prettier: "npm:^2.3.1" - reflect-metadata: "npm:^0.1.10" - rimraf: "npm:^5.0.0" - semver: "npm:^7.3.2" - style-dictionary: "npm:^3.7.0" - typescript: "npm:^5.4.5" - xhr2: "npm:^0.2.1" - languageName: unknown - linkType: soft - "arduino-mcp-extension@npm:0.7.1, arduino-mcp-extension@workspace:arduino-mcp-extension": version: 0.0.0-use.local resolution: "arduino-mcp-extension@workspace:arduino-mcp-extension" @@ -14294,11 +14263,11 @@ __metadata: "node-pty@patch:node-pty@npm%3A0.11.0-beta24#~/.yarn/patches/node-pty-npm-0.11.0-beta24-bb53864dbd.patch": version: 0.11.0-beta24 - resolution: "node-pty@patch:node-pty@npm%3A0.11.0-beta24#~/.yarn/patches/node-pty-npm-0.11.0-beta24-bb53864dbd.patch::version=0.11.0-beta24&hash=047db5" + resolution: "node-pty@patch:node-pty@npm%3A0.11.0-beta24#~/.yarn/patches/node-pty-npm-0.11.0-beta24-bb53864dbd.patch::version=0.11.0-beta24&hash=e307d0" dependencies: nan: "npm:^2.17.0" node-gyp: "npm:latest" - checksum: 10c0/46c055d10388b54069c0958b418810f2c425e120e3d78dd0c8f30cb4d4a41b86128b05feab330bbd81490c3d9d8c8151233ff5817d6c87da8447dd0e042e0549 + checksum: 10c0/da059d3f67fff434be63a8ef83cb08bc7cb2234ccd964fa643264e7e096fecd9e22bd9c3414cb89979876db8337dc8fb33b8535bf2ad6ca90e0eb8cb4210ae74 languageName: node linkType: hard @@ -14807,6 +14776,37 @@ __metadata: languageName: node linkType: hard +"open-duino@workspace:.": + version: 0.0.0-use.local + resolution: "open-duino@workspace:." + dependencies: + "@theia/cli": "npm:1.57.0" + "@typescript-eslint/eslint-plugin": "npm:^5.59.0" + "@typescript-eslint/parser": "npm:^5.59.0" + "@xhmikosr/downloader": "npm:^13.0.1" + eslint: "npm:^8.39.0" + eslint-config-prettier: "npm:^8.8.0" + eslint-plugin-prettier: "npm:^4.2.1" + eslint-plugin-react: "npm:^7.32.2" + eslint-plugin-react-hooks: "npm:^4.6.0" + eslint-plugin-unused-imports: "npm:^2.0.0" + husky: "npm:^6.0.0" + ignore-styles: "npm:^5.0.1" + js-yaml: "npm:4.1.0" + lerna: "npm:^7.1.4" + lint-staged: "npm:^11.0.0" + node-fetch: "npm:^2.6.1" + node-gyp: "npm:^9.3.0" + prettier: "npm:^2.3.1" + reflect-metadata: "npm:^0.1.10" + rimraf: "npm:^5.0.0" + semver: "npm:^7.3.2" + style-dictionary: "npm:^3.7.0" + typescript: "npm:^5.4.5" + xhr2: "npm:^0.2.1" + languageName: unknown + linkType: soft + "open@npm:^7.4.2": version: 7.4.2 resolution: "open@npm:7.4.2"