From a4ddbd628edd5a336fd97048215db76b6501b110 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 10 Jul 2026 15:20:34 -0400 Subject: [PATCH 1/6] feat(desktop): Electron desktop app skeleton (read-only v1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker-Desktop-style dashboard for non-technical users: shows control plane health (GET /health) and the locally installed agent nodes from ~/.agentfield/installed.yaml, cross-checked against GET /api/v1/nodes for a running/stopped/unknown badge per agent. Polls every 5s with a manual Refresh button and graceful empty states. Electron + electron-vite + React + TypeScript, plain CSS. Secure defaults: contextIsolation on, nodeIntegration off, sandboxed renderer, single contextBridge API. All data access is isolated in src/main/agentfield.ts with a marked seam to later swap the registry read to `af list -o json`. 29 vitest unit tests cover registry parsing, health mapping, and badge derivation. Self-contained under desktop/ (own package.json); no packaging (electron-builder) yet, and the GUI is untested in this headless environment — typecheck, production build, and unit tests all pass. Co-Authored-By: Claude Fable 5 --- desktop/.gitignore | 4 + desktop/README.md | 70 + desktop/electron.vite.config.ts | 14 + desktop/package-lock.json | 3221 +++++++++++++++++ desktop/package.json | 31 + desktop/src/main/agentfield.test.ts | 301 ++ desktop/src/main/agentfield.ts | 216 ++ desktop/src/main/index.ts | 43 + desktop/src/preload/index.ts | 9 + desktop/src/renderer/index.html | 16 + desktop/src/renderer/src/App.tsx | 61 + .../renderer/src/components/AgentsList.tsx | 80 + .../src/components/ControlPlaneCard.tsx | 38 + desktop/src/renderer/src/env.d.ts | 6 + desktop/src/renderer/src/main.tsx | 10 + desktop/src/renderer/src/styles.css | 232 ++ desktop/src/shared/types.ts | 59 + desktop/tsconfig.json | 21 + 18 files changed, 4432 insertions(+) create mode 100644 desktop/.gitignore create mode 100644 desktop/README.md create mode 100644 desktop/electron.vite.config.ts create mode 100644 desktop/package-lock.json create mode 100644 desktop/package.json create mode 100644 desktop/src/main/agentfield.test.ts create mode 100644 desktop/src/main/agentfield.ts create mode 100644 desktop/src/main/index.ts create mode 100644 desktop/src/preload/index.ts create mode 100644 desktop/src/renderer/index.html create mode 100644 desktop/src/renderer/src/App.tsx create mode 100644 desktop/src/renderer/src/components/AgentsList.tsx create mode 100644 desktop/src/renderer/src/components/ControlPlaneCard.tsx create mode 100644 desktop/src/renderer/src/env.d.ts create mode 100644 desktop/src/renderer/src/main.tsx create mode 100644 desktop/src/renderer/src/styles.css create mode 100644 desktop/src/shared/types.ts create mode 100644 desktop/tsconfig.json diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 000000000..b9e9034b3 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +out/ +*.log diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 000000000..4f4fc208f --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,70 @@ +# AgentField Desktop + +A read-only v1 desktop dashboard for AgentField, in the spirit of Docker Desktop: +one window that shows the health of the local control plane and the agent nodes +installed on this machine. + +What it shows: + +- **Control plane health** — polls `GET http://localhost:8080/health` and renders + Running (healthy), Reachable (unhealthy), or Not reachable. +- **Installed agent nodes** — reads `~/.agentfield/installed.yaml` (name, version, + language, port, PID) and derives a status badge per agent by cross-checking the + registry against the control plane's `GET /api/v1/nodes` view: + - `running` — registry says running and the control plane sees the node + - `stopped` — registry says stopped and the control plane does not see it + - `unknown` — registry and control plane disagree (stale registry / conflict), + or the registry status is unrecognized + +The renderer polls a single snapshot over IPC every 5 seconds, with a manual +Refresh button. + +## Prerequisites + +- Node.js 20+ (developed on Node 22) +- An AgentField control plane on `http://localhost:8080` (optional — the app + degrades gracefully when it is not running) +- Optionally, agents installed via `af install ...` (populates + `~/.agentfield/installed.yaml`) + +## Development + +```bash +cd desktop +npm install +npm run dev # electron-vite dev server + Electron window (needs a display) +``` + +## Build, typecheck, test + +```bash +npm run typecheck # tsc --noEmit over main, preload, shared, and renderer +npm run build # typecheck + electron-vite production build into out/ +npm test # vitest unit tests for the data-access module (headless) +``` + +## Architecture + +- **All Node-side data access lives in one module:** `src/main/agentfield.ts` + (registry parsing, control-plane HTTP probes, badge derivation, snapshot + composition). It has no Electron imports, so it is unit-tested directly with + Vitest. +- **Secure Electron layout:** the renderer runs with `contextIsolation: true`, + `nodeIntegration: false`, `sandbox: true`. The preload + (`src/preload/index.ts`) exposes exactly one method — `window.agentfield.getSnapshot()` + — via `contextBridge`/`ipcRenderer.invoke`. +- **Shared IPC types** live in `src/shared/types.ts` and are imported type-only + by main, preload, and renderer. +- Standard electron-vite project layout: `src/main`, `src/preload`, + `src/renderer` (with `src/renderer/index.html`), built into `out/`. + +## Current limitations + +- **Read-only** — no install/start/stop actions; it only observes. +- Control plane URL is hard-coded to `http://localhost:8080` (not configurable yet). +- The registry is read directly from `~/.agentfield/installed.yaml`; once + `af list -o json` lands, the app should shell out to the CLI instead so the CLI + stays the single source of truth for registry parsing (see the `TODO(af-cli)` + seam in `src/main/agentfield.ts`). +- Not packaged — no electron-builder / installer targets yet. +- Developed headless (WSL); the GUI is untested on Windows/macOS so far. diff --git a/desktop/electron.vite.config.ts b/desktop/electron.vite.config.ts new file mode 100644 index 000000000..be5eddd5c --- /dev/null +++ b/desktop/electron.vite.config.ts @@ -0,0 +1,14 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'electron-vite' + +// electron-vite defaults (kept deliberately): +// main: src/main/index.ts -> out/main/index.js (CJS, package has no "type": "module") +// preload: src/preload/index.ts -> out/preload/index.js (CJS — required for sandbox: true) +// renderer: src/renderer/index.html + src/renderer/src -> out/renderer +export default defineConfig({ + main: {}, + preload: {}, + renderer: { + plugins: [react()] + } +}) diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 000000000..27f388fb8 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,3221 @@ +{ + "name": "agentfield-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agentfield-desktop", + "version": "0.1.0", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.10.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "electron": "^33.2.1", + "electron-vite": "^3.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.7.2", + "vite": "^6.0.5", + "vitest": "^3.0.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron": { + "version": "33.4.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", + "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-3.1.0.tgz", + "integrity": "sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "cac": "^6.7.14", + "esbuild": "^0.25.1", + "magic-string": "^0.30.17", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 000000000..3ea024d05 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,31 @@ +{ + "name": "agentfield-desktop", + "version": "0.1.0", + "private": true, + "description": "Read-only desktop dashboard for the AgentField control plane and locally installed agent nodes", + "main": "./out/main/index.js", + "scripts": { + "dev": "electron-vite dev", + "typecheck": "tsc --noEmit", + "build": "npm run typecheck && electron-vite build", + "preview": "electron-vite preview", + "test": "vitest run" + }, + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.10.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "electron": "^33.2.1", + "electron-vite": "^3.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.7.2", + "vite": "^6.0.5", + "vitest": "^3.0.5" + } +} diff --git a/desktop/src/main/agentfield.test.ts b/desktop/src/main/agentfield.test.ts new file mode 100644 index 000000000..416ac5a83 --- /dev/null +++ b/desktop/src/main/agentfield.test.ts @@ -0,0 +1,301 @@ +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + checkControlPlane, + deriveAgentBadge, + fetchControlPlaneNodes, + getAgentFieldHome, + getSnapshot, + readInstalledAgents, + type FetchLike +} from './agentfield' + +const tmpDirs: string[] = [] + +async function makeHome(installedYaml?: string): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentfield-desktop-test-')) + tmpDirs.push(dir) + if (installedYaml !== undefined) { + await fs.writeFile(path.join(dir, 'installed.yaml'), installedYaml, 'utf8') + } + return dir +} + +afterEach(async () => { + await Promise.all( + tmpDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })) + ) +}) + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' } + }) +} + +const REGISTRY_FIXTURE = `installed: + pr-af: + name: pr-af + version: 0.1.0 + description: Opens draft pull requests from a task description + path: /home/abir/.agentfield/packages/pr-af + source: local + source_path: ./fix-praf + installed_at: "2026-07-08T10:35:03-04:00" + status: running + language: python + runtime: + port: 9001 + pid: 4242 + started_at: "2026-07-08T10:36:00-04:00" + log_file: /home/abir/.agentfield/logs/pr-af.log + swe-af: + version: 0.2.1 + description: Software engineering agent + status: stopped + runtime: + port: null + pid: null + started_at: null + log_file: /home/abir/.agentfield/logs/swe-af.log +` + +describe('getAgentFieldHome', () => { + it('is /.agentfield', () => { + expect(getAgentFieldHome()).toBe(path.join(os.homedir(), '.agentfield')) + }) +}) + +describe('readInstalledAgents', () => { + // Contract: registry with running + stopped entries (including null runtime + // fields and a missing optional language) parses into a correct agents array. + it('parses running and stopped entries, null runtime fields, optional language', async () => { + const home = await makeHome(REGISTRY_FIXTURE) + const result = await readInstalledAgents(home) + + expect(result.exists).toBe(true) + expect(result.error).toBeUndefined() + expect(result.agents).toHaveLength(2) + + const prAf = result.agents.find((a) => a.name === 'pr-af') + expect(prAf).toEqual({ + name: 'pr-af', + version: '0.1.0', + description: 'Opens draft pull requests from a task description', + language: 'python', + status: 'running', + port: 9001, + pid: 4242 + }) + + // Entry without a `name` field falls back to its registry key; nulls stay null. + const sweAf = result.agents.find((a) => a.name === 'swe-af') + expect(sweAf).toEqual({ + name: 'swe-af', + version: '0.2.1', + description: 'Software engineering agent', + language: undefined, + status: 'stopped', + port: null, + pid: null + }) + }) + + // Contract: missing installed.yaml (or missing ~/.agentfield entirely) is a + // graceful empty state, not an error. + it('returns { exists: false, agents: [] } when installed.yaml is missing', async () => { + const home = await makeHome() // dir exists, no installed.yaml + expect(await readInstalledAgents(home)).toEqual({ exists: false, agents: [] }) + }) + + it('returns { exists: false, agents: [] } when the home dir itself is missing', async () => { + const home = await makeHome() + const missing = path.join(home, 'does-not-exist') + expect(await readInstalledAgents(missing)).toEqual({ exists: false, agents: [] }) + }) + + // Contract: malformed YAML surfaces as an error string — never throws. + it('surfaces malformed YAML as an error string without throwing', async () => { + const home = await makeHome('installed:\n pr-af: [unclosed\n') + const result = await readInstalledAgents(home) + expect(result.exists).toBe(true) + expect(result.agents).toEqual([]) + expect(result.error).toContain('installed.yaml') + }) + + it('treats a YAML doc without an installed map as an empty registry', async () => { + const home = await makeHome('something_else: true\n') + const result = await readInstalledAgents(home) + expect(result).toEqual({ exists: true, agents: [] }) + }) +}) + +describe('deriveAgentBadge', () => { + // Contract: full truth table. + it.each([ + // [registryStatus, cpReachable, nodeSeen, expected] + // CP view unavailable -> trust the registry + ['running', false, false, 'running'], + ['running', false, true, 'running'], + ['stopped', false, false, 'stopped'], + ['stopped', false, true, 'stopped'], + ['error', false, false, 'unknown'], + [undefined, false, false, 'unknown'], + // CP view available -> cross-check + ['running', true, true, 'running'], + ['running', true, false, 'unknown'], // stale registry + ['stopped', true, true, 'unknown'], // conflict + ['stopped', true, false, 'stopped'], + ['error', true, true, 'unknown'], + [undefined, true, false, 'unknown'] + ] as const)( + 'status=%s reachable=%s seen=%s -> %s', + (status, reachable, seen, expected) => { + expect(deriveAgentBadge(status, reachable, seen)).toBe(expected) + } + ) +}) + +describe('checkControlPlane', () => { + // Contract: 200 healthy body -> reachable + healthy. + it('maps a 200 healthy body to reachable/healthy', async () => { + const body = { + status: 'healthy', + timestamp: '2026-07-10T12:00:00Z', + version: '0.1.107', + checks: {} + } + const fetchImpl: FetchLike = async () => jsonResponse(body, 200) + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result).toEqual({ reachable: true, healthy: true, raw: body }) + }) + + // Contract: 503 with an unhealthy body still means reachable, just not healthy. + it('maps a 503 unhealthy body to reachable but not healthy', async () => { + const body = { status: 'unhealthy', checks: { database: 'down' } } + const fetchImpl: FetchLike = async () => jsonResponse(body, 503) + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result.reachable).toBe(true) + expect(result.healthy).toBe(false) + expect(result.raw).toEqual(body) + }) + + // Contract: network error / timeout -> not reachable, error captured. + it('maps a rejected fetch to unreachable with an error message', async () => { + const fetchImpl: FetchLike = async () => { + throw new TypeError('fetch failed') + } + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result).toEqual({ reachable: false, healthy: false, error: 'fetch failed' }) + }) + + it('probes {baseUrl}/health', async () => { + let requested = '' + const fetchImpl: FetchLike = async (input) => { + requested = String(input) + return jsonResponse({ status: 'healthy' }) + } + await checkControlPlane('http://example.test:1234', fetchImpl) + expect(requested).toBe('http://example.test:1234/health') + }) +}) + +describe('fetchControlPlaneNodes', () => { + it('returns node ids from a 200 nodes payload', async () => { + const fetchImpl: FetchLike = async () => + jsonResponse({ + nodes: [ + { id: 'pr-af', health_status: 'active' }, + { id: 'swe-af', health_status: 'active' } + ], + count: 2 + }) + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toEqual([ + 'pr-af', + 'swe-af' + ]) + }) + + it('returns null on a non-200 response', async () => { + const fetchImpl: FetchLike = async () => jsonResponse({ error: 'nope' }, 500) + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toBeNull() + }) + + it('returns null when fetch rejects', async () => { + const fetchImpl: FetchLike = async () => { + throw new TypeError('fetch failed') + } + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toBeNull() + }) + + it('returns null on an unexpected payload shape', async () => { + const fetchImpl: FetchLike = async () => jsonResponse({ items: [] }) + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toBeNull() + }) +}) + +describe('getSnapshot', () => { + function routedFetch(routes: Record Response>): FetchLike { + return async (input) => { + const url = String(input) + const route = Object.keys(routes).find((suffix) => url.endsWith(suffix)) + if (!route) throw new TypeError(`unexpected fetch: ${url}`) + return routes[route]() + } + } + + it('composes control plane + registry with cross-checked badges', async () => { + const home = await makeHome(REGISTRY_FIXTURE) + const fetchImpl = routedFetch({ + '/health': () => jsonResponse({ status: 'healthy' }), + // Control plane sees pr-af but not swe-af. + '/api/v1/nodes': () => jsonResponse({ nodes: [{ id: 'pr-af' }], count: 1 }) + }) + + const snapshot = await getSnapshot({ homeDir: home, fetchImpl }) + + expect(snapshot.controlPlane.baseUrl).toBe('http://localhost:8080') + expect(snapshot.controlPlane.reachable).toBe(true) + expect(snapshot.controlPlane.healthy).toBe(true) + expect(snapshot.registry.exists).toBe(true) + expect(Date.parse(snapshot.fetchedAt)).not.toBeNaN() + + const badges = Object.fromEntries( + snapshot.registry.agents.map((a) => [a.name, a.badge]) + ) + expect(badges).toEqual({ + 'pr-af': 'running', // registry running + seen on CP + 'swe-af': 'stopped' // registry stopped + not seen + }) + }) + + it('falls back to registry status when the nodes endpoint fails', async () => { + const home = await makeHome(REGISTRY_FIXTURE) + const fetchImpl = routedFetch({ + '/health': () => jsonResponse({ status: 'healthy' }), + '/api/v1/nodes': () => jsonResponse({ error: 'boom' }, 500) + }) + + const snapshot = await getSnapshot({ homeDir: home, fetchImpl }) + const badges = Object.fromEntries( + snapshot.registry.agents.map((a) => [a.name, a.badge]) + ) + // Nodes view unavailable -> trust registry statuses directly. + expect(badges).toEqual({ 'pr-af': 'running', 'swe-af': 'stopped' }) + }) + + it('reports an unreachable control plane and an absent registry gracefully', async () => { + const home = await makeHome() + const missing = path.join(home, 'nope') + const fetchImpl: FetchLike = async () => { + throw new TypeError('fetch failed') + } + + const snapshot = await getSnapshot({ homeDir: missing, fetchImpl }) + expect(snapshot.controlPlane.reachable).toBe(false) + expect(snapshot.registry).toEqual({ exists: false, agents: [], error: undefined }) + }) +}) diff --git a/desktop/src/main/agentfield.ts b/desktop/src/main/agentfield.ts new file mode 100644 index 000000000..3d68108d4 --- /dev/null +++ b/desktop/src/main/agentfield.ts @@ -0,0 +1,216 @@ +// TODO(af-cli): this module currently reads ~/.agentfield/installed.yaml directly; +// a sibling branch is adding `af list -o json` — swap readInstalledAgents() to shell +// out to that once it lands, so the CLI stays the single source of truth for +// registry parsing. +// +// This is THE single data-access module for AgentField Desktop. Everything that +// touches the AgentField installation (~/.agentfield) or the control plane HTTP +// API lives here and nowhere else. It deliberately does NOT import from +// 'electron' so it stays unit-testable under plain vitest. + +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import yaml from 'js-yaml' +import type { + AgentBadge, + AgentFieldSnapshot, + ControlPlaneStatus, + InstalledAgent, + RegistryResult +} from '../shared/types' + +export const DEFAULT_BASE_URL = 'http://localhost:8080' + +const HTTP_TIMEOUT_MS = 3000 + +/** Injectable fetch so tests never hit the network. */ +export type FetchLike = typeof fetch + +/** Root of the local AgentField installation. os.homedir() is platform-aware + * (resolves %USERPROFILE% on Windows, $HOME elsewhere). */ +export function getAgentFieldHome(): string { + return path.join(os.homedir(), '.agentfield') +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err) +} + +/** + * Probe GET {baseUrl}/health. + * - 200 {"status":"healthy",...} -> { reachable: true, healthy: true } + * - 503 {"status":"unhealthy",...} -> { reachable: true, healthy: false } + * (an HTTP response — even 503 — still means the control plane is reachable) + * - network error / timeout (3s) -> { reachable: false, healthy: false, error } + */ +export async function checkControlPlane( + baseUrl: string = DEFAULT_BASE_URL, + fetchImpl: FetchLike = fetch +): Promise { + try { + const res = await fetchImpl(`${baseUrl}/health`, { + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }) + let raw: unknown + try { + raw = await res.json() + } catch { + raw = undefined + } + return { reachable: true, healthy: res.ok, raw } + } catch (err) { + return { reachable: false, healthy: false, error: errorMessage(err) } + } +} + +function toInstalledAgent(key: string, entry: unknown): InstalledAgent { + const record = isRecord(entry) ? entry : {} + const runtime = isRecord(record.runtime) ? record.runtime : {} + return { + name: typeof record.name === 'string' && record.name !== '' ? record.name : key, + version: typeof record.version === 'string' ? record.version : '', + description: typeof record.description === 'string' ? record.description : '', + language: typeof record.language === 'string' ? record.language : undefined, + status: typeof record.status === 'string' ? record.status : 'unknown', + port: typeof runtime.port === 'number' ? runtime.port : null, + pid: typeof runtime.pid === 'number' ? runtime.pid : null + } +} + +/** + * Read /installed.yaml (the local agent-node registry). + * - Missing file or missing ~/.agentfield dir -> { exists: false, agents: [] } + * (graceful empty state, NOT an error). + * - Malformed YAML -> error surfaced as a string in the result; never throws, + * so nothing blows up across the IPC boundary. + */ +export async function readInstalledAgents( + homeDir: string = getAgentFieldHome() +): Promise { + const registryPath = path.join(homeDir, 'installed.yaml') + let text: string + try { + text = await fs.readFile(registryPath, 'utf8') + } catch (err) { + const code = (err as NodeJS.ErrnoException).code + if (code === 'ENOENT' || code === 'ENOTDIR') { + return { exists: false, agents: [] } + } + return { exists: false, agents: [], error: errorMessage(err) } + } + + let doc: unknown + try { + doc = yaml.load(text) + } catch (err) { + return { + exists: true, + agents: [], + error: `Failed to parse ${registryPath}: ${errorMessage(err)}` + } + } + + const installed = isRecord(doc) && isRecord(doc.installed) ? doc.installed : {} + const agents = Object.entries(installed).map(([key, entry]) => toInstalledAgent(key, entry)) + return { exists: true, agents } +} + +/** + * GET {baseUrl}/api/v1/nodes -> {"nodes":[{"id":...,"health_status":...},...],"count":N} + * (the server's default filter returns active nodes only). + * Returns the list of node ids, or null on any failure — callers treat null as + * "control plane view unavailable" and fall back to registry status alone. + */ +export async function fetchControlPlaneNodes( + baseUrl: string = DEFAULT_BASE_URL, + fetchImpl: FetchLike = fetch +): Promise { + try { + const res = await fetchImpl(`${baseUrl}/api/v1/nodes`, { + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }) + if (!res.ok) return null + const body: unknown = await res.json() + if (!isRecord(body) || !Array.isArray(body.nodes)) return null + return body.nodes + .filter(isRecord) + .map((node) => (typeof node.id === 'string' ? node.id : '')) + .filter((id) => id.length > 0) + } catch { + return null + } +} + +/** + * Pure badge derivation. `controlPlaneReachable` here means "we have a usable + * control-plane node view" (health reachable AND the nodes list fetched). + * + * CP view unavailable — trust the registry: + * 'running' -> 'running' | 'stopped' -> 'stopped' | other/absent -> 'unknown' + * CP view available — cross-check: + * registry running + node seen -> 'running' + * registry running + node NOT seen -> 'unknown' (stale registry) + * registry stopped + node seen -> 'unknown' (conflict) + * registry stopped + node NOT seen -> 'stopped' + * other/absent registry status -> 'unknown' + */ +export function deriveAgentBadge( + registryStatus: string | undefined, + controlPlaneReachable: boolean, + nodeSeenOnControlPlane: boolean +): AgentBadge { + if (!controlPlaneReachable) { + if (registryStatus === 'running') return 'running' + if (registryStatus === 'stopped') return 'stopped' + return 'unknown' + } + if (registryStatus === 'running') { + return nodeSeenOnControlPlane ? 'running' : 'unknown' + } + if (registryStatus === 'stopped') { + return nodeSeenOnControlPlane ? 'unknown' : 'stopped' + } + return 'unknown' +} + +export interface SnapshotOptions { + baseUrl?: string + homeDir?: string + fetchImpl?: FetchLike +} + +/** + * Compose everything into the single IPC payload the renderer polls. + * Options exist only for tests; production callers use the defaults. + */ +export async function getSnapshot(options: SnapshotOptions = {}): Promise { + const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL + const fetchImpl = options.fetchImpl ?? fetch + + const [controlPlane, registry] = await Promise.all([ + checkControlPlane(baseUrl, fetchImpl), + readInstalledAgents(options.homeDir) + ]) + + const nodeIds = controlPlane.reachable + ? await fetchControlPlaneNodes(baseUrl, fetchImpl) + : null + const hasControlPlaneView = nodeIds !== null + const seen = new Set(nodeIds ?? []) + + const agents = registry.agents.map((agent) => ({ + ...agent, + badge: deriveAgentBadge(agent.status, hasControlPlaneView, seen.has(agent.name)) + })) + + return { + controlPlane: { ...controlPlane, baseUrl }, + registry: { exists: registry.exists, agents, error: registry.error }, + fetchedAt: new Date().toISOString() + } +} diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts new file mode 100644 index 000000000..665ee73e7 --- /dev/null +++ b/desktop/src/main/index.ts @@ -0,0 +1,43 @@ +import { join } from 'node:path' +import { BrowserWindow, app, ipcMain } from 'electron' +import { getSnapshot } from './agentfield' + +function createWindow(): void { + const win = new BrowserWindow({ + width: 1080, + height: 720, + title: 'AgentField Desktop', + backgroundColor: '#111418', + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true + } + }) + + // Read-only dashboard: never open child windows. + win.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) + + // electron-vite convention: dev server URL in dev, built file in production. + const devUrl = process.env['ELECTRON_RENDERER_URL'] + if (devUrl) { + void win.loadURL(devUrl) + } else { + void win.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +app.whenReady().then(() => { + ipcMain.handle('agentfield:snapshot', () => getSnapshot()) + + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/desktop/src/preload/index.ts b/desktop/src/preload/index.ts new file mode 100644 index 000000000..4317803ca --- /dev/null +++ b/desktop/src/preload/index.ts @@ -0,0 +1,9 @@ +import { contextBridge, ipcRenderer } from 'electron' +import type { AgentFieldApi } from '../shared/types' + +// Sandboxed preload: only contextBridge/ipcRenderer are used, no Node APIs. +const api: AgentFieldApi = { + getSnapshot: () => ipcRenderer.invoke('agentfield:snapshot') +} + +contextBridge.exposeInMainWorld('agentfield', api) diff --git a/desktop/src/renderer/index.html b/desktop/src/renderer/index.html new file mode 100644 index 000000000..6ca8ff3b2 --- /dev/null +++ b/desktop/src/renderer/index.html @@ -0,0 +1,16 @@ + + + + + + + AgentField Desktop + + +
+ + + diff --git a/desktop/src/renderer/src/App.tsx b/desktop/src/renderer/src/App.tsx new file mode 100644 index 000000000..ef267b47c --- /dev/null +++ b/desktop/src/renderer/src/App.tsx @@ -0,0 +1,61 @@ +import { useCallback, useEffect, useState } from 'react' +import type { AgentFieldSnapshot } from '../../shared/types' +import { AgentsList } from './components/AgentsList' +import { ControlPlaneCard } from './components/ControlPlaneCard' + +const POLL_INTERVAL_MS = 5000 + +export default function App() { + const [snapshot, setSnapshot] = useState(null) + const [ipcError, setIpcError] = useState(null) + const [refreshing, setRefreshing] = useState(false) + + const refresh = useCallback(async () => { + setRefreshing(true) + try { + const next = await window.agentfield.getSnapshot() + setSnapshot(next) + setIpcError(null) + } catch (err) { + setIpcError(err instanceof Error ? err.message : String(err)) + } finally { + setRefreshing(false) + } + }, []) + + useEffect(() => { + void refresh() + const timer = setInterval(() => void refresh(), POLL_INTERVAL_MS) + return () => clearInterval(timer) + }, [refresh]) + + const lastUpdated = snapshot + ? new Date(snapshot.fetchedAt).toLocaleTimeString() + : null + + return ( +
+
+
+

AgentField Desktop

+

Read-only dashboard · polls every {POLL_INTERVAL_MS / 1000}s

+
+
+ {lastUpdated && Last updated {lastUpdated}} + +
+
+ + {ipcError && ( +
Failed to fetch snapshot: {ipcError}
+ )} + +
+ + +
+
+ ) +} diff --git a/desktop/src/renderer/src/components/AgentsList.tsx b/desktop/src/renderer/src/components/AgentsList.tsx new file mode 100644 index 000000000..bd412ff33 --- /dev/null +++ b/desktop/src/renderer/src/components/AgentsList.tsx @@ -0,0 +1,80 @@ +import type { AgentFieldSnapshot } from '../../../shared/types' + +interface AgentsListProps { + registry: AgentFieldSnapshot['registry'] | null +} + +export function AgentsList({ registry }: AgentsListProps) { + return ( +
+
+

Installed agents

+ {registry && registry.exists && ( + {registry.agents.length} installed + )} +
+ +
+ ) +} + +function AgentsListBody({ registry }: AgentsListProps) { + if (!registry) { + return

Loading…

+ } + + if (registry.error) { + return
{registry.error}
+ } + + if (!registry.exists) { + return ( +
+

No AgentField installation found (~/.agentfield missing).

+

+ Install an agent with af install <source> to get started. +

+
+ ) + } + + if (registry.agents.length === 0) { + return ( +
+

No agents installed yet.

+
+ ) + } + + return ( + + + + + + + + + + + + + {registry.agents.map((agent) => ( + + + + + + + + + ))} + +
NameVersionLanguagePortPIDStatus
+ {agent.name} + {agent.description && — {agent.description}} + {agent.version || '—'}{agent.language ?? '—'}{agent.port ?? '—'}{agent.pid ?? '—'} + {agent.badge} +
+ ) +} diff --git a/desktop/src/renderer/src/components/ControlPlaneCard.tsx b/desktop/src/renderer/src/components/ControlPlaneCard.tsx new file mode 100644 index 000000000..7554cead9 --- /dev/null +++ b/desktop/src/renderer/src/components/ControlPlaneCard.tsx @@ -0,0 +1,38 @@ +import type { AgentFieldSnapshot } from '../../../shared/types' + +interface ControlPlaneCardProps { + controlPlane: AgentFieldSnapshot['controlPlane'] | null +} + +export function ControlPlaneCard({ controlPlane }: ControlPlaneCardProps) { + let dotClass = 'gray' + let label = 'Checking…' + if (controlPlane) { + if (controlPlane.reachable && controlPlane.healthy) { + dotClass = 'green' + label = 'Running' + } else if (controlPlane.reachable) { + dotClass = 'yellow' + label = 'Reachable (unhealthy)' + } else { + dotClass = 'red' + label = 'Not reachable' + } + } + + return ( +
+
+

Control plane

+ {controlPlane?.baseUrl ?? 'http://localhost:8080'} +
+
+
+ {controlPlane && !controlPlane.reachable && controlPlane.error && ( +

{controlPlane.error}

+ )} +
+ ) +} diff --git a/desktop/src/renderer/src/env.d.ts b/desktop/src/renderer/src/env.d.ts new file mode 100644 index 000000000..c74e20608 --- /dev/null +++ b/desktop/src/renderer/src/env.d.ts @@ -0,0 +1,6 @@ +declare module '*.css' + +interface Window { + /** Exposed by src/preload/index.ts via contextBridge. */ + agentfield: import('../../shared/types').AgentFieldApi +} diff --git a/desktop/src/renderer/src/main.tsx b/desktop/src/renderer/src/main.tsx new file mode 100644 index 000000000..241cf437d --- /dev/null +++ b/desktop/src/renderer/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles.css' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +) diff --git a/desktop/src/renderer/src/styles.css b/desktop/src/renderer/src/styles.css new file mode 100644 index 000000000..fb47bc6c0 --- /dev/null +++ b/desktop/src/renderer/src/styles.css @@ -0,0 +1,232 @@ +/* AgentField Desktop — minimal dark dashboard styles. Plain CSS, no framework. */ + +:root { + --bg: #111418; + --surface: #1a1f26; + --border: #2a313b; + --text: #e6e9ed; + --text-muted: #8b95a3; + --green: #3fb950; + --yellow: #d29922; + --red: #f85149; + --gray: #6e7681; + --accent: #4493f8; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; + line-height: 1.5; +} + +.app { + max-width: 960px; + margin: 0 auto; + padding: 24px; +} + +.app-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 20px; +} + +.app-header h1 { + margin: 0; + font-size: 20px; + font-weight: 600; +} + +.subtitle { + margin: 2px 0 0; + color: var(--text-muted); + font-size: 12px; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +button { + background: var(--accent); + color: #fff; + border: none; + border-radius: 6px; + padding: 6px 14px; + font-size: 13px; + cursor: pointer; +} + +button:hover:not(:disabled) { + filter: brightness(1.1); +} + +button:disabled { + opacity: 0.6; + cursor: default; +} + +.app-main { + display: flex; + flex-direction: column; + gap: 16px; +} + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; +} + +.card-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.card-header h2 { + margin: 0; + font-size: 15px; + font-weight: 600; +} + +.status-line { + display: flex; + align-items: center; + gap: 8px; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; +} + +.status-dot.green { + background: var(--green); +} + +.status-dot.yellow { + background: var(--yellow); +} + +.status-dot.red { + background: var(--red); +} + +.status-dot.gray { + background: var(--gray); +} + +.status-label { + font-weight: 500; +} + +.agents-table { + width: 100%; + border-collapse: collapse; +} + +.agents-table th, +.agents-table td { + text-align: left; + padding: 8px 10px; + border-bottom: 1px solid var(--border); +} + +.agents-table th { + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.agents-table tbody tr:last-child td { + border-bottom: none; +} + +.agent-name { + font-weight: 600; +} + +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + text-transform: capitalize; +} + +.badge.running { + background: rgba(63, 185, 80, 0.15); + color: var(--green); +} + +.badge.stopped { + background: rgba(110, 118, 129, 0.2); + color: var(--text-muted); +} + +.badge.unknown { + background: rgba(210, 153, 34, 0.15); + color: var(--yellow); +} + +.banner { + border-radius: 6px; + padding: 10px 14px; + margin-bottom: 16px; +} + +.banner.error { + background: rgba(248, 81, 73, 0.12); + border: 1px solid rgba(248, 81, 73, 0.4); + color: var(--red); +} + +.empty-state { + padding: 20px 0; + text-align: center; +} + +.empty-state p { + margin: 4px 0; +} + +.muted { + color: var(--text-muted); +} + +.small { + font-size: 12px; +} + +code { + font-family: ui-monospace, 'Cascadia Mono', 'Fira Code', monospace; + font-size: 12px; + background: rgba(110, 118, 129, 0.15); + padding: 1px 6px; + border-radius: 4px; +} diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts new file mode 100644 index 000000000..dac8815f7 --- /dev/null +++ b/desktop/src/shared/types.ts @@ -0,0 +1,59 @@ +// Shared types crossing the main / preload / renderer IPC boundary. +// Import these type-only from every layer — this file must stay runtime-free. + +/** Result of probing GET {baseUrl}/health on the control plane. */ +export interface ControlPlaneStatus { + /** An HTTP response came back (any status code, including 503). */ + reachable: boolean + /** The health endpoint answered 200 (body reports "healthy"). */ + healthy: boolean + /** Raw JSON body of the health response, when one was parseable. */ + raw?: unknown + /** Network/timeout error message when unreachable. */ + error?: string +} + +/** One entry parsed from ~/.agentfield/installed.yaml. */ +export interface InstalledAgent { + name: string + version: string + description: string + /** Optional on newer registry entries (python/go); absent on older ones. */ + language?: string + /** Raw registry status string (e.g. "running", "stopped"). */ + status: string + port: number | null + pid: number | null +} + +/** Registry read result. Missing file/dir is a graceful empty state, not an error. */ +export interface RegistryResult { + exists: boolean + agents: InstalledAgent[] + /** Set when the registry file exists but could not be parsed. */ + error?: string +} + +/** Status badge shown in the UI, derived from registry + control-plane view. */ +export type AgentBadge = 'running' | 'stopped' | 'unknown' + +export interface SnapshotAgent extends InstalledAgent { + badge: AgentBadge +} + +/** The single payload shipped over IPC to the renderer. */ +export interface AgentFieldSnapshot { + controlPlane: ControlPlaneStatus & { baseUrl: string } + registry: { + exists: boolean + agents: SnapshotAgent[] + error?: string + } + /** ISO timestamp of when this snapshot was assembled. */ + fetchedAt: string +} + +/** Surface exposed on window.agentfield by the preload script. */ +export interface AgentFieldApi { + getSnapshot(): Promise +} diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 000000000..85821ea66 --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "types": ["node"], + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true + }, + "include": ["electron.vite.config.ts", "src"] +} From ab490326bf67459aa5f91dee584c73b3344e59b4 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 10 Jul 2026 15:20:46 -0400 Subject: [PATCH 2/6] feat(release): add windows/amd64 build target to goreleaser Replaces the stale commented-out windows block with a real agentfield-windows-amd64 build mirroring the linux/darwin ones (goreleaser appends .exe on its own). Groundwork only: the release workflow's build matrix filters by --id and does not build this id yet; shipping the artifact needs a follow-up matrix entry (windows runner, or mingw-w64 on the linux runner for the CGO sqlite dependency). Also modernizes archives.builds/format to ids/formats so `goreleaser check` passes clean again (both were deprecated). Co-Authored-By: Claude Fable 5 --- .goreleaser.yml | 54 +++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index a4141fff2..6668dbff8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -121,41 +121,47 @@ builds: - -X main.commit={{ .ShortCommit }} - -X main.date={{ .Date }} - # Windows build disabled - focus on Linux and macOS for now - # - id: agentfield-windows-amd64 - # dir: control-plane - # main: ./cmd/af - # binary: agentfield - # env: - # - CGO_ENABLED=1 - # - CC=gcc - # goos: - # - windows - # goarch: - # - amd64 - # ldflags: - # - -s -w - # - -X main.version={{ .Version }} - # - -X main.commit={{ .ShortCommit }} - # - -X main.date={{ .Date }} - # tags: - # - embedded - # - sqlite_fts5 - # no_unique_dist_dir: true + # Windows CLI/server binary (goreleaser appends .exe automatically). + # Groundwork only: the release workflow's build matrix filters by --id and + # does not build this id yet. CGO is required (mattn/go-sqlite3 with + # sqlite_fts5), so building it needs either a windows runner (drop the CC + # override) or mingw-w64 on the linux runner (mirrors the aarch64 + # cross-compile approach). + - id: agentfield-windows-amd64 + dir: control-plane + main: ./cmd/af + binary: agentfield-windows-amd64 + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc + goos: + - windows + goarch: + - amd64 + ldflags: + - -s -w + - -X main.version={{ .Version }} + - -X main.commit={{ .ShortCommit }} + - -X main.date={{ .Date }} + tags: + - embedded + - sqlite_fts5 # Don't create archives - ship raw binaries archives: - id: default - builds: + ids: - agentfield-linux-amd64 - agentfield-linux-arm64 - agentfield-darwin-amd64 - agentfield-darwin-arm64 + - agentfield-windows-amd64 - agentfield-tray-darwin-amd64 - agentfield-tray-darwin-arm64 - format: binary + formats: [binary] # Each build's `binary:` is already the exact asset name we want - # (agentfield-- and agentfield-tray-darwin-). + # (agentfield-- and agentfield-tray-darwin-); goreleaser + # appends .exe for the windows build on its own. name_template: "{{ .Binary }}" checksum: From 0300bdbb857120c67d4340abd1b53984e8a12b53 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 10 Jul 2026 15:20:57 -0400 Subject: [PATCH 3/6] fix(control-plane): build-tagged process stop/liveness for windows `af stop` used process.Signal(os.Interrupt) and a signal-0 probe, both unsupported on Windows (and os.FindProcess always succeeds there, so the liveness check was meaningless). Extract the two process operations into build-tagged helpers: proc_unix.go keeps the existing SIGINT + signal-0 behaviour; proc_windows.go uses taskkill for the graceful request and a tasklist PID query for liveness. stop.go changes are limited to swapping the two call sites and the now-unused syscall import. Windows paths are compile-verified only (GOOS=windows cross-build), not yet tested on a real Windows machine. Co-Authored-By: Claude Fable 5 --- control-plane/internal/cli/proc_unix.go | 21 ++++++++ control-plane/internal/cli/proc_unix_test.go | 52 ++++++++++++++++++++ control-plane/internal/cli/proc_windows.go | 38 ++++++++++++++ control-plane/internal/cli/stop.go | 7 ++- 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 control-plane/internal/cli/proc_unix.go create mode 100644 control-plane/internal/cli/proc_unix_test.go create mode 100644 control-plane/internal/cli/proc_windows.go diff --git a/control-plane/internal/cli/proc_unix.go b/control-plane/internal/cli/proc_unix.go new file mode 100644 index 000000000..4b3be5e97 --- /dev/null +++ b/control-plane/internal/cli/proc_unix.go @@ -0,0 +1,21 @@ +//go:build !windows + +package cli + +import ( + "os" + "syscall" +) + +// signalGracefulStop asks a process to shut down gracefully. On Unix this +// sends SIGINT (os.Interrupt), matching the historical `af stop` behaviour. +// Callers fall back to process.Kill() when it returns an error. +func signalGracefulStop(process *os.Process) error { + return process.Signal(os.Interrupt) +} + +// isProcessAlive reports whether the process is still running. On Unix, +// signal 0 probes for liveness without delivering an actual signal. +func isProcessAlive(process *os.Process) bool { + return process.Signal(syscall.Signal(0)) == nil +} diff --git a/control-plane/internal/cli/proc_unix_test.go b/control-plane/internal/cli/proc_unix_test.go new file mode 100644 index 000000000..dfcbf2f4c --- /dev/null +++ b/control-plane/internal/cli/proc_unix_test.go @@ -0,0 +1,52 @@ +//go:build !windows + +package cli + +import ( + "os" + "os/exec" + "testing" +) + +// Contract: isProcessAlive must report true for a live process and false once +// the process has exited. `af stop` uses this to decide whether the graceful +// shutdown worked or a force-kill is needed. +func TestIsProcessAlive(t *testing.T) { + self, err := os.FindProcess(os.Getpid()) + if err != nil { + t.Fatalf("FindProcess(self): %v", err) + } + if !isProcessAlive(self) { + t.Fatal("isProcessAlive(current process) = false; want true") + } + + cmd := exec.Command("true") + if err := cmd.Start(); err != nil { + t.Fatalf("start: %v", err) + } + if err := cmd.Wait(); err != nil { + t.Fatalf("wait: %v", err) + } + if isProcessAlive(cmd.Process) { + t.Fatal("isProcessAlive(exited process) = true; want false") + } +} + +// Contract: signalGracefulStop must deliver an interrupt that terminates a +// well-behaved (default-signal-disposition) child process. +func TestSignalGracefulStop(t *testing.T) { + cmd := exec.Command("sleep", "30") + if err := cmd.Start(); err != nil { + t.Fatalf("start: %v", err) + } + if err := signalGracefulStop(cmd.Process); err != nil { + t.Fatalf("signalGracefulStop: %v", err) + } + // The child dies from the signal, so Wait returns a non-nil ExitError. + if err := cmd.Wait(); err == nil { + t.Fatal("child exited cleanly; want interrupt-terminated") + } + if isProcessAlive(cmd.Process) { + t.Fatal("process still alive after graceful stop") + } +} diff --git a/control-plane/internal/cli/proc_windows.go b/control-plane/internal/cli/proc_windows.go new file mode 100644 index 000000000..9cd8aed27 --- /dev/null +++ b/control-plane/internal/cli/proc_windows.go @@ -0,0 +1,38 @@ +//go:build windows + +package cli + +// NOTE: compile-verified only (GOOS=windows cross-build); not yet exercised on +// a real Windows machine. + +import ( + "os" + "os/exec" + "strconv" + "strings" +) + +// signalGracefulStop asks a process to shut down gracefully. Windows cannot +// deliver SIGINT/SIGTERM to an unrelated process (os.Process.Signal only +// supports os.Kill there), so use `taskkill` without /F, which requests the +// target to close. Callers fall back to process.Kill() when it returns an +// error. +func signalGracefulStop(process *os.Process) error { + return exec.Command("taskkill", "/PID", strconv.Itoa(process.Pid)).Run() +} + +// isProcessAlive reports whether the process is still running. On Windows +// os.FindProcess always succeeds and signal-0 probing is unsupported, so ask +// tasklist whether the PID is present. When the probe itself fails, report +// not-alive so callers skip the force-kill rather than killing blindly. +func isProcessAlive(process *os.Process) bool { + out, err := exec.Command( + "tasklist", "/FI", "PID eq "+strconv.Itoa(process.Pid), "/NH", "/FO", "CSV", + ).Output() + if err != nil { + return false + } + // CSV rows quote every field; a live PID appears as "...","",... + // A no-match run prints an INFO message instead and still exits 0. + return strings.Contains(string(out), `"`+strconv.Itoa(process.Pid)+`"`) +} diff --git a/control-plane/internal/cli/stop.go b/control-plane/internal/cli/stop.go index 6e96f3268..f9a13c850 100644 --- a/control-plane/internal/cli/stop.go +++ b/control-plane/internal/cli/stop.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "path/filepath" - "syscall" "time" "github.com/Agent-Field/agentfield/control-plane/internal/packages" @@ -130,8 +129,8 @@ func (as *AgentNodeStopper) StopAgentNode(agentNodeName string) error { if !httpShutdownSuccess { fmt.Printf("🔄 Falling back to process signal shutdown for agent %s\n", agentNodeName) - // Send SIGTERM for graceful shutdown - if err := process.Signal(os.Interrupt); err != nil { + // Ask for graceful shutdown (SIGINT on Unix, taskkill on Windows) + if err := signalGracefulStop(process); err != nil { // If graceful shutdown fails, force kill if err := process.Kill(); err != nil { return fmt.Errorf("failed to kill process: %w", err) @@ -141,7 +140,7 @@ func (as *AgentNodeStopper) StopAgentNode(agentNodeName string) error { time.Sleep(3 * time.Second) // Check if process is still running - if err := process.Signal(syscall.Signal(0)); err == nil { + if isProcessAlive(process) { // Process still running, force kill fmt.Printf("⚠️ Process still running, force killing agent %s\n", agentNodeName) if err := process.Kill(); err != nil { From df6121d7f0b39369dd714889aa01e0b6e6cc065a Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 10 Jul 2026 15:21:06 -0400 Subject: [PATCH 4/6] fix(control-plane): platform-aware log tailing for af logs `af logs` shelled out to tail(1), which does not exist on Windows. The tail/follow commands now go through one tailCommand helper: unchanged tail(1) invocations on Unix, PowerShell Get-Content -Tail (-Wait for follow) on Windows, with proper single-quote escaping of the log path. Windows path is compile-verified only, not yet run on a real machine. Co-Authored-By: Claude Fable 5 --- control-plane/internal/cli/logs.go | 32 ++++++++++++-- control-plane/internal/cli/logs_tail_test.go | 44 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 control-plane/internal/cli/logs_tail_test.go diff --git a/control-plane/internal/cli/logs.go b/control-plane/internal/cli/logs.go index c08917c76..53c9f8c8c 100644 --- a/control-plane/internal/cli/logs.go +++ b/control-plane/internal/cli/logs.go @@ -4,8 +4,10 @@ import ( "errors" "fmt" "os" - "os/exec" // Added missing import + "os/exec" "path/filepath" + "runtime" + "strings" "github.com/Agent-Field/agentfield/control-plane/internal/logger" "github.com/Agent-Field/agentfield/control-plane/internal/packages" @@ -104,7 +106,7 @@ func (lv *LogViewer) ViewLogs(agentNodeName string) error { // tailLogs shows the last N lines of the log file func (lv *LogViewer) tailLogs(logFile string, lines int) error { - cmd := exec.Command("tail", "-n", fmt.Sprintf("%d", lines), logFile) + cmd := tailCommand(logFile, lines, false) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() @@ -112,8 +114,32 @@ func (lv *LogViewer) tailLogs(logFile string, lines int) error { // followLogs follows the log file in real-time func (lv *LogViewer) followLogs(logFile string) error { - cmd := exec.Command("tail", "-f", logFile) + cmd := tailCommand(logFile, 10, true) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } + +// tailCommand builds the platform command that prints the last n lines of a +// file, optionally following it. Unix uses tail(1); Windows has no tail, so +// PowerShell's Get-Content stands in (compile-verified only, not yet tested on +// a real Windows machine). +func tailCommand(logFile string, n int, follow bool) *exec.Cmd { + if runtime.GOOS == "windows" { + script := fmt.Sprintf("Get-Content -LiteralPath %s -Tail %d", psSingleQuote(logFile), n) + if follow { + script += " -Wait" + } + return exec.Command("powershell", "-NoProfile", "-Command", script) + } + if follow { + return exec.Command("tail", "-n", fmt.Sprintf("%d", n), "-f", logFile) + } + return exec.Command("tail", "-n", fmt.Sprintf("%d", n), logFile) +} + +// psSingleQuote quotes s as a PowerShell single-quoted string literal, where +// the only escape is doubling embedded single quotes. +func psSingleQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} diff --git a/control-plane/internal/cli/logs_tail_test.go b/control-plane/internal/cli/logs_tail_test.go new file mode 100644 index 000000000..954093a61 --- /dev/null +++ b/control-plane/internal/cli/logs_tail_test.go @@ -0,0 +1,44 @@ +package cli + +import ( + "runtime" + "strings" + "testing" +) + +// Contract: on Unix, `af logs` shells out to tail(1) with the requested line +// count, adding -f when following. (The Windows branch builds a PowerShell +// Get-Content command instead; it is compile-verified via the windows +// cross-build and exercised only on a real Windows machine.) +func TestTailCommandUnix(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("unix tail arguments are not used on windows") + } + + cmd := tailCommand("/var/log/agent.log", 7, false) + want := []string{"tail", "-n", "7", "/var/log/agent.log"} + if got := strings.Join(cmd.Args, " "); got != strings.Join(want, " ") { + t.Fatalf("tailCommand(no follow) args = %q; want %q", got, strings.Join(want, " ")) + } + + follow := tailCommand("/var/log/agent.log", 10, true) + wantFollow := []string{"tail", "-n", "10", "-f", "/var/log/agent.log"} + if got := strings.Join(follow.Args, " "); got != strings.Join(wantFollow, " ") { + t.Fatalf("tailCommand(follow) args = %q; want %q", got, strings.Join(wantFollow, " ")) + } +} + +// Contract: psSingleQuote produces a PowerShell single-quoted literal where +// embedded single quotes are doubled — the only escape that quoting form has. +func TestPSSingleQuote(t *testing.T) { + cases := map[string]string{ + `C:\logs\agent.log`: `'C:\logs\agent.log'`, + `C:\it's here\a.log`: `'C:\it''s here\a.log'`, + ``: `''`, + } + for in, want := range cases { + if got := psSingleQuote(in); got != want { + t.Errorf("psSingleQuote(%q) = %s; want %s", in, got, want) + } + } +} From bbe63dc09b4d248ae250aad470f63af7753af7db Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 10 Jul 2026 15:21:17 -0400 Subject: [PATCH 5/6] fix(control-plane): .exe-aware Go node binary naming on windows Go agent nodes declare unix-style binary paths in their manifests (entrypoint.start: bin/foo). On Windows the install-time `go build -o` output now carries the conventional .exe extension, and the runner's GoBinaryProgram resolves an extensionless start path to the built .exe when present. No behaviour change on other platforms; windows path is compile-verified only. Co-Authored-By: Claude Fable 5 --- control-plane/internal/packages/gointerp.go | 30 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/control-plane/internal/packages/gointerp.go b/control-plane/internal/packages/gointerp.go index e5e90bf79..8c033f150 100644 --- a/control-plane/internal/packages/gointerp.go +++ b/control-plane/internal/packages/gointerp.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" ) @@ -221,19 +222,31 @@ func (m *PackageMetadata) goBuildTarget() (buildPkg, outBin string) { // The output is the start token when start is a bare binary path (not a // `go run ...` command). Otherwise derive a sensible default under bin/. if len(start) > 0 && start[0] != "go" { - return build, start[0] + return build, withExeSuffix(start[0]) } return build, defaultGoBinName(build) } // defaultGoBinName derives a bin/ output path from a Go build package spec -// like "./cmd/swe-planner" -> "bin/swe-planner". +// like "./cmd/swe-planner" -> "bin/swe-planner" ("bin/swe-planner.exe" on +// Windows). func defaultGoBinName(buildPkg string) string { name := filepath.Base(strings.TrimSpace(buildPkg)) if name == "" || name == "." || name == "/" || name == "..." { name = "app" } - return filepath.Join("bin", name) + return withExeSuffix(filepath.Join("bin", name)) +} + +// withExeSuffix appends ".exe" to a binary path on Windows (manifests declare +// unix-style paths like "bin/swe-planner"; the compiled output must carry the +// conventional executable extension there). A no-op on other platforms and on +// paths that already end in .exe. +func withExeSuffix(path string) string { + if runtime.GOOS == "windows" && !strings.EqualFold(filepath.Ext(path), ".exe") { + return path + ".exe" + } + return path } // hasVendorDir reports whether the package ships a Go vendor/ directory, which @@ -369,7 +382,16 @@ func GoBinaryProgram(packageDir, program string) string { return program } if strings.ContainsRune(program, '/') || strings.ContainsRune(program, filepath.Separator) { - return filepath.Join(packageDir, program) + resolved := filepath.Join(packageDir, program) + // Manifests declare unix-style binary paths ("bin/swe-planner"); on + // Windows the install-time build produced "bin/swe-planner.exe", so + // resolve to that when the extensionless path is absent. + if !fileExists(resolved) { + if withExe := withExeSuffix(resolved); withExe != resolved && fileExists(withExe) { + return withExe + } + } + return resolved } return program } From ba52ae293f2c718fa733fd5bba94a3f477f162fb Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 10 Jul 2026 16:21:25 -0400 Subject: [PATCH 6/6] test(control-plane): cover windows-only branches via goos-parameterized helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The patch-coverage gate flagged the runtime.GOOS-gated windows branches (PowerShell tail construction in logs.go, .exe naming/resolution in gointerp.go) as untestable on linux CI. Extract each into a pure helper taking an explicit goos string — tailCommandArgs, withExeSuffixFor, goBinaryProgramFor — with the exported wrappers passing runtime.GOOS, so behavior is unchanged while both platform paths are unit-testable anywhere. Table-driven tests cover the windows tail command (incl. single-quote escaping), .exe suffixing, and the built-.exe fallback resolution; refactored regions now profile with zero uncovered blocks. Co-Authored-By: Claude Fable 5 --- control-plane/internal/cli/logs.go | 22 ++-- control-plane/internal/cli/logs_tail_test.go | 82 ++++++++++--- .../internal/packages/exe_suffix_test.go | 115 ++++++++++++++++++ control-plane/internal/packages/gointerp.go | 16 ++- 4 files changed, 209 insertions(+), 26 deletions(-) create mode 100644 control-plane/internal/packages/exe_suffix_test.go diff --git a/control-plane/internal/cli/logs.go b/control-plane/internal/cli/logs.go index 53c9f8c8c..4b4fbad72 100644 --- a/control-plane/internal/cli/logs.go +++ b/control-plane/internal/cli/logs.go @@ -121,21 +121,29 @@ func (lv *LogViewer) followLogs(logFile string) error { } // tailCommand builds the platform command that prints the last n lines of a -// file, optionally following it. Unix uses tail(1); Windows has no tail, so -// PowerShell's Get-Content stands in (compile-verified only, not yet tested on -// a real Windows machine). +// file, optionally following it. func tailCommand(logFile string, n int, follow bool) *exec.Cmd { - if runtime.GOOS == "windows" { + program, args := tailCommandArgs(runtime.GOOS, logFile, n, follow) + return exec.Command(program, args...) +} + +// tailCommandArgs returns the program and arguments that tail a log file on +// the given GOOS. Unix uses tail(1); Windows has no tail, so PowerShell's +// Get-Content stands in (compile-verified only, not yet tested on a real +// Windows machine). Pure so both platform branches are unit-testable anywhere. +func tailCommandArgs(goos, logFile string, n int, follow bool) (string, []string) { + if goos == "windows" { script := fmt.Sprintf("Get-Content -LiteralPath %s -Tail %d", psSingleQuote(logFile), n) if follow { script += " -Wait" } - return exec.Command("powershell", "-NoProfile", "-Command", script) + return "powershell", []string{"-NoProfile", "-Command", script} } + args := []string{"-n", fmt.Sprintf("%d", n)} if follow { - return exec.Command("tail", "-n", fmt.Sprintf("%d", n), "-f", logFile) + args = append(args, "-f") } - return exec.Command("tail", "-n", fmt.Sprintf("%d", n), logFile) + return "tail", append(args, logFile) } // psSingleQuote quotes s as a PowerShell single-quoted string literal, where diff --git a/control-plane/internal/cli/logs_tail_test.go b/control-plane/internal/cli/logs_tail_test.go index 954093a61..421d7c396 100644 --- a/control-plane/internal/cli/logs_tail_test.go +++ b/control-plane/internal/cli/logs_tail_test.go @@ -1,30 +1,78 @@ package cli import ( + "reflect" "runtime" - "strings" "testing" ) -// Contract: on Unix, `af logs` shells out to tail(1) with the requested line -// count, adding -f when following. (The Windows branch builds a PowerShell -// Get-Content command instead; it is compile-verified via the windows -// cross-build and exercised only on a real Windows machine.) -func TestTailCommandUnix(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("unix tail arguments are not used on windows") +// Contract: `af logs` tails via tail(1) on Unix and via PowerShell +// Get-Content on Windows (which has no tail), preserving the requested line +// count, the follow flag, and safe quoting of the log path. The helper is +// parameterized by GOOS so both platform branches are exercised regardless of +// the host running the tests. +func TestTailCommandArgs(t *testing.T) { + cases := []struct { + name string + goos string + file string + n int + follow bool + wantProg string + wantArgs []string + }{ + { + name: "unix no follow", goos: "linux", + file: "/var/log/agent.log", n: 7, + wantProg: "tail", + wantArgs: []string{"-n", "7", "/var/log/agent.log"}, + }, + { + name: "unix follow", goos: "darwin", + file: "/var/log/agent.log", n: 10, follow: true, + wantProg: "tail", + wantArgs: []string{"-n", "10", "-f", "/var/log/agent.log"}, + }, + { + name: "windows no follow", goos: "windows", + file: `C:\logs\agent.log`, n: 7, + wantProg: "powershell", + wantArgs: []string{ + "-NoProfile", "-Command", + `Get-Content -LiteralPath 'C:\logs\agent.log' -Tail 7`, + }, + }, + { + name: "windows follow with quote escaping", goos: "windows", + file: `C:\it's here\agent.log`, n: 10, follow: true, + wantProg: "powershell", + wantArgs: []string{ + "-NoProfile", "-Command", + `Get-Content -LiteralPath 'C:\it''s here\agent.log' -Tail 10 -Wait`, + }, + }, } - - cmd := tailCommand("/var/log/agent.log", 7, false) - want := []string{"tail", "-n", "7", "/var/log/agent.log"} - if got := strings.Join(cmd.Args, " "); got != strings.Join(want, " ") { - t.Fatalf("tailCommand(no follow) args = %q; want %q", got, strings.Join(want, " ")) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + prog, args := tailCommandArgs(tc.goos, tc.file, tc.n, tc.follow) + if prog != tc.wantProg { + t.Fatalf("program = %q; want %q", prog, tc.wantProg) + } + if !reflect.DeepEqual(args, tc.wantArgs) { + t.Fatalf("args = %q; want %q", args, tc.wantArgs) + } + }) } +} - follow := tailCommand("/var/log/agent.log", 10, true) - wantFollow := []string{"tail", "-n", "10", "-f", "/var/log/agent.log"} - if got := strings.Join(follow.Args, " "); got != strings.Join(wantFollow, " ") { - t.Fatalf("tailCommand(follow) args = %q; want %q", got, strings.Join(wantFollow, " ")) +// Contract: tailCommand builds an exec.Cmd from the host platform's +// tailCommandArgs — the first Args entry is the program itself. +func TestTailCommandUsesHostGOOS(t *testing.T) { + cmd := tailCommand("/var/log/agent.log", 5, false) + prog, args := tailCommandArgs(runtime.GOOS, "/var/log/agent.log", 5, false) + want := append([]string{prog}, args...) + if !reflect.DeepEqual(cmd.Args, want) { + t.Fatalf("cmd.Args = %q; want %q", cmd.Args, want) } } diff --git a/control-plane/internal/packages/exe_suffix_test.go b/control-plane/internal/packages/exe_suffix_test.go new file mode 100644 index 000000000..7e45d1a61 --- /dev/null +++ b/control-plane/internal/packages/exe_suffix_test.go @@ -0,0 +1,115 @@ +package packages + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +// Contract: compiled Go node binaries get the conventional .exe extension on +// Windows and stay untouched elsewhere; already-suffixed paths (any case) are +// never double-suffixed. +func TestWithExeSuffixFor(t *testing.T) { + cases := []struct { + name string + goos string + in string + want string + }{ + {"windows adds exe", "windows", "bin/swe-planner", "bin/swe-planner.exe"}, + {"windows keeps existing exe", "windows", "bin/swe-planner.exe", "bin/swe-planner.exe"}, + {"windows keeps uppercase exe", "windows", "bin/APP.EXE", "bin/APP.EXE"}, + {"linux untouched", "linux", "bin/swe-planner", "bin/swe-planner"}, + {"darwin untouched", "darwin", "bin/swe-planner", "bin/swe-planner"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := withExeSuffixFor(tc.goos, tc.in); got != tc.want { + t.Fatalf("withExeSuffixFor(%q, %q) = %q; want %q", tc.goos, tc.in, got, tc.want) + } + }) + } +} + +// Contract: an unusable build-package base falls back to bin/app (suffixed on +// Windows like every other derived binary name). +func TestDefaultGoBinNameFallback(t *testing.T) { + want := withExeSuffix(filepath.Join("bin", "app")) + if got := defaultGoBinName("..."); got != want { + t.Fatalf("defaultGoBinName(\"...\") = %q; want %q", got, want) + } +} + +// Contract: on Windows, a manifest's unix-style start path ("bin/app") +// resolves to the .exe the install-time build produced when the extensionless +// file is absent; everywhere else (and whenever the extensionless file exists) +// the plain resolved path wins. +func TestGoBinaryProgramForWindowsExeFallback(t *testing.T) { + writeFile := func(t *testing.T, dir, rel string) { + t.Helper() + p := filepath.Join(dir, rel) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte("bin"), 0o755); err != nil { + t.Fatal(err) + } + } + + t.Run("windows falls back to built exe", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, filepath.Join("bin", "app.exe")) + want := filepath.Join(dir, "bin", "app.exe") + if got := goBinaryProgramFor("windows", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("windows prefers extensionless file when present", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, filepath.Join("bin", "app")) + writeFile(t, dir, filepath.Join("bin", "app.exe")) + want := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("windows", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("windows with neither file returns resolved path", func(t *testing.T) { + dir := t.TempDir() + want := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("windows", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("non-windows never substitutes exe", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, filepath.Join("bin", "app.exe")) + want := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("linux", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("bare and special program tokens pass through", func(t *testing.T) { + dir := t.TempDir() + for _, program := range []string{"", "go", "app"} { + if got := goBinaryProgramFor("windows", dir, program); got != program { + t.Fatalf("goBinaryProgramFor(%q) = %q; want it unchanged", program, got) + } + } + abs := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("windows", dir, abs); got != abs { + t.Fatalf("goBinaryProgramFor(abs) = %q; want %q", got, abs) + } + }) + + t.Run("exported wrapper uses host GOOS", func(t *testing.T) { + dir := t.TempDir() + if got, want := GoBinaryProgram(dir, "bin/app"), goBinaryProgramFor(runtime.GOOS, dir, "bin/app"); got != want { + t.Fatalf("GoBinaryProgram = %q; want %q", got, want) + } + }) +} diff --git a/control-plane/internal/packages/gointerp.go b/control-plane/internal/packages/gointerp.go index 8c033f150..212c7d5dc 100644 --- a/control-plane/internal/packages/gointerp.go +++ b/control-plane/internal/packages/gointerp.go @@ -243,7 +243,13 @@ func defaultGoBinName(buildPkg string) string { // conventional executable extension there). A no-op on other platforms and on // paths that already end in .exe. func withExeSuffix(path string) string { - if runtime.GOOS == "windows" && !strings.EqualFold(filepath.Ext(path), ".exe") { + return withExeSuffixFor(runtime.GOOS, path) +} + +// withExeSuffixFor is withExeSuffix for an explicit GOOS, pure so the Windows +// branch is unit-testable on any platform. +func withExeSuffixFor(goos, path string) string { + if goos == "windows" && !strings.EqualFold(filepath.Ext(path), ".exe") { return path + ".exe" } return path @@ -378,6 +384,12 @@ func applyGoReplaceOverrides(goCmd, packagePath string) error { // already-absolute path is returned unchanged. It is the Go counterpart to the // venv-python substitution the runner does for Python nodes. func GoBinaryProgram(packageDir, program string) string { + return goBinaryProgramFor(runtime.GOOS, packageDir, program) +} + +// goBinaryProgramFor is GoBinaryProgram for an explicit GOOS, pure so the +// Windows .exe fallback is unit-testable on any platform. +func goBinaryProgramFor(goos, packageDir, program string) string { if program == "" || program == "go" || filepath.IsAbs(program) { return program } @@ -387,7 +399,7 @@ func GoBinaryProgram(packageDir, program string) string { // Windows the install-time build produced "bin/swe-planner.exe", so // resolve to that when the extensionless path is absent. if !fileExists(resolved) { - if withExe := withExeSuffix(resolved); withExe != resolved && fileExists(withExe) { + if withExe := withExeSuffixFor(goos, resolved); withExe != resolved && fileExists(withExe) { return withExe } }