diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..192335c63 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "Bash(tee /tmp/launcher-dev.log)", + "Bash(pkill -f \"tsc -p tsconfig\")", + "Bash(pkill -f \"node scripts/dev\")", + "Bash(pkill -f vite)", + "Bash(node -e \"console.log\\(require\\('electron'\\)\\)\")", + "Bash(/home/xiang/openagents/packages/launcher/node_modules/electron/dist/electron --version)", + "Bash(unset ELECTRON_RUN_AS_NODE)", + "Bash(/home/xiang/openagents/packages/launcher/node_modules/electron/dist/electron --version --no-sandbox)", + "Bash(HEADLESS=1 npm run dev)", + "Bash(npm show *)", + "Bash(npm install *)", + "Bash(npm run *)", + "Bash(npx electron-vite *)", + "Bash(sed -i '' -e 's|\"./pages/Dashboard\"|\"./pages/dashboard\"|' -e 's|\"./pages/Agents\"|\"./pages/agents\"|' -e 's|\"./pages/Install\"|\"./pages/install\"|' -e 's|\"./pages/Logs\"|\"./pages/logs\"|' -e 's|\"./pages/Settings\"|\"./pages/settings\"|' /Users/ashin/Documents/Projects/openagents/packages/launcher/src/renderer/App.tsx)", + "Bash(sed -i '' -e 's|\"../Agents/AgentDetail\"|\"../agents/AgentDetail\"|' /Users/ashin/Documents/Projects/openagents/packages/launcher/src/renderer/pages/install/index.tsx)" + ] + } +} diff --git a/packages/launcher/.gitignore b/packages/launcher/.gitignore index f245d70a2..9e011a71f 100644 --- a/packages/launcher/.gitignore +++ b/packages/launcher/.gitignore @@ -1,4 +1,13 @@ -node_modules/ -dist/ -resources/python/ +node_modules +out +dist +.DS_Store *.log + +# TypeScript incremental build artifacts +*.tsbuildinfo + +# Local tool configuration (often contains machine-specific paths) +.claude/ +.vscode/ +.idea/ diff --git a/packages/launcher/README.md b/packages/launcher/README.md deleted file mode 100644 index ad00431bd..000000000 --- a/packages/launcher/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# OpenAgents Desktop Connector - -Electron app for installing, configuring, and managing OpenAgents agents on Windows and macOS. - -**Status: Experimental** - -## What it does - -- Install Python SDK and agent runtimes (OpenClaw) with one click -- Add/configure agents with API keys, models, and workspace connections -- Start/stop agents via the daemon -- View logs and agent status -- System tray for background operation - -## Development - -```bash -cd workspace/apps/desktop-connector -npm install -npm start -``` - -## Build - -```bash -# Windows -npm run build:win - -# macOS -npm run build:mac -``` - -## Architecture - -``` -src/ - main/ # Electron main process - main.js - Window/tray management, IPC - preload.js - Context bridge (renderer ↔ main) - agent-manager.js - Agent CRUD, daemon lifecycle - python-manager.js - Python/SDK detection and install - renderer/ # UI (plain HTML/CSS/JS) - index.html - Dashboard layout - renderer.js - UI logic - styles.css - Dark theme styles -``` - -The app wraps the @openagents-org/agent-launcher library — it runs a Node.js daemon and manages agents via native adapters. No Python required. diff --git a/packages/launcher/electron.vite.config.ts b/packages/launcher/electron.vite.config.ts new file mode 100644 index 000000000..38acebb62 --- /dev/null +++ b/packages/launcher/electron.vite.config.ts @@ -0,0 +1,21 @@ +import { resolve } from 'path' +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()] + }, + preload: { + plugins: [externalizeDepsPlugin()] + }, + renderer: { + resolve: { + alias: { + '@renderer': resolve('src/renderer') + } + }, + plugins: [react(), tailwindcss()] + } +}) diff --git a/packages/launcher/entitlements.mac.plist b/packages/launcher/entitlements.mac.plist index ea3e9d2c7..633882ca4 100644 --- a/packages/launcher/entitlements.mac.plist +++ b/packages/launcher/entitlements.mac.plist @@ -2,17 +2,15 @@ - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.allow-dyld-environment-variables - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.files.user-selected.read-write - + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + diff --git a/packages/launcher/package-lock.json b/packages/launcher/package-lock.json index ea35c878b..176a802f9 100644 --- a/packages/launcher/package-lock.json +++ b/packages/launcher/package-lock.json @@ -1,16 +1,330 @@ { - "name": "openagents-launcher", + "name": "launcher", "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "openagents-launcher", + "name": "launcher", "version": "0.7.1", - "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.460.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.2", + "zustand": "^5.0.0" + }, "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^20.17.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", "electron": "^33.0.0", - "electron-builder": "^25.0.0" + "electron-builder": "^25.0.0", + "electron-vite": "^2.3.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.2", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@develar/schema-utils": { @@ -57,9 +371,9 @@ "license": "MIT" }, "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -134,9 +448,9 @@ } }, "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -207,9 +521,9 @@ } }, "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -274,9 +588,9 @@ } }, "node_modules/@electron/rebuild/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -287,9 +601,9 @@ } }, "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -336,9 +650,9 @@ "license": "MIT" }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -346,9 +660,9 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", "dependencies": { @@ -361,9 +675,9 @@ } }, "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -399,281 +713,1396 @@ "node": ">= 10.0.0" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "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/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 20" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">= 20" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">= 20" } }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, "engines": { - "node": ">= 12.13.0" + "node": ">= 20" } }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 10.0.0" + "node": ">= 20" } }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">= 20" } }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "universalify": "^2.0.0" + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 10.0.0" + "node": ">= 20" } }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">= 20" } }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@npmcli/move-file": { + "node_modules/@tootallnate/once": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", "dev": true, "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">= 10" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "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", - "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "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==", + "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", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "dependencies": { + "@babel/types": "^7.0.0" } }, - "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==", + "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": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "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", - "engines": { - "node": ">= 10" + "dependencies": { + "@babel/types": "^7.28.2" } }, "node_modules/@types/cacheable-request": { @@ -699,6 +2128,13 @@ "@types/ms": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -734,9 +2170,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -755,6 +2191,34 @@ "xmlbuilder": ">=11.0.1" } }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": 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", @@ -784,14 +2248,35 @@ "@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/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", "dev": true, "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, "node_modules/7zip-bin": { @@ -846,9 +2331,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -969,9 +2454,9 @@ } }, "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -982,9 +2467,9 @@ } }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -1199,6 +2684,19 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1238,16 +2736,50 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "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": { - "balanced-match": "^4.0.2" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/buffer": { @@ -1347,9 +2879,9 @@ } }, "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1369,6 +2901,16 @@ "node": ">= 10.0.0" } }, + "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/cacache": { "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", @@ -1407,9 +2949,9 @@ "license": "MIT" }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -1503,6 +3045,27 @@ "node": ">= 0.4" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "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/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1645,6 +3208,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1751,9 +3323,9 @@ "license": "MIT" }, "node_modules/config-file-ts/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -1815,6 +3387,13 @@ "dev": true, "license": "ISC" }, + "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/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1877,6 +3456,13 @@ "node": ">= 8" } }, + "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==", + "devOptional": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2039,9 +3625,9 @@ "license": "MIT" }, "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2096,9 +3682,9 @@ } }, "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2288,9 +3874,9 @@ } }, "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "peer": true, @@ -2328,9 +3914,9 @@ } }, "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2382,9 +3968,9 @@ } }, "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2404,6 +3990,43 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.355", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.355.tgz", + "integrity": "sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.3.0.tgz", + "integrity": "sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "cac": "^6.7.14", + "esbuild": "^0.21.5", + "magic-string": "^0.30.10", + "picocolors": "^1.0.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" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2432,6 +4055,20 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -2506,6 +4143,45 @@ "license": "MIT", "optional": true }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2611,9 +4287,9 @@ "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -2723,6 +4399,21 @@ "dev": true, "license": "ISC" }, + "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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2754,6 +4445,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.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-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2849,9 +4550,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2892,9 +4593,9 @@ } }, "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "optional": true, @@ -3030,9 +4731,9 @@ "license": "ISC" }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -3055,6 +4756,26 @@ "node": ">=10" } }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -3213,9 +4934,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { @@ -3337,6 +5058,22 @@ "node": ">=10" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -3350,6 +5087,19 @@ "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", @@ -3443,29 +5193,290 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "peer": true + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -3526,6 +5537,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -3537,16 +5560,32 @@ } }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "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": "^4.0.0" - }, - "engines": { - "node": ">=10" + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.460.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz", + "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "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/make-fetch-happen": { @@ -3710,13 +5749,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -3780,11 +5819,11 @@ } }, "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "minipass": "^3.0.0" }, @@ -3818,6 +5857,13 @@ "node": ">=8" } }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", @@ -3832,6 +5878,13 @@ "node": ">= 8" } }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -3852,6 +5905,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -3863,9 +5935,9 @@ } }, "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3876,9 +5948,9 @@ } }, "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -3907,9 +5979,9 @@ } }, "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -3946,9 +6018,9 @@ } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -3958,6 +6030,13 @@ "node": ">=10" } }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -4209,13 +6288,13 @@ "license": "ISC" }, "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", "dev": true, "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", + "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, @@ -4223,6 +6302,35 @@ "node": ">=10.4.0" } }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -4296,6 +6404,41 @@ "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==", + "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==", + "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/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -4344,9 +6487,9 @@ "peer": true }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "peer": true, @@ -4476,6 +6619,51 @@ "node": ">=8.0" } }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4524,6 +6712,15 @@ "node": ">=11.0.0" } }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "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", @@ -4610,9 +6807,9 @@ } }, "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -4650,13 +6847,13 @@ } }, "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -4702,6 +6899,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -4838,6 +7045,37 @@ "node": ">=8" } }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -4885,6 +7123,13 @@ "node": ">=8" } }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", @@ -4912,9 +7157,9 @@ } }, "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4964,6 +7209,14 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -5035,6 +7288,37 @@ "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/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5075,6 +7359,66 @@ "node": ">=0.6.0" } }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -5176,9 +7520,9 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, @@ -5273,6 +7617,35 @@ "engines": { "node": ">= 10" } + }, + "node_modules/zustand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/packages/launcher/package.json b/packages/launcher/package.json index af3cb7a7e..c98acfa1b 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -1,49 +1,82 @@ { - "name": "openagents-launcher", + "name": "launcher", "version": "0.7.1", - "description": "OpenAgents Launcher — install, configure, and manage your AI agents", - "main": "src/main/main.js", + "description": "OpenAgents Launcher", + "main": "out/main/index.js", "scripts": { - "start": "electron .", - "start:headless": "electron . --headless", - "dev": "electron . --remote-debugging-port=9223 --remote-allow-origins=*", - "dev:nogpu": "electron . --remote-debugging-port=9223 --remote-allow-origins=* --disable-gpu", - "dev:headless": "electron . --remote-debugging-port=9223 --remote-allow-origins=* --headless", - "build:win": "electron-builder --win --publish never", - "build:mac": "electron-builder --mac --x64 --arm64 --publish never", - "build:mac:x64": "electron-builder --mac --x64 --publish never", - "build:mac:arm64": "electron-builder --mac --arm64 --publish never", - "build:linux": "electron-builder --linux --publish never", - "build:all": "electron-builder --win --mac --linux --publish never", - "dist": "electron-builder --publish always" + "dev": "electron-vite dev", + "build": "electron-vite build", + "preview": "electron-vite preview", + "build:mac": "npm run build && electron-builder --mac --x64 --arm64 --publish never", + "build:mac:x64": "npm run build && electron-builder --mac --x64 --publish never", + "build:mac:arm64": "npm run build && electron-builder --mac --arm64 --publish never", + "build:win": "npm run build && electron-builder --win --publish never", + "build:linux": "npm run build && electron-builder --linux --publish never", + "build:all": "npm run build && electron-builder --win --mac --linux --publish never", + "dist": "npm run build && electron-builder --publish always" }, - "author": { - "name": "OpenAgents", - "email": "team@openagents.org" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/openagents-org/openagents.git", - "directory": "packages/launcher" + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.460.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.2", + "zustand": "^5.0.0" }, "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^20.17.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", "electron": "^33.0.0", - "electron-builder": "^25.0.0" + "electron-builder": "^25.0.0", + "electron-vite": "^2.3.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.2", + "vite": "^5.4.8" }, - "dependencies": {}, "build": { "appId": "com.openagents.launcher", "productName": "OpenAgents Launcher", "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", - "directories": { - "output": "dist" - }, "files": [ - "src/**/*", - "node_modules/**/*", + "out/**/*", "package.json" ], + "directories": { + "buildResources": "assets", + "output": "dist" + }, + "mac": { + "target": [ + { + "target": "dmg", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "zip", + "arch": [ + "x64", + "arm64" + ] + } + ], + "icon": "assets/icon.icns", + "category": "public.app-category.developer-tools", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "entitlements.mac.plist", + "entitlementsInherit": "entitlements.mac.plist", + "identity": "ZHONGYUAN ZHU (AP69DBR725)", + "notarize": true + }, + "dmg": { + "title": "${productName} ${version}" + }, "win": { "target": [ { @@ -73,28 +106,6 @@ "installerIcon": "assets/icon.ico", "uninstallerIcon": "assets/icon.ico" }, - "mac": { - "target": [ - { - "target": "dmg", - "arch": [ - "x64", - "arm64" - ] - } - ], - "icon": "assets/icon.icns", - "category": "public.app-category.developer-tools", - "identity": "ZHONGYUAN ZHU (AP69DBR725)", - "hardenedRuntime": true, - "gatekeeperAssess": false, - "entitlements": "entitlements.mac.plist", - "entitlementsInherit": "entitlements.mac.plist", - "notarize": true - }, - "dmg": { - "title": "${productName} ${version}" - }, "linux": { "target": [ { diff --git a/packages/launcher/scripts/azure-sign.js b/packages/launcher/scripts/azure-sign.js deleted file mode 100644 index a8a9d63f5..000000000 --- a/packages/launcher/scripts/azure-sign.js +++ /dev/null @@ -1,53 +0,0 @@ -const { execSync } = require("child_process"); -const path = require("path"); - -// Custom sign function for electron-builder using Azure Trusted Signing. -// Called automatically by electron-builder during the Windows build process. -// -// Required environment variables: -// AZURE_TENANT_ID - Azure AD tenant ID -// AZURE_CLIENT_ID - Azure AD app registration client ID -// AZURE_CLIENT_SECRET - Azure AD app registration client secret -// AZURE_ENDPOINT - Trusted Signing endpoint (e.g. https://wus.codesigning.azure.net) -// AZURE_CODE_SIGNING_ACCOUNT - Trusted Signing account name (e.g. "openagents") -// AZURE_CERT_PROFILE - Certificate profile name - -exports.default = async function azureSign(configuration) { - const filePath = configuration.path; - - // Skip if not an executable/signable file - if (!/\.(exe|dll|msi|msix|appx)$/i.test(filePath)) { - return; - } - - const endpoint = process.env.AZURE_ENDPOINT; - const account = process.env.AZURE_CODE_SIGNING_ACCOUNT; - const certProfile = process.env.AZURE_CERT_PROFILE; - - if (!endpoint || !account || !certProfile) { - console.warn("Azure Trusted Signing env vars not set, skipping signing."); - return; - } - - console.log(`Signing: ${path.basename(filePath)}`); - - const args = [ - "trusted-signing", - "-e", endpoint, - "-a", account, - "-c", certProfile, - "-r", "http://timestamp.acs.microsoft.com", - "-d", "sha256", - filePath, - ]; - - try { - execSync(`sign code ${args.join(" ")}`, { - stdio: "inherit", - timeout: 120000, - }); - } catch (err) { - console.error(`Failed to sign ${path.basename(filePath)}: ${err.message}`); - throw err; - } -}; diff --git a/packages/launcher/src/main/agent-manager.js b/packages/launcher/src/main/agent-manager.js deleted file mode 100644 index d8dcb92c0..000000000 --- a/packages/launcher/src/main/agent-manager.js +++ /dev/null @@ -1,646 +0,0 @@ -/** - * Agent manager for the OpenAgents Launcher. - * - * Thin adapter over @openagents-org/agent-launcher — provides the same - * IPC-facing API as the old Python-based agent-manager, but all operations - * are now pure Node.js (no Python subprocess calls). - */ - -const path = require('path'); -const fs = require('fs'); -const os = require('os'); - -const CONFIG_DIR = path.join(os.homedir(), '.openagents'); -// All platforms use --prefix, so node_modules/ is always the location -const GLOBAL_CORE = path.join(CONFIG_DIR, 'nodejs', 'node_modules', '@openagents-org', 'agent-launcher'); -const LOCAL_CORE = path.resolve(__dirname, '../../../agent-connector'); - -// Load core library from global install (not bundled asar) -function loadCore() { - const isDev = !process.versions.electron || process.defaultApp || process.argv.includes('--dev'); - // In development, prefer local source over global install - if (fs.existsSync(path.join(LOCAL_CORE, 'package.json'))) { - try { return require(LOCAL_CORE); } catch (e) { - console.error('Failed to load local core:', e); - } - } - if (fs.existsSync(path.join(GLOBAL_CORE, 'package.json'))) { - try { return require(GLOBAL_CORE); } catch {} - } - // Fallback to bundled (for dev mode or if global not yet installed) - try { return require('@openagents-org/agent-launcher'); } catch {} - return null; -} - -let core = loadCore(); - -class AgentManager { - constructor(store) { - this._store = store; - this._healthByType = new Map(); - this._healthRefreshInFlight = new Set(); - this._lastHealthRefreshAt = 0; - if (!core) core = loadCore(); - if (core) { - this._connector = new core.AgentConnector({ configDir: CONFIG_DIR }); - } else { - // Core not available yet — will be initialized after install - this._connector = null; - } - } - - getSupportedAgentTypes() { - const supported = core?.adapters?.ADAPTER_MAP - ? Object.keys(core.adapters.ADAPTER_MAP) - : []; - return supported.sort(); - } - - getCoreInfo() { - return { - version: this.coreVersion, - supportedTypes: this.getSupportedAgentTypes(), - globalCorePath: GLOBAL_CORE, - globalCorePresent: fs.existsSync(path.join(GLOBAL_CORE, 'package.json')), - }; - } - - /** Reload core library after install/update */ - reloadCore() { - // Clear require cache for global path - const cacheKeys = Object.keys(require.cache).filter(k => k.includes('agent-launcher') || k.includes('agent-connector')); - for (const k of cacheKeys) delete require.cache[k]; - core = loadCore(); - if (core) { - this._connector = new core.AgentConnector({ configDir: CONFIG_DIR }); - } - return !!core; - } - - get coreVersion() { - try { - const pkg = path.join(LOCAL_CORE, 'package.json'); - if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg, 'utf-8')).version; - } catch {} - try { - const pkg = path.join(GLOBAL_CORE, 'package.json'); - if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg, 'utf-8')).version; - } catch {} - // Fallback to bundled - try { return require('@openagents-org/agent-launcher/package.json').version; } catch {} - return null; - } - - _ensureConnector() { - if (!this._connector) { - if (!this.reloadCore()) { - throw new Error('Core library not installed. Install an agent first via the Install tab.'); - } - } - } - - // ------------------------------------------------------------------ - // Agent listing (merges config + status + env) - // ------------------------------------------------------------------ - - getAgents() { - if (!this._connector) return []; - const agents = this._connector.listAgents(); - const status = this.getAllStatus(); - this._scheduleHealthRefresh(agents); - - const supportedTypes = new Set(this.getSupportedAgentTypes()); - return agents.map((a) => { - const type = a.type || 'openclaw'; - const runtimeMismatch = !supportedTypes.has(type); - const runtimeMessage = runtimeMismatch - ? `Agent runtime '${type}' is not available in the currently loaded core. This usually means the Launcher core is outdated or did not reload correctly. Update Launcher and restart it.` - : null; - const statusError = status[a.name]?.last_error || null; - return { - ...a, - state: status[a.name]?.state || 'stopped', - restarts: status[a.name]?.restarts || 0, - lastError: statusError || runtimeMessage, - health: this._healthByType.get(type) || null, - runtimeMismatch, - }; - }); - } - - _scheduleHealthRefresh(agents) { - const now = Date.now(); - if (now - this._lastHealthRefreshAt < 3000) return; - this._lastHealthRefreshAt = now; - - const types = [...new Set((agents || []).map((agent) => agent.type || 'openclaw'))]; - for (const type of types) { - if (this._healthRefreshInFlight.has(type)) continue; - this._healthRefreshInFlight.add(type); - setTimeout(() => { - try { - const health = this._connector ? this._connector.healthCheck(type) : null; - this._healthByType.set(type, health); - } catch { - this._healthByType.set(type, null); - } finally { - this._healthRefreshInFlight.delete(type); - } - }, 0); - } - } - - // ------------------------------------------------------------------ - // Agent CRUD - // ------------------------------------------------------------------ - - async addAgent(agentConfig) { - const name = agentConfig.name; - const type = agentConfig.type || 'openclaw'; - const supportedTypes = this.getSupportedAgentTypes(); - - if (supportedTypes.length > 0 && !supportedTypes.includes(type)) { - throw new Error(`Agent type '${type}' is not supported in Launcher yet. Supported: ${supportedTypes.join(', ')}`); - } - - this._connector.addAgent({ name, type, role: 'worker', path: agentConfig.path, env: agentConfig.env }); - - return { success: true, agent: agentConfig }; - } - - async removeAgent(name) { - try { await this.stopAgent(name); } catch {} - this._connector.removeAgent(name); - return { success: true }; - } - - async updateAgent(name, updates) { - if (updates.env) { - this._connector.saveAgentInstanceEnv(name, updates.env); - } - return { success: true }; - } - - // ------------------------------------------------------------------ - // Agent catalog & env config - // ------------------------------------------------------------------ - - async getCatalog() { - let catalog; - try { - catalog = await this._connector.getCatalog(); - } catch { - catalog = this._connector.registry.getCatalogSync().map((e) => { - const info = this._connector.installer.getInstallInfo(e.name); - return { ...e, installed: info.installed, managed: info.managed, location: info.location }; - }); - } - // Ensure bundled fields (check_ready, env_config, launch) are always present - const bundled = this._connector.registry._loadBundled(); - for (const entry of catalog) { - const b = bundled.find(x => x.name === entry.name); - if (b) { - if (!entry.check_ready && b.check_ready) entry.check_ready = b.check_ready; - if ((!entry.env_config || !entry.env_config.length) && b.env_config?.length) entry.env_config = b.env_config; - if (!entry.install && b.install) entry.install = b.install; - if (!entry.launch && b.launch) entry.launch = b.launch; - } - } - return catalog; - } - - async getEnvFields(agentType) { - return this._connector.getEnvFields(agentType); - } - - getAgentEnv(agentType) { - return this._connector.getAgentEnv(agentType); - } - - getAgentInstanceEnv(agentName) { - return this._connector.getAgentInstanceEnv(agentName); - } - - saveAgentEnv(agentType, env) { - this._connector.saveAgentEnv(agentType, env); - - // Configure agent-specific native auth (e.g., OpenClaw's auth-profiles.json) - try { - if (agentType === 'openclaw') { - const OpenClawAdapter = require('@openagents-org/agent-launcher/src/adapters/openclaw'); - OpenClawAdapter.configureNativeAuth(env); - } - } catch {} - - this.signalReload(); - return { success: true }; - } - - saveAgentInstanceEnv(agentName, env) { - this._connector.saveAgentInstanceEnv(agentName, env); - this.signalReload(); - return { success: true }; - } - - async testLLM(env) { - return this._connector.testLLM(env); - } - - signalReload() { - const pid = this._connector.getDaemonPid(); - if (!pid) return; - - if (process.platform === 'win32') { - this._connector.sendDaemonCommand('reload'); - } else { - try { process.kill(pid, 'SIGHUP'); } catch {} - } - } - - // ------------------------------------------------------------------ - // Workspace - // ------------------------------------------------------------------ - - getNetworks() { - return this._connector.listWorkspaces(); - } - - async createWorkspace(name) { - return this._connector.createWorkspace({ name: name || 'My Workspace' }); - } - - async connectWorkspace(agentName, tokenOrSlug) { - // Resolve the token to get workspace info (slug, name, id) - try { - const info = await this._connector.resolveToken(tokenOrSlug); - const slug = info.slug || info.workspace_id; - const wsName = info.name || slug; - - // Save network to config - this._connector.config.addNetwork({ - id: info.workspace_id, - slug, - name: wsName, - endpoint: info.endpoint || this._connector.workspace?.endpoint, - token: tokenOrSlug, - }); - - // Connect agent to the resolved slug - this._connector.connectWorkspace(agentName, slug); - } catch { - // Fallback: treat as slug directly - this._connector.connectWorkspace(agentName, tokenOrSlug); - } - this.signalReload(); - return { success: true }; - } - - async disconnectWorkspace(agentName) { - this._connector.disconnectWorkspace(agentName); - this.signalReload(); - return { success: true }; - } - - async removeWorkspace(slug) { - const result = await this._connector.removeWorkspace(slug); - this.signalReload(); - return result; - } - - // ------------------------------------------------------------------ - // Agent type install / uninstall - // ------------------------------------------------------------------ - - async checkAgentType(agentType) { - const installed = this._connector.isInstalled(agentType); - const binary = installed ? this._connector.installer.which(agentType) : null; - return { installed, binary: binary || null }; - } - - async installAgentType(agentType) { - return this._connector.install(agentType); - } - - async installAgentTypeStreaming(agentType, onData) { - const result = await this._connector.installer.installStreaming(agentType, onData); - this._connector.clearCatalogCache(); - return result; - } - - async uninstallAgentType(agentType) { - const result = await this._connector.uninstall(agentType); - this._connector.clearCatalogCache(); - return result; - } - - async uninstallAgentTypeStreaming(agentType, onData) { - const result = await this._connector.installer.uninstallStreaming(agentType, onData); - this._connector.clearCatalogCache(); - return result; - } - - // ------------------------------------------------------------------ - // Daemon lifecycle - // ------------------------------------------------------------------ - - async startAgent(name) { - // Ensure daemon is running (long-lived background process) - await this._ensureDaemon(); - // Send start command — daemon will launch the agent's adapter - this._connector.sendDaemonCommand(`start:${name}`); - return { success: true, message: `Start command sent for ${name}` }; - } - - async stopAgent(name) { - const pid = this._connector.getDaemonPid(); - if (!pid) { - return { success: true, message: 'Daemon not running' }; - } - // Send stop command — daemon stops only this agent, keeps running - this._connector.sendDaemonCommand(`stop:${name}`); - return { success: true, message: `Stop command sent for ${name}` }; - } - - async startAll() { - // Ensure daemon is running, then restart all agents - await this._ensureDaemon(); - this._connector.sendDaemonCommand('reload'); - return { success: true, message: 'Start all command sent' }; - } - - async stopAll() { - const stopped = this._connector.stopDaemon(); - return { success: stopped, message: stopped ? 'Daemon stopped' : 'Daemon not running' }; - } - - /** - * Ensure the daemon is running. Start it if not. - * The daemon is a long-lived process — it stays running and - * individual agents are started/stopped via commands. - */ - async _ensureDaemon() { - const pid = this._connector.getDaemonPid(); - if (pid) { - // Daemon already running — don't restart (avoids status flicker) - return; - } - // Don't attempt if Node.js isn't installed yet - const portableNodeDir = path.join(os.homedir(), '.openagents', 'nodejs'); - // Check both unified path (symlink) and legacy platform-specific path - const nodeBin = path.join(portableNodeDir, 'node' + (process.platform === 'win32' ? '.exe' : '')); - const nodeBinLegacy = path.join(portableNodeDir, 'bin', 'node'); - if (!fs.existsSync(nodeBin) && !fs.existsSync(nodeBinLegacy)) return; - - return this._startDaemon(); - } - - getAllStatus() { - // Read status file directly — PID validation is unreliable in Electron - // on Windows (cross-session process.kill, fs.existsSync races). - // The daemon cleans up its own status file on exit. - return this._connector.getDaemonStatus(); - } - - getLogs(name, lines = 200) { - const logLines = this._connector.getLogs(name, lines); - return { lines: logLines }; - } - - tailLogs(name, lines = 200, offset = 0) { - return this._connector.config.tailLogs({ agent: name || undefined, lines, offset }); - } - - clearLogsInRange(start, end) { - const startTime = normalizeTimeValue(start); - const endTime = normalizeTimeValue(end); - - if (!startTime || !endTime) { - throw new Error('Start time and end time are required'); - } - if (startTime.getTime() > endTime.getTime()) { - throw new Error('Start time must be before end time'); - } - - const logFile = path.join(CONFIG_DIR, 'daemon.log'); - if (!fs.existsSync(logFile)) { - return { removed: 0, remaining: 0 }; - } - - const content = fs.readFileSync(logFile, 'utf-8'); - const hasTrailingNewline = content.endsWith('\n'); - const allLines = content.split('\n'); - if (hasTrailingNewline) allLines.pop(); - const { keptLines, removed } = filterLogsByTimeRange(allLines, startTime, endTime); - - const nextContent = keptLines.join('\n') + (hasTrailingNewline && keptLines.length > 0 ? '\n' : ''); - const tempFile = `${logFile}.tmp`; - fs.writeFileSync(tempFile, nextContent, 'utf-8'); - fs.renameSync(tempFile, logFile); - - return { removed, remaining: keptLines.length }; - } - - healthCheck(type) { - return this._connector.healthCheck(type); - } - - // ------------------------------------------------------------------ - // Internal - // ------------------------------------------------------------------ - - _startDaemon() { - try { this._connector.stopDaemon(); } catch {} - - const { spawn } = require('child_process'); - const portableNodeDir = path.join(os.homedir(), '.openagents', 'nodejs'); - - // Build enhanced PATH with portable Node.js and agent binaries - const openagentsDir = path.join(os.homedir(), '.openagents'); - const extraDirs = [ - portableNodeDir, // node, npm (unified via symlinks) - path.join(portableNodeDir, 'bin'), // legacy macOS/Linux node, npm - ]; - // Add per-agent runtime bin dirs (~/.openagents/runtimes//node_modules/.bin) - const runtimesDir = path.join(openagentsDir, 'runtimes'); - try { - for (const d of fs.readdirSync(runtimesDir, { withFileTypes: true })) { - if (d.isDirectory()) extraDirs.push(path.join(runtimesDir, d.name, 'node_modules', '.bin')); - } - } catch {} - // Core library bin - extraDirs.push(path.join(openagentsDir, 'core', 'node_modules', '.bin')); - // Legacy shared bin - extraDirs.push(path.join(portableNodeDir, 'node_modules', '.bin')); - if (process.platform === 'win32') { - extraDirs.push(path.join(process.env.APPDATA || '', 'npm')); - // Include custom npm global prefix (e.g. D:\node\node_global) - try { - const { execSync } = require('child_process'); - const npmPrefix = execSync('npm config get prefix', { - encoding: 'utf-8', timeout: 5000, windowsHide: true, - }).trim(); - if (npmPrefix && !extraDirs.includes(npmPrefix)) { - extraDirs.push(npmPrefix); - } - } catch {} - } - const enhancedPath = [...extraDirs, process.env.PATH || ''].join(path.delimiter); - - // Find CLI entry point on disk (NOT in asar) - // All platforms use --prefix ~/.openagents/nodejs → node_modules/ - let cliPath = null; - const cliCandidates = [ - path.join(LOCAL_CORE, 'bin', 'agent-connector.js'), - path.join(portableNodeDir, 'node_modules', '@openagents-org', 'agent-launcher', 'bin', 'agent-connector.js'), - ]; - for (const c of cliCandidates) { - try { if (fs.existsSync(c)) { cliPath = c; break; } } catch {} - } - if (!cliPath) { - return { success: false, message: 'agent-launcher CLI not found. Install an agent first via the Install tab.' }; - } - - // Find node binary - let nodeBin = path.join(portableNodeDir, 'node' + (process.platform === 'win32' ? '.exe' : '')); - if (!fs.existsSync(nodeBin)) { - try { - const { execSync } = require('child_process'); - nodeBin = execSync(process.platform === 'win32' ? 'where node' : 'which node', - { encoding: 'utf-8', timeout: 5000, env: { ...process.env, PATH: enhancedPath } }).split(/\r?\n/)[0].trim(); - } catch { nodeBin = 'node'; } - } - - // Spawn daemon as detached background process - try { - fs.mkdirSync(CONFIG_DIR, { recursive: true }); - const logFile = path.join(CONFIG_DIR, 'daemon.log'); - const pidFile = path.join(CONFIG_DIR, 'daemon.pid'); - const logFd = fs.openSync(logFile, 'a'); - - const proc = spawn(nodeBin, [cliPath, 'up', '--foreground'], { - detached: true, - stdio: ['ignore', logFd, logFd], - env: { ...process.env, PATH: enhancedPath }, - windowsHide: true, - }); - proc.unref(); - fs.writeFileSync(pidFile, String(proc.pid), 'utf-8'); - fs.closeSync(logFd); - - return { success: true, pid: proc.pid, message: `Daemon started (PID ${proc.pid})` }; - } catch (e) { - return { success: false, message: `Failed to start daemon: ${e.message}` }; - } - } -} - -function normalizeTimeValue(value) { - if (value instanceof Date) { - return Number.isNaN(value.getTime()) ? null : value; - } - if (typeof value === 'number') { - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; - } - if (typeof value === 'string' && value.trim()) { - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; - } - return null; -} - -function filterLogsByTimeRange(lines, start, end) { - const headerTimes = resolveLogHeaderTimestamps(lines, end); - let activeRemove = false; - let removed = 0; - const keptLines = []; - - for (let index = 0; index < lines.length; index += 1) { - const headerTime = headerTimes[index]; - if (headerTime) { - const time = headerTime.getTime(); - activeRemove = time >= start.getTime() && time <= end.getTime(); - } - - if (activeRemove) { - removed += 1; - } else { - keptLines.push(lines[index]); - } - } - - return { keptLines, removed }; -} - -function resolveLogHeaderTimestamps(lines, referenceTime) { - const resolved = new Array(lines.length).fill(null); - let currentDay = startOfLocalDay(referenceTime); - let lastClockSeconds = null; - - for (let index = lines.length - 1; index >= 0; index -= 1) { - const token = parseLogTimestampToken(lines[index]); - if (!token) continue; - - if (token.kind === 'iso') { - resolved[index] = token.date; - currentDay = startOfLocalDay(token.date); - lastClockSeconds = ( - token.date.getHours() * 3600 + - token.date.getMinutes() * 60 + - token.date.getSeconds() - ); - continue; - } - - if (lastClockSeconds !== null && token.seconds > lastClockSeconds) { - currentDay = addLocalDays(currentDay, -1); - } - - resolved[index] = withLocalClock(currentDay, token.seconds); - lastClockSeconds = token.seconds; - } - - return resolved; -} - -function parseLogTimestampToken(line) { - if (!line) return null; - - const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))/); - if (isoMatch) { - const date = new Date(isoMatch[1]); - if (!Number.isNaN(date.getTime())) { - return { kind: 'iso', date }; - } - } - - const clockMatch = line.match(/^\[(\d{2}):(\d{2}):(\d{2})\]/); - if (clockMatch) { - return { - kind: 'clock', - seconds: - Number(clockMatch[1]) * 3600 + - Number(clockMatch[2]) * 60 + - Number(clockMatch[3]), - }; - } - - return null; -} - -function startOfLocalDay(date) { - return new Date(date.getFullYear(), date.getMonth(), date.getDate()); -} - -function addLocalDays(date, days) { - return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days); -} - -function withLocalClock(day, seconds) { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - return new Date(day.getFullYear(), day.getMonth(), day.getDate(), hours, minutes, secs); -} - -module.exports = { AgentManager }; diff --git a/packages/launcher/src/main/agent-manager.ts b/packages/launcher/src/main/agent-manager.ts new file mode 100644 index 000000000..d95dd2f1b --- /dev/null +++ b/packages/launcher/src/main/agent-manager.ts @@ -0,0 +1,894 @@ +import path from 'path' +import fs from 'fs' +import os from 'os' +import https from 'https' + +const CONFIG_DIR = path.join(os.homedir(), '.openagents') +const GLOBAL_CORE = path.join(CONFIG_DIR, 'nodejs', 'node_modules', '@openagents-org', 'agent-launcher') +const LOCAL_CORE = path.resolve(__dirname, '../../../agent-connector') +const INSTALLED_HISTORY_FILE = path.join(CONFIG_DIR, 'installed_agents_history.json') + +export interface InstalledAgentRecord { + name: string + version: string | null + installedAt: string + previousVersion?: string | null + history?: Array<{ version: string; installedAt: string }> +} + +interface NpmRegistryInfo { + 'dist-tags'?: { latest?: string } + versions?: Record + time?: Record + homepage?: string +} + +function loadCore(): Record | null { + if (fs.existsSync(path.join(LOCAL_CORE, 'package.json'))) { + try { return require(LOCAL_CORE) } catch (e) { + console.error('Failed to load local core:', e) + } + } + if (fs.existsSync(path.join(GLOBAL_CORE, 'package.json'))) { + try { return require(GLOBAL_CORE) } catch {} + } + try { return require('@openagents-org/agent-launcher') } catch {} + return null +} + +let core: Record | null = loadCore() + +export class AgentManager { + private _store: unknown + private _healthByType = new Map() + private _healthRefreshInFlight = new Set() + private _lastHealthRefreshAt = 0 + private _healthQueue: string[] = [] + private _healthProcessing = false + private _statusCache: { value: unknown; at: number } = { value: {}, at: 0 } + _connector: Record | null = null + + constructor(store: unknown) { + this._store = store + if (!core) core = loadCore() + if (core) { + const AgentConnector = (core as Record).AgentConnector as new (opts: unknown) => Record + this._connector = new AgentConnector({ configDir: CONFIG_DIR }) + } + } + + getSupportedAgentTypes(): string[] { + const supported = (core as Record | null)?.adapters + ? Object.keys(((core as Record).adapters as Record).ADAPTER_MAP as Record) + : [] + return (supported as string[]).sort() + } + + getCoreInfo(): unknown { + return { + version: this.coreVersion, + supportedTypes: this.getSupportedAgentTypes(), + globalCorePath: GLOBAL_CORE, + globalCorePresent: fs.existsSync(path.join(GLOBAL_CORE, 'package.json')), + } + } + + reloadCore(): boolean { + const cacheKeys = Object.keys(require.cache).filter(k => k.includes('agent-launcher') || k.includes('agent-connector')) + for (const k of cacheKeys) delete require.cache[k] + core = loadCore() + if (core) { + const AgentConnector = (core as Record).AgentConnector as new (opts: unknown) => Record + this._connector = new AgentConnector({ configDir: CONFIG_DIR }) + } + return !!core + } + + get coreVersion(): string | null { + try { + const pkg = path.join(LOCAL_CORE, 'package.json') + if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg, 'utf-8')).version + } catch {} + try { + const pkg = path.join(GLOBAL_CORE, 'package.json') + if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg, 'utf-8')).version + } catch {} + try { return require('@openagents-org/agent-launcher/package.json').version } catch {} + return null + } + + private _ensureConnector(): void { + if (!this._connector) { + if (!this.reloadCore()) { + throw new Error('Core library not installed. Install an agent first via the Install tab.') + } + } + } + + getAgents(): unknown[] { + if (!this._connector) return [] + const listAgents = this._connector.listAgents as () => unknown[] + const agents = listAgents.call(this._connector) + const status = this.getAllStatus() as Record + this._scheduleHealthRefresh(agents as Array<{ type?: string; name: string }>) + + const supportedTypes = new Set(this.getSupportedAgentTypes()) + return (agents as Array>).map((a) => { + const type = (a.type as string) || 'openclaw' + const runtimeMismatch = !supportedTypes.has(type) + const runtimeMessage = runtimeMismatch + ? `Agent runtime '${type}' is not available in the currently loaded core. Update Launcher and restart it.` + : null + const statusEntry = status[a.name as string] + const statusError = statusEntry?.last_error || null + return { + ...a, + state: statusEntry?.state || 'stopped', + restarts: statusEntry?.restarts || 0, + lastError: statusError || runtimeMessage, + health: this._healthByType.get(type) || null, + runtimeMismatch, + } + }) + } + + private _scheduleHealthRefresh(agents: Array<{ type?: string; name: string }>): void { + const now = Date.now() + if (now - this._lastHealthRefreshAt < 30_000) return + this._lastHealthRefreshAt = now + + const types = [...new Set((agents || []).map(a => a.type || 'openclaw'))] + for (const type of types) { + if (this._healthRefreshInFlight.has(type)) continue + if (this._healthQueue.includes(type)) continue + this._healthRefreshInFlight.add(type) + this._healthQueue.push(type) + } + this._processHealthQueue() + } + + private _processHealthQueue(): void { + if (this._healthProcessing) return + this._healthProcessing = true + const tick = (): void => { + const type = this._healthQueue.shift() + if (!type) { this._healthProcessing = false; return } + setTimeout(() => { + try { + const healthCheck = this._connector?.healthCheck as ((type: string) => unknown) | undefined + const health = healthCheck ? healthCheck.call(this._connector, type) : null + this._healthByType.set(type, health) + } catch { + this._healthByType.set(type, null) + } finally { + this._healthRefreshInFlight.delete(type) + } + setTimeout(tick, 250) + }, 0) + } + tick() + } + + async addAgent(agentConfig: { name: string; type?: string; path?: string; env?: Record }): Promise { + const name = agentConfig.name + const type = agentConfig.type || 'openclaw' + const supportedTypes = this.getSupportedAgentTypes() + + if (supportedTypes.length > 0 && !supportedTypes.includes(type)) { + throw new Error(`Agent type '${type}' is not supported. Supported: ${supportedTypes.join(', ')}`) + } + + const addAgent = this._connector!.addAgent as (opts: unknown) => void + addAgent.call(this._connector, { name, type, role: 'worker', path: agentConfig.path, env: agentConfig.env }) + return { success: true, agent: agentConfig } + } + + async removeAgent(name: string): Promise { + try { await this.stopAgent(name) } catch {} + const removeAgent = this._connector!.removeAgent as (name: string) => void + removeAgent.call(this._connector, name) + return { success: true } + } + + async updateAgent(name: string, updates: { env?: Record }): Promise { + if (updates.env) { + const saveEnv = this._connector!.saveAgentInstanceEnv as (name: string, env: unknown) => void + saveEnv.call(this._connector, name, updates.env) + } + return { success: true } + } + + async getCatalog(): Promise { + let catalog: unknown[] + try { + const getCatalog = this._connector!.getCatalog as () => Promise + catalog = await getCatalog.call(this._connector) + } catch { + const registry = this._connector!.registry as Record + const getCatalogSync = registry.getCatalogSync as () => unknown[] + catalog = getCatalogSync.call(registry).map((e) => { + const entry = e as Record + const installer = this._connector!.installer as Record + const getInstallInfo = installer.getInstallInfo as (name: string) => { installed: boolean; managed?: boolean; location?: string } + const info = getInstallInfo.call(installer, entry.name as string) + return { ...entry, installed: info.installed, managed: info.managed, location: info.location } + }) + } + const registry = this._connector!.registry as Record + const loadBundled = registry._loadBundled as () => unknown[] + const bundled = loadBundled.call(registry) + for (const entry of catalog) { + const e = entry as Record + const b = (bundled as Array>).find(x => x.name === e.name) + if (b) { + if (!e.check_ready && b.check_ready) e.check_ready = b.check_ready + if ((!e.env_config || !(e.env_config as unknown[]).length) && (b.env_config as unknown[] | undefined)?.length) e.env_config = b.env_config + if (!e.install && b.install) e.install = b.install + if (!e.launch && b.launch) e.launch = b.launch + } + } + return catalog + } + + async getEnvFields(agentType: string): Promise { + this._ensureConnector() + const getEnvFields = this._connector!.getEnvFields as (type: string) => unknown[] + return getEnvFields.call(this._connector, agentType) + } + + getAgentEnv(agentType: string): unknown { + const getAgentEnv = this._connector!.getAgentEnv as (type: string) => unknown + return getAgentEnv.call(this._connector, agentType) + } + + getAgentInstanceEnv(agentName: string): unknown { + const getInstanceEnv = this._connector!.getAgentInstanceEnv as (name: string) => unknown + return getInstanceEnv.call(this._connector, agentName) + } + + saveAgentEnv(agentType: string, env: Record): unknown { + const saveEnv = this._connector!.saveAgentEnv as (type: string, env: unknown) => void + saveEnv.call(this._connector, agentType, env) + + try { + if (agentType === 'openclaw') { + const OpenClawAdapter = require('@openagents-org/agent-launcher/src/adapters/openclaw') + OpenClawAdapter.configureNativeAuth(env) + } + } catch {} + + this.signalReload() + return { success: true } + } + + saveAgentInstanceEnv(agentName: string, env: Record): unknown { + const saveEnv = this._connector!.saveAgentInstanceEnv as (name: string, env: unknown) => void + saveEnv.call(this._connector, agentName, env) + this.signalReload() + return { success: true } + } + + async testLLM(env: Record): Promise { + const testLLM = this._connector!.testLLM as (env: unknown) => Promise + return testLLM.call(this._connector, env) + } + + signalReload(): void { + const getDaemonPid = this._connector!.getDaemonPid as () => number | null + const pid = getDaemonPid.call(this._connector) + if (!pid) return + + if (process.platform === 'win32') { + const sendCmd = this._connector!.sendDaemonCommand as (cmd: string) => void + sendCmd.call(this._connector, 'reload') + } else { + try { process.kill(pid, 'SIGHUP') } catch {} + } + } + + getNetworks(): unknown[] { + const listWorkspaces = this._connector!.listWorkspaces as () => unknown[] + return listWorkspaces.call(this._connector) + } + + async createWorkspace(name: string): Promise { + const createWorkspace = this._connector!.createWorkspace as (opts: unknown) => Promise + return createWorkspace.call(this._connector, { name: name || 'My Workspace' }) + } + + async connectWorkspace(agentName: string, tokenOrSlug: string): Promise { + try { + const resolveToken = this._connector!.resolveToken as (token: string) => Promise<{ slug?: string; workspace_id?: string; name?: string; endpoint?: string }> + const info = await resolveToken.call(this._connector, tokenOrSlug) + const slug = info.slug || info.workspace_id + const wsName = info.name || slug + + const addNetwork = (this._connector!.config as Record).addNetwork as (opts: unknown) => void + addNetwork.call((this._connector!.config as Record), { + id: info.workspace_id, + slug, + name: wsName, + endpoint: info.endpoint, + token: tokenOrSlug, + }) + + const connectWorkspace = this._connector!.connectWorkspace as (name: string, slug: string) => void + connectWorkspace.call(this._connector, agentName, slug as string) + } catch { + const connectWorkspace = this._connector!.connectWorkspace as (name: string, slug: string) => void + connectWorkspace.call(this._connector, agentName, tokenOrSlug) + } + this.signalReload() + return { success: true } + } + + async disconnectWorkspace(agentName: string): Promise { + const disconnectWorkspace = this._connector!.disconnectWorkspace as (name: string) => void + disconnectWorkspace.call(this._connector, agentName) + this.signalReload() + return { success: true } + } + + async removeWorkspace(slug: string): Promise { + const removeWorkspace = this._connector!.removeWorkspace as (slug: string) => Promise + const result = await removeWorkspace.call(this._connector, slug) + this.signalReload() + return result + } + + async checkAgentType(agentType: string): Promise { + const isInstalled = this._connector!.isInstalled as (type: string) => boolean + const installed = isInstalled.call(this._connector, agentType) + const installer = this._connector!.installer as Record + const which = installer.which as (type: string) => string | null + const binary = installed ? which.call(installer, agentType) : null + return { installed, binary: binary || null } + } + + async installAgentType(agentType: string): Promise { + const install = this._connector!.install as (type: string) => Promise + return install.call(this._connector, agentType) + } + + async installAgentTypeStreaming(agentType: string, onData: (data: string) => void): Promise { + const installer = this._connector!.installer as Record + const installStreaming = installer.installStreaming as (type: string, onData: (data: string) => void) => Promise + const result = await installStreaming.call(installer, agentType, onData) + const clearCache = this._connector!.clearCatalogCache as () => void + clearCache.call(this._connector) + this._recordInstall(agentType) + return result + } + + async uninstallAgentType(agentType: string): Promise { + const uninstall = this._connector!.uninstall as (type: string) => Promise + const result = await uninstall.call(this._connector, agentType) + const clearCache = this._connector!.clearCatalogCache as () => void + clearCache.call(this._connector) + this._recordUninstall(agentType) + return result + } + + async uninstallAgentTypeStreaming(agentType: string, onData: (data: string) => void): Promise { + const installer = this._connector!.installer as Record + const uninstallStreaming = installer.uninstallStreaming as (type: string, onData: (data: string) => void) => Promise + const result = await uninstallStreaming.call(installer, agentType, onData) + const clearCache = this._connector!.clearCatalogCache as () => void + clearCache.call(this._connector) + this._recordUninstall(agentType) + return result + } + + /** Read installed package version by inspecting runtime prefix package.json. */ + getInstalledVersion(agentType: string): string | null { + try { + const entry = this._getRegistryEntry(agentType) + const npmPkg = this._resolveNpmPackage(entry) + if (!npmPkg) return null + const candidates = [ + path.join(CONFIG_DIR, 'runtimes', agentType, 'node_modules', npmPkg, 'package.json'), + path.join(CONFIG_DIR, 'nodejs', 'node_modules', npmPkg, 'package.json'), + ] + for (const c of candidates) { + try { + if (fs.existsSync(c)) { + const pkg = JSON.parse(fs.readFileSync(c, 'utf-8')) + if (pkg?.version) return pkg.version + } + } catch {} + } + } catch {} + return null + } + + private _getRegistryEntry(agentType: string): Record | null { + try { + const registry = this._connector?.registry as Record | undefined + if (!registry) return null + const getEntry = registry.getEntry as ((t: string) => unknown) | undefined + const entry = getEntry ? (getEntry.call(registry, agentType) as Record | null) : null + return entry || null + } catch { return null } + } + + private _resolveNpmPackage(entry: Record | null): string | null { + if (!entry) return null + const install = entry.install as Record | undefined + if (!install) return null + if (install.npm_package) return install.npm_package as string + const cmd = (install[Installer.platformKey()] || install.command) as string | undefined + if (!cmd) return install.binary as string | null + const m = cmd.match(/npm install\s+(?:-g\s+)?(@?[\w-]+(?:\/[\w-]+)?)(?:@\S*)?$/) + if (m) return m[1] + return (install.binary as string | undefined) || null + } + + getInstalledHistory(): Record { + try { + if (fs.existsSync(INSTALLED_HISTORY_FILE)) { + const data = JSON.parse(fs.readFileSync(INSTALLED_HISTORY_FILE, 'utf-8')) + if (data && typeof data === 'object') return data + } + } catch {} + return {} + } + + private _writeInstalledHistory(data: Record): void { + try { + fs.mkdirSync(CONFIG_DIR, { recursive: true }) + fs.writeFileSync(INSTALLED_HISTORY_FILE, JSON.stringify(data, null, 2), 'utf-8') + } catch {} + } + + private _recordInstall(agentType: string): void { + try { + const data = this.getInstalledHistory() + const version = this.getInstalledVersion(agentType) + const prev = data[agentType] + const history = prev?.history ? [...prev.history] : [] + if (prev?.version && prev.version !== version) { + history.unshift({ version: prev.version, installedAt: prev.installedAt }) + } + data[agentType] = { + name: agentType, + version, + installedAt: new Date().toISOString(), + previousVersion: prev?.version || null, + history: history.slice(0, 10), + } + this._writeInstalledHistory(data) + } catch {} + } + + private _recordUninstall(agentType: string): void { + try { + const data = this.getInstalledHistory() + if (data[agentType]) { + delete data[agentType] + this._writeInstalledHistory(data) + } + } catch {} + } + + listInstalledAgents(): InstalledAgentRecord[] { + const data = this.getInstalledHistory() + const out: InstalledAgentRecord[] = [] + for (const name of Object.keys(data)) { + const r = data[name] + out.push({ ...r, version: r.version || this.getInstalledVersion(name) }) + } + return out + } + + async rollbackAgentType(agentType: string, onData: (data: string) => void): Promise<{ success: boolean; version: string | null; error?: string }> { + const data = this.getInstalledHistory() + const record = data[agentType] + const target = record?.history?.[0]?.version || record?.previousVersion + if (!target) return { success: false, version: null, error: 'No previous version to roll back to' } + + const entry = this._getRegistryEntry(agentType) + const npmPkg = this._resolveNpmPackage(entry) + if (!npmPkg) return { success: false, version: null, error: 'Cannot determine npm package for rollback' } + + const { spawn } = require('child_process') as typeof import('child_process') + const prefixDir = path.join(CONFIG_DIR, 'runtimes', agentType) + fs.mkdirSync(prefixDir, { recursive: true }) + const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm' + const args = ['install', '--save', '--prefix', prefixDir, `${npmPkg}@${target}`] + + if (onData) onData(`$ ${npmCmd} ${args.join(' ')}\n\n`) + + return new Promise((resolve) => { + const proc = spawn(npmCmd, args, { shell: true, cwd: prefixDir, stdio: ['ignore', 'pipe', 'pipe'] }) + proc.stdout?.setEncoding('utf-8') + proc.stderr?.setEncoding('utf-8') + proc.stdout?.on('data', (d) => onData && onData(d)) + proc.stderr?.on('data', (d) => onData && onData(d)) + proc.on('error', (err) => resolve({ success: false, version: null, error: err.message })) + proc.on('close', (code) => { + if (code === 0) { + this._recordInstall(agentType) + if (onData) onData(`\nRolled back to ${target}.\n`) + resolve({ success: true, version: target }) + } else { + resolve({ success: false, version: null, error: `Rollback failed with code ${code}` }) + } + }) + }) + } + + async checkAgentUpdates(): Promise> { + // Use the full catalog (every entry with installed=true), not just the + // history file — agents installed globally / pre-launcher won't be in + // the history but are still installed and worth checking for updates. + const catalog = (await this.getCatalog()) as Array> + const installedEntries = catalog.filter((e) => e.installed === true) + const historyByName = new Map(this.listInstalledAgents().map((r) => [r.name, r.version])) + + const results = await Promise.all(installedEntries.map(async (entry) => { + const name = entry.name as string + const npmPkg = this._resolveNpmPackage(entry) + const current = historyByName.get(name) || this.getInstalledVersion(name) + if (!npmPkg) return { name, current, latest: null } + const info = await fetchNpmInfo(npmPkg).catch(() => null) + return { name, current, latest: resolveLatestVersion(info) } + })) + return results + } + + async getAgentChangelog(agentType: string): Promise<{ versions: Array<{ version: string; date?: string }>; homepage?: string; error?: string }> { + const entry = this._getRegistryEntry(agentType) + const homepage = (entry?.homepage as string | undefined) || undefined + const npmPkg = this._resolveNpmPackage(entry) + if (!npmPkg) return { versions: [], homepage, error: 'No npm package' } + try { + const info = await fetchNpmInfo(npmPkg) + const time = info.time || {} + const versions = sortedPublishedVersions(info) + .slice(0, 12) + .map((v) => ({ version: v, date: time[v] })) + return { versions, homepage } + } catch (e: unknown) { + return { versions: [], homepage, error: (e as Error).message } + } + } + + async startAgent(name: string): Promise { + await this._ensureDaemon() + const sendCmd = this._connector!.sendDaemonCommand as (cmd: string) => void + sendCmd.call(this._connector, `start:${name}`) + return { success: true, message: `Start command sent for ${name}` } + } + + async stopAgent(name: string): Promise { + const getDaemonPid = this._connector!.getDaemonPid as () => number | null + const pid = getDaemonPid.call(this._connector) + if (!pid) return { success: true, message: 'Daemon not running' } + const sendCmd = this._connector!.sendDaemonCommand as (cmd: string) => void + sendCmd.call(this._connector, `stop:${name}`) + return { success: true, message: `Stop command sent for ${name}` } + } + + async startAll(): Promise { + await this._ensureDaemon() + const sendCmd = this._connector!.sendDaemonCommand as (cmd: string) => void + sendCmd.call(this._connector, 'reload') + return { success: true, message: 'Start all command sent' } + } + + async stopAll(): Promise { + const stopDaemon = this._connector!.stopDaemon as () => boolean + const stopped = stopDaemon.call(this._connector) + return { success: stopped, message: stopped ? 'Daemon stopped' : 'Daemon not running' } + } + + async _ensureDaemon(): Promise { + const getDaemonPid = this._connector!.getDaemonPid as () => number | null + const pid = getDaemonPid.call(this._connector) + if (pid) return + + const portableNodeDir = path.join(os.homedir(), '.openagents', 'nodejs') + const nodeBin = path.join(portableNodeDir, 'node' + (process.platform === 'win32' ? '.exe' : '')) + const nodeBinLegacy = path.join(portableNodeDir, 'bin', 'node') + if (!fs.existsSync(nodeBin) && !fs.existsSync(nodeBinLegacy)) return + + await this._startDaemon() + } + + getAllStatus(): unknown { + const now = Date.now() + if (this._statusCache.value && now - this._statusCache.at < 1000) { + return this._statusCache.value + } + const getDaemonStatus = this._connector!.getDaemonStatus as () => unknown + let value: unknown = {} + try { value = getDaemonStatus.call(this._connector) } catch { value = {} } + this._statusCache = { value, at: now } + return value + } + + getLogs(name: string, lines = 200): unknown { + const getLogs = this._connector!.getLogs as (name: string, lines: number) => string[] + const logLines = getLogs.call(this._connector, name, lines) + return { lines: logLines } + } + + tailLogs(name: string, lines = 200, offset = 0): unknown { + const config = this._connector!.config as Record + const tailLogs = config.tailLogs as (opts: unknown) => unknown + return tailLogs.call(config, { agent: name || undefined, lines, offset }) + } + + clearLogsInRange(start: string | number | Date, end: string | number | Date): unknown { + const startTime = normalizeTimeValue(start) + const endTime = normalizeTimeValue(end) + + if (!startTime || !endTime) { + throw new Error('Start time and end time are required') + } + if (startTime.getTime() > endTime.getTime()) { + throw new Error('Start time must be before end time') + } + + const logFile = path.join(CONFIG_DIR, 'daemon.log') + if (!fs.existsSync(logFile)) return { removed: 0, remaining: 0 } + + const content = fs.readFileSync(logFile, 'utf-8') + const hasTrailingNewline = content.endsWith('\n') + const allLines = content.split('\n') + if (hasTrailingNewline) allLines.pop() + + const { keptLines, removed } = filterLogsByTimeRange(allLines, startTime, endTime) + + const nextContent = keptLines.join('\n') + (hasTrailingNewline && keptLines.length > 0 ? '\n' : '') + const tempFile = `${logFile}.tmp` + fs.writeFileSync(tempFile, nextContent, 'utf-8') + fs.renameSync(tempFile, logFile) + + return { removed, remaining: keptLines.length } + } + + healthCheck(type: string): unknown { + const healthCheck = this._connector!.healthCheck as (type: string) => unknown + return healthCheck.call(this._connector, type) + } + + private _startDaemon(): { success: boolean; pid?: number; message: string } { + try { + const stopDaemon = this._connector!.stopDaemon as () => void + stopDaemon.call(this._connector) + } catch {} + + const { spawn } = require('child_process') + const portableNodeDir = path.join(os.homedir(), '.openagents', 'nodejs') + const openagentsDir = path.join(os.homedir(), '.openagents') + + const extraDirs = [ + portableNodeDir, + path.join(portableNodeDir, 'bin'), + ] + const runtimesDir = path.join(openagentsDir, 'runtimes') + try { + for (const d of fs.readdirSync(runtimesDir, { withFileTypes: true })) { + if (d.isDirectory()) extraDirs.push(path.join(runtimesDir, d.name, 'node_modules', '.bin')) + } + } catch {} + extraDirs.push(path.join(openagentsDir, 'core', 'node_modules', '.bin')) + extraDirs.push(path.join(portableNodeDir, 'node_modules', '.bin')) + if (process.platform === 'win32') { + extraDirs.push(path.join(process.env.APPDATA || '', 'npm')) + try { + const { execSync: _exec } = require('child_process') + const npmPrefix = _exec('npm config get prefix', { + encoding: 'utf-8', timeout: 5000, windowsHide: true, + }).trim() + if (npmPrefix && !extraDirs.includes(npmPrefix)) extraDirs.push(npmPrefix) + } catch {} + } + const enhancedPath = [...extraDirs, process.env.PATH || ''].join(path.delimiter) + + let cliPath: string | null = null + const cliCandidates = [ + path.join(LOCAL_CORE, 'bin', 'agent-connector.js'), + path.join(portableNodeDir, 'node_modules', '@openagents-org', 'agent-launcher', 'bin', 'agent-connector.js'), + ] + for (const c of cliCandidates) { + try { if (fs.existsSync(c)) { cliPath = c; break } } catch {} + } + if (!cliPath) { + return { success: false, message: 'agent-launcher CLI not found. Install an agent first via the Install tab.' } + } + + let nodeBin = path.join(portableNodeDir, 'node' + (process.platform === 'win32' ? '.exe' : '')) + if (!fs.existsSync(nodeBin)) { + try { + const { execSync } = require('child_process') + nodeBin = execSync( + process.platform === 'win32' ? 'where node' : 'which node', + { encoding: 'utf-8', timeout: 5000, env: { ...process.env, PATH: enhancedPath } } + ).split(/\r?\n/)[0].trim() + } catch { nodeBin = 'node' } + } + + try { + fs.mkdirSync(CONFIG_DIR, { recursive: true }) + const logFile = path.join(CONFIG_DIR, 'daemon.log') + const pidFile = path.join(CONFIG_DIR, 'daemon.pid') + const logFd = fs.openSync(logFile, 'a') + + const proc = spawn(nodeBin, [cliPath, 'up', '--foreground'], { + detached: true, + stdio: ['ignore', logFd, logFd], + env: { ...process.env, PATH: enhancedPath }, + windowsHide: true, + }) + proc.unref() + fs.writeFileSync(pidFile, String(proc.pid), 'utf-8') + fs.closeSync(logFd) + + return { success: true, pid: proc.pid, message: `Daemon started (PID ${proc.pid})` } + } catch (e: unknown) { + return { success: false, message: `Failed to start daemon: ${(e as Error).message}` } + } + } +} + +class Installer { + static platformKey(): 'macos' | 'linux' | 'windows' { + if (process.platform === 'darwin') return 'macos' + if (process.platform === 'win32') return 'windows' + return 'linux' + } +} + +function fetchNpmInfo(pkg: string): Promise { + return new Promise((resolve, reject) => { + const url = `https://registry.npmjs.org/${encodeURIComponent(pkg).replace('%40', '@')}` + const req = https.get(url, { headers: { Accept: 'application/json' } }, (res) => { + if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + fetchNpmInfo(res.headers.location as string).then(resolve, reject) + return + } + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode}`)) + return + } + let data = '' + res.setEncoding('utf-8') + res.on('data', (c) => { data += c }) + res.on('end', () => { + try { resolve(JSON.parse(data) as NpmRegistryInfo) } + catch (e) { reject(e as Error) } + }) + }) + req.on('error', reject) + req.setTimeout(10000, () => req.destroy(new Error('npm registry timeout'))) + }) +} + +function compareVersionsDesc(a: string, b: string): number { + const pa = a.split('.').map((n) => parseInt(n, 10) || 0) + const pb = b.split('.').map((n) => parseInt(n, 10) || 0) + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const x = pa[i] || 0 + const y = pb[i] || 0 + if (x !== y) return y - x + } + return 0 +} + +// Versions published to npm, sorted highest-first (includes pre-releases like +// beta / rc — npm's dist-tags.latest excludes them, which made the marketplace +// card miss updates that the detail page surfaced via the changelog fallback). +function sortedPublishedVersions(info: NpmRegistryInfo | null): string[] { + return Object.keys(info?.versions || {}) + .filter((v) => /^\d/.test(v)) + .sort(compareVersionsDesc) +} + +function resolveLatestVersion(info: NpmRegistryInfo | null): string | null { + return sortedPublishedVersions(info)[0] || info?.['dist-tags']?.latest || null +} + +function normalizeTimeValue(value: string | number | Date): Date | null { + if (value instanceof Date) { + return Number.isNaN(value.getTime()) ? null : value + } + if (typeof value === 'number') { + const date = new Date(value) + return Number.isNaN(date.getTime()) ? null : date + } + if (typeof value === 'string' && value.trim()) { + const date = new Date(value) + return Number.isNaN(date.getTime()) ? null : date + } + return null +} + +function filterLogsByTimeRange(lines: string[], start: Date, end: Date): { keptLines: string[]; removed: number } { + const headerTimes = resolveLogHeaderTimestamps(lines, end) + let activeRemove = false + let removed = 0 + const keptLines: string[] = [] + + for (let index = 0; index < lines.length; index++) { + const headerTime = headerTimes[index] + if (headerTime) { + const time = headerTime.getTime() + activeRemove = time >= start.getTime() && time <= end.getTime() + } + if (activeRemove) { + removed++ + } else { + keptLines.push(lines[index]) + } + } + + return { keptLines, removed } +} + +function resolveLogHeaderTimestamps(lines: string[], referenceTime: Date): (Date | null)[] { + const resolved: (Date | null)[] = new Array(lines.length).fill(null) + let currentDay = startOfLocalDay(referenceTime) + let lastClockSeconds: number | null = null + + for (let index = lines.length - 1; index >= 0; index--) { + const token = parseLogTimestampToken(lines[index]) + if (!token) continue + + if (token.kind === 'iso') { + resolved[index] = token.date + currentDay = startOfLocalDay(token.date) + lastClockSeconds = ( + token.date.getHours() * 3600 + + token.date.getMinutes() * 60 + + token.date.getSeconds() + ) + continue + } + + if (lastClockSeconds !== null && token.seconds > lastClockSeconds) { + currentDay = addLocalDays(currentDay, -1) + } + + resolved[index] = withLocalClock(currentDay, token.seconds) + lastClockSeconds = token.seconds + } + + return resolved +} + +function parseLogTimestampToken(line: string): { kind: 'iso'; date: Date } | { kind: 'clock'; seconds: number } | null { + if (!line) return null + + const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))/) + if (isoMatch) { + const date = new Date(isoMatch[1]) + if (!Number.isNaN(date.getTime())) return { kind: 'iso', date } + } + + const clockMatch = line.match(/^\[(\d{2}):(\d{2}):(\d{2})\]/) + if (clockMatch) { + return { + kind: 'clock', + seconds: Number(clockMatch[1]) * 3600 + Number(clockMatch[2]) * 60 + Number(clockMatch[3]), + } + } + + return null +} + +function startOfLocalDay(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()) +} + +function addLocalDays(date: Date, days: number): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days) +} + +function withLocalClock(day: Date, seconds: number): Date { + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + return new Date(day.getFullYear(), day.getMonth(), day.getDate(), hours, minutes, secs) +} diff --git a/packages/launcher/src/main/index.ts b/packages/launcher/src/main/index.ts new file mode 100644 index 000000000..8c1928268 --- /dev/null +++ b/packages/launcher/src/main/index.ts @@ -0,0 +1,940 @@ +import { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage, shell } from 'electron' +import path from 'path' +import fs from 'fs' +import os from 'os' +import { execSync, execFile } from 'child_process' +import { Store } from './store' +import { AgentManager } from './agent-manager' + +function execFileAsync(file: string, args: string[], opts: { timeout?: number; env?: NodeJS.ProcessEnv } = {}): Promise { + return new Promise((resolve, reject) => { + execFile(file, args, { timeout: opts.timeout || 10000, env: opts.env, encoding: 'utf-8' }, (err, stdout) => { + if (err) reject(err) + else resolve((stdout || '').toString().trim()) + }) + }) +} + +app.setName('OpenAgents Launcher') + +const isHeadless = process.argv.includes('--headless') +if (process.argv.includes('--disable-gpu') || isHeadless) { + app.disableHardwareAcceleration() +} + +const PORTABLE_NODE_DIR = path.join(os.homedir(), '.openagents', 'nodejs') +const GLOBAL_MODULES = path.join(PORTABLE_NODE_DIR, 'node_modules') +const CORE_PKG = '@openagents-org/agent-launcher' + +if (fs.existsSync(GLOBAL_MODULES) && !require('module').globalPaths.includes(GLOBAL_MODULES)) { + require('module').globalPaths.push(GLOBAL_MODULES) +} + +const store = new Store() +let mainWindow: BrowserWindow | null = null +let tray: Tray | null = null +let agentManager: AgentManager | null = null +let coreVersion: string | null = null + +let _launcherVersionCache: string | null = null +function getLauncherVersion(): string { + if (_launcherVersionCache) return _launcherVersionCache + try { _launcherVersionCache = require('../../package.json').version as string } catch { _launcherVersionCache = '0.0.0' } + return _launcherVersionCache! +} + +interface RuntimeInfo { nodeVersion: string | null; npmVersion: string | null; coreVersion: string | null; latestVersion: string | null } +const _runtimeCache: { value: RuntimeInfo; stableAt: number; latestAt: number; refreshing: boolean } = { + value: { nodeVersion: null, npmVersion: null, coreVersion: null, latestVersion: null }, + stableAt: 0, + latestAt: 0, + refreshing: false, +} +const RUNTIME_STABLE_TTL = 60_000 * 30 +const RUNTIME_LATEST_TTL = 60_000 * 10 + +const STARTUP_LOG = path.join(os.homedir(), '.openagents', 'startup.log') +function slog(msg: string): void { + try { + fs.mkdirSync(path.dirname(STARTUP_LOG), { recursive: true }) + fs.appendFileSync(STARTUP_LOG, `${new Date().toISOString()} ${msg}\n`) + } catch {} + console.log('[startup]', msg) +} + +function downloadFile(https: typeof import('https'), url: string, destPath: string, onProgress: ((pct: number, detail: string) => void) | null): Promise { + return new Promise((resolve, reject) => { + const doGet = (u: string): void => { + https.get(u, (res) => { + if (res.statusCode === 302 || res.statusCode === 301) { + doGet(res.headers.location as string) + return + } + if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return } + const total = parseInt(res.headers['content-length'] || '0') + let downloaded = 0 + const file = fs.createWriteStream(destPath) + res.on('data', (chunk: Buffer) => { + downloaded += chunk.length + file.write(chunk) + if (total && onProgress) onProgress(Math.round(downloaded / total * 100), `${(downloaded / 1e6).toFixed(1)} MB`) + }) + res.on('end', () => { file.end(resolve) }) + res.on('error', reject) + }).on('error', reject) + } + doGet(url) + }) +} + +async function downloadNodejs(nodejsDir: string, onProgress: (pct: number, detail: string) => void): Promise { + const https = require('https') + const nodeVersion = 'v22.14.0' + const arch = process.arch === 'arm64' ? 'arm64' : 'x64' + + try { fs.rmSync(nodejsDir, { recursive: true, force: true }) } catch {} + fs.mkdirSync(nodejsDir, { recursive: true }) + slog(`downloadNodejs: platform=${process.platform} arch=${arch} dir=${nodejsDir}`) + + if (process.platform === 'win32') { + const nodeExeUrl = `https://nodejs.org/dist/${nodeVersion}/win-${arch}/node.exe` + const nodeExeDest = path.join(nodejsDir, 'node.exe') + await downloadFile(https, nodeExeUrl, nodeExeDest, onProgress) + + const npmVersion = '10.9.2' + const npmUrl = `https://registry.npmjs.org/npm/-/npm-${npmVersion}.tgz` + const npmTgz = path.join(os.tmpdir(), `npm-${npmVersion}.tgz`) + const npmModDir = path.join(nodejsDir, 'node_modules', 'npm') + if (onProgress) onProgress(85, 'Installing npm...') + await downloadFile(https, npmUrl, npmTgz, null) + + fs.mkdirSync(npmModDir, { recursive: true }) + try { + execSync(`tar -xzf "${npmTgz}" -C "${npmModDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }) + } catch (e: unknown) { + slog(`npm extraction failed: ${(e as Error).message}`) + } + try { fs.unlinkSync(npmTgz) } catch {} + + const npmCliPath = path.join(npmModDir, 'bin', 'npm-cli.js') + if (fs.existsSync(npmCliPath)) { + fs.writeFileSync(path.join(nodejsDir, 'npm.cmd'), `@echo off\r\n"${nodeExeDest}" "${npmCliPath}" %*\r\n`) + fs.writeFileSync(path.join(nodejsDir, 'npx.cmd'), `@echo off\r\n"${nodeExeDest}" "${path.join(npmModDir, 'bin', 'npx-cli.js')}" %*\r\n`) + } + } else { + const platName = process.platform === 'darwin' ? 'darwin' : 'linux' + const ext = process.platform === 'darwin' ? 'tar.gz' : 'tar.xz' + const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-${platName}-${arch}.${ext}` + const tarPath = path.join(os.tmpdir(), `node-${nodeVersion}.${ext}`) + + await downloadFile(https, url, tarPath, onProgress) + if (onProgress) onProgress(90, 'Extracting...') + const flag = ext === 'tar.gz' ? '-xzf' : '-xJf' + execSync(`tar ${flag} "${tarPath}" -C "${nodejsDir}" --strip-components=1`, { timeout: 120000 }) + try { fs.unlinkSync(tarPath) } catch {} + + const binDir = path.join(nodejsDir, 'bin') + for (const name of ['node', 'npm', 'npx']) { + const src = path.join(binDir, name) + const dest = path.join(nodejsDir, name) + if (fs.existsSync(src) && !fs.existsSync(dest)) { + try { fs.symlinkSync(src, dest) } catch {} + } + } + } + if (onProgress) onProgress(100, 'Done') +} + +function findNpmCommand(): string | null { + const nodeUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node') + const nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'node') + if (!fs.existsSync(nodeBin)) return null + const candidates = [ + path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm', 'bin', 'npm-cli.js'), + path.join(PORTABLE_NODE_DIR, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), + ] + const npmCli = candidates.find(p => fs.existsSync(p)) + if (npmCli) return `"${nodeBin}" "${npmCli}"` + if (process.platform !== 'win32') { + const npmBin = path.join(PORTABLE_NODE_DIR, 'bin', 'npm') + if (fs.existsSync(npmBin)) return `"${npmBin}"` + } + return null +} + +function _addToPrefixPackageJson(pkg: string, version: string): void { + const pkgJsonPath = path.join(PORTABLE_NODE_DIR, 'package.json') + let data: { dependencies?: Record } = {} + try { data = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) } catch {} + if (!data.dependencies) data.dependencies = {} + data.dependencies[pkg] = version + try { fs.writeFileSync(pkgJsonPath, JSON.stringify(data, null, 2) + '\n', 'utf-8') } catch {} +} + +let _updateSplash: ((msg: string, pct: number, detail?: string) => void) | null = null + +async function ensureCoreLibrary(): Promise { + const corePkgPath = path.join(GLOBAL_MODULES, CORE_PKG, 'package.json') + let installedVersion: string | null = null + + if (fs.existsSync(corePkgPath)) { + try { installedVersion = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version } catch {} + } + + const https = require('https') + try { + const latestVersion: string = await new Promise((res, rej) => { + https.get(`https://registry.npmjs.org/${CORE_PKG}/latest`, (r: import('http').IncomingMessage) => { + let d = '' + r.on('data', (c: Buffer) => d += c) + r.on('end', () => { try { res(JSON.parse(d).version) } catch { rej(new Error('parse error')) } }) + }).on('error', rej) + }) + + if (!installedVersion) { + slog('Core library not found — installing v' + latestVersion + '...') + if (_updateSplash) _updateSplash('Installing core library...', 65, 'v' + latestVersion) + } else if (latestVersion !== installedVersion) { + slog('Core library update: v' + installedVersion + ' → v' + latestVersion) + if (_updateSplash) _updateSplash('Updating core library...', 65, 'v' + installedVersion + ' → v' + latestVersion) + } else { + slog('Core library v' + installedVersion + ' (already latest)') + if (_updateSplash) _updateSplash('Core library up to date', 80, 'v' + installedVersion) + } + + if (!installedVersion || latestVersion !== installedVersion) { + const tgzUrl = `https://registry.npmjs.org/${CORE_PKG}/-/agent-launcher-${latestVersion}.tgz` + const tgzPath = path.join(os.tmpdir(), `agent-launcher-${latestVersion}.tgz`) + const destDir = path.join(GLOBAL_MODULES, CORE_PKG) + + await downloadFile(https, tgzUrl, tgzPath, null) + try { fs.rmSync(destDir, { recursive: true, force: true }) } catch {} + fs.mkdirSync(destDir, { recursive: true }) + execSync(`tar -xzf "${tgzPath}" -C "${destDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }) + try { fs.unlinkSync(tgzPath) } catch {} + + const newVersion = (() => { try { return JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version } catch { return null } })() + if (newVersion) { + slog('Core library installed: v' + newVersion) + if (_updateSplash) _updateSplash('Core library ready', 80, 'v' + newVersion) + installedVersion = newVersion + _addToPrefixPackageJson(CORE_PKG, newVersion) + } + } + } catch (e: unknown) { + slog('Core update failed: ' + (e as Error).message) + if (!installedVersion) { + slog('Falling back to npm...') + const npmCmd = findNpmCommand() + if (npmCmd) { + try { + execSync(`${npmCmd} install --prefix "${PORTABLE_NODE_DIR}" ${CORE_PKG}@latest --ignore-scripts`, { + stdio: 'pipe', timeout: 120000, + env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, + }) + try { installedVersion = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version } catch {} + } catch {} + } + } + } + + coreVersion = installedVersion + + const npmCheck = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm', 'bin', 'npm-cli.js') + if (!fs.existsSync(npmCheck)) { + slog('npm was removed by --prefix install — reinstalling...') + try { + const npmTgz = path.join(os.tmpdir(), 'npm-reinstall.tgz') + const npmDir = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm') + await downloadFile(https, 'https://registry.npmjs.org/npm/-/npm-10.9.2.tgz', npmTgz, null) + fs.mkdirSync(npmDir, { recursive: true }) + execSync(`tar -xzf "${npmTgz}" -C "${npmDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }) + try { fs.unlinkSync(npmTgz) } catch {} + slog('npm reinstalled') + } catch (e: unknown) { + slog('npm reinstall failed: ' + (e as Error).message) + } + } + + if (installedVersion && agentManager) { + agentManager.reloadCore() + } +} + +async function checkCoreUpdate(): Promise { + const npmCmd = findNpmCommand() + if (!npmCmd) return + try { + const latest = execSync(`${npmCmd} view ${CORE_PKG} version`, { + encoding: 'utf-8', timeout: 15000, + env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, + }).trim() + + if (coreVersion && latest && latest !== coreVersion) { + if (mainWindow) { + mainWindow.webContents.send('core-update-available', { current: coreVersion, latest }) + } + } + } catch {} +} + +function createWindow(): void { + if (mainWindow) { + if (process.platform === 'darwin' && app.dock) app.dock.show() + mainWindow.show() + mainWindow.focus() + return + } + + mainWindow = new BrowserWindow({ + width: 900, + height: 650, + minWidth: 700, + minHeight: 500, + title: 'OpenAgents Launcher', + autoHideMenuBar: true, + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + }, + show: false, + }) + + if (process.env.ELECTRON_RENDERER_URL) { + mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) + } else { + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) + } + + mainWindow.once('ready-to-show', () => { + if (process.platform === 'darwin' && app.dock) app.dock.show() + mainWindow!.show() + }) + + mainWindow.on('close', (e) => { + if (!(app as typeof app & { isQuitting?: boolean }).isQuitting) { + e.preventDefault() + mainWindow!.hide() + if (process.platform === 'darwin' && app.dock) app.dock.hide() + } + }) + + mainWindow.on('closed', () => { mainWindow = null }) +} + +function createPlaceholderIcon(): Electron.NativeImage { + const size = 16 + const canvas = Buffer.alloc(size * size * 4) + const cx = 7.5, cy = 7.5, r = 7, ri = 4 + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const i = (y * size + x) * 4 + const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) + if (d <= r) { + if (d <= ri) { + canvas[i] = 0xFF; canvas[i + 1] = 0xFF; canvas[i + 2] = 0xFF; canvas[i + 3] = 0xFF + } else { + canvas[i] = 0x6C; canvas[i + 1] = 0x63; canvas[i + 2] = 0xFF; canvas[i + 3] = 0xFF + } + } + } + } + return nativeImage.createFromBuffer(canvas, { width: size, height: size }) +} + +function createTray(): void { + const assetsDir = path.join(__dirname, '../../assets') + let trayIcon: Electron.NativeImage + + if (process.platform === 'darwin') { + trayIcon = nativeImage.createFromPath(path.join(assetsDir, 'tray-iconTemplate.png')) + } else { + trayIcon = nativeImage.createFromPath(path.join(assetsDir, 'tray-icon.png')) + } + + if (!trayIcon || trayIcon.isEmpty()) trayIcon = createPlaceholderIcon() + + tray = new Tray(trayIcon) + tray.setToolTip('OpenAgents Launcher') + updateTrayMenu() + tray.on('click', () => createWindow()) +} + +let _pendingAgentUpdates: Array<{ name: string; current: string | null; latest: string | null }> = [] + +function updateTrayMenu(): void { + if (!tray) return + + const agents = agentManager ? agentManager.getAgents() as Array<{ name: string; state: string }> : [] + const agentItems = agents.length > 0 + ? agents.map(a => ({ label: `${a.name} (${a.state})`, enabled: false })) + : [{ label: 'No agents configured', enabled: false }] + + const updateItems: Electron.MenuItemConstructorOptions[] = _pendingAgentUpdates.length > 0 + ? [ + { type: 'separator' }, + { label: `Updates available (${_pendingAgentUpdates.length})`, enabled: false }, + ..._pendingAgentUpdates.slice(0, 5).map((u): Electron.MenuItemConstructorOptions => ({ + label: `${u.name}: v${u.current ?? '?'} → v${u.latest ?? '?'}`, + click: () => { + createWindow() + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('navigate-to-install', u.name) + } + }, + })), + ] + : [] + + const menu = Menu.buildFromTemplate([ + { label: 'Open Dashboard', click: () => createWindow() }, + { type: 'separator' }, + ...agentItems, + ...updateItems, + { type: 'separator' }, + { + label: 'Quit OpenAgents', + click: async () => { + const { dialog } = require('electron') + const result = await dialog.showMessageBox({ + type: 'question', + buttons: ['Quit', 'Cancel'], + defaultId: 1, + title: 'Quit OpenAgents Launcher', + message: 'Quit OpenAgents Launcher?', + detail: 'The daemon will stop and all connected agents will go offline.', + }) + if (result.response === 0) { + (app as typeof app & { isQuitting: boolean }).isQuitting = true + try { if (agentManager) await agentManager.stopAll() } catch {} + app.quit() + } + }, + }, + ]) + + tray.setContextMenu(menu) + if (_pendingAgentUpdates.length > 0) { + tray.setToolTip(`OpenAgents Launcher · ${_pendingAgentUpdates.length} update${_pendingAgentUpdates.length > 1 ? 's' : ''} available`) + } else { + tray.setToolTip('OpenAgents Launcher') + } +} + +async function refreshAgentUpdates(): Promise { + if (!agentManager) return + try { + const all = await agentManager.checkAgentUpdates() + _pendingAgentUpdates = all.filter((u) => u.current && u.latest && u.current !== u.latest) + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('agent-updates-changed', _pendingAgentUpdates) + } + updateTrayMenu() + } catch {} +} + +type InstallPhase = 'idle' | 'preparing' | 'downloading' | 'installing' | 'verifying' | 'done' | 'error' +type InstallVerb = 'install' | 'update' | 'uninstall' | 'rollback' + +function broadcastInstallProgress(payload: { + agent: string + verb: InstallVerb + phase: InstallPhase + detail?: string + log?: string + error?: string +}): void { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('install:progress', payload) + } +} + +function classifyInstallChunk(chunk: string, verb: InstallVerb): { phase?: InstallPhase; detail?: string } { + const line = chunk.toLowerCase() + if (verb === 'uninstall') { + if (line.includes('removed') || line.includes('uninstall')) return { phase: 'installing', detail: 'Removing files' } + if (line.includes('done!')) return { phase: 'verifying', detail: 'Cleaning shims' } + return {} + } + if (line.includes('downloading') || /\b\d+\s*%/.test(line) || line.includes('mb')) { + return { phase: 'downloading', detail: chunk.trim().slice(0, 80) } + } + if (line.includes('extracting') || line.includes('expanding')) { + return { phase: 'installing', detail: 'Extracting archive' } + } + if (line.includes('npm warn') || line.includes('npm http')) { + return { phase: 'installing' } + } + if (line.includes('added ') && line.includes('package')) { + return { phase: 'verifying', detail: chunk.trim().slice(0, 80) } + } + if (line.includes('done!') || line.includes('installed.')) { + return { phase: 'verifying', detail: 'Finalizing' } + } + return {} +} + +async function runInstallWithPhases( + agent: string, + verb: InstallVerb, + runner: (onData: (data: string) => void) => Promise, +): Promise { + let currentPhase: InstallPhase = 'preparing' + broadcastInstallProgress({ agent, verb, phase: 'preparing', detail: 'Resolving dependencies' }) + + const onData = (data: string): void => { + if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('install:output', data) + const { phase, detail } = classifyInstallChunk(data, verb) + if (phase && phase !== currentPhase) { + currentPhase = phase + broadcastInstallProgress({ agent, verb, phase, detail }) + } else if (detail) { + broadcastInstallProgress({ agent, verb, phase: currentPhase, detail }) + } + } + + try { + const result = await runner(onData) + broadcastInstallProgress({ agent, verb, phase: 'done', detail: 'Complete' }) + return result + } catch (e: unknown) { + broadcastInstallProgress({ agent, verb, phase: 'error', error: (e as Error).message }) + throw e + } +} + +function resolveNpmInvocation(): { node: string; args: string[] } | null { + const nodeUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node') + const nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'node') + if (!fs.existsSync(nodeBin)) return null + const candidates = [ + path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm', 'bin', 'npm-cli.js'), + path.join(PORTABLE_NODE_DIR, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), + ] + const npmCli = candidates.find(p => fs.existsSync(p)) + if (npmCli) return { node: nodeBin, args: [npmCli] } + if (process.platform !== 'win32') { + const npmBin = path.join(PORTABLE_NODE_DIR, 'bin', 'npm') + if (fs.existsSync(npmBin)) return { node: npmBin, args: [] } + } + return null +} + +async function refreshRuntimeInfo(force = false): Promise { + const now = Date.now() + const info = _runtimeCache.value + info.coreVersion = coreVersion || info.coreVersion || null + + if (_runtimeCache.refreshing) return info + const needStable = force || !info.nodeVersion || !info.npmVersion || (now - _runtimeCache.stableAt > RUNTIME_STABLE_TTL) + const needLatest = force || !info.latestVersion || (now - _runtimeCache.latestAt > RUNTIME_LATEST_TTL) + if (!needStable && !needLatest) return info + + _runtimeCache.refreshing = true + try { + const env = { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') } + const npm = resolveNpmInvocation() + + if (needStable) { + const nodeUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node') + const nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'node') + if (fs.existsSync(nodeBin)) { + try { info.nodeVersion = await execFileAsync(nodeBin, ['--version'], { timeout: 5000 }) } catch {} + } + if (npm) { + try { + info.npmVersion = await execFileAsync(npm.node, [...npm.args, '--version'], { timeout: 5000, env }) + } catch {} + } + _runtimeCache.stableAt = now + } + + if (needLatest) { + if (npm) { + try { + info.latestVersion = await execFileAsync(npm.node, [...npm.args, 'view', CORE_PKG, 'version'], { timeout: 10_000, env }) + } catch {} + } + _runtimeCache.latestAt = now + } + } finally { + _runtimeCache.refreshing = false + } + return info +} + +function setupIPC(): void { + ipcMain.handle('python:status', () => ({ + pythonPath: null, + pythonFound: true, + sdkInstalled: true, + sdkVersion: coreVersion || 'not installed', + launcherVersion: getLauncherVersion(), + runtime: 'node', + })) + ipcMain.handle('python:install', () => ({ success: true, message: 'No installation needed — using Node.js agent-connector' })) + + ipcMain.handle('runtime:info', async (_e, opts?: { force?: boolean }) => { + const force = !!(opts && opts.force) + const info = _runtimeCache.value + const needStable = force || !info.nodeVersion || !info.npmVersion + if (needStable && !_runtimeCache.refreshing) { + _runtimeCache.refreshing = true + try { + const env = { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') } + const npm = resolveNpmInvocation() + const nodeUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node') + const nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'node') + if (fs.existsSync(nodeBin)) { + try { info.nodeVersion = await execFileAsync(nodeBin, ['--version'], { timeout: 5000 }) } catch {} + } + if (npm) { + try { info.npmVersion = await execFileAsync(npm.node, [...npm.args, '--version'], { timeout: 5000, env }) } catch {} + } + _runtimeCache.stableAt = Date.now() + } finally { + _runtimeCache.refreshing = false + } + } + info.coreVersion = coreVersion || info.coreVersion || null + const needLatest = force || !info.latestVersion || (Date.now() - _runtimeCache.latestAt > RUNTIME_LATEST_TTL) + if (needLatest) { + // Don't block IPC on the network call. Refresh in background. + void refreshRuntimeInfo(force).catch(() => {}) + } + return { ...info } + }) + + const requireManager = (): AgentManager => { + if (!agentManager) throw new Error('Launcher is still initializing, please wait a moment') + return agentManager + } + + ipcMain.handle('agents:list', () => agentManager ? agentManager.getAgents() : []) + ipcMain.handle('agents:supported-types', () => agentManager ? agentManager.getSupportedAgentTypes() : []) + ipcMain.handle('agents:core-info', () => agentManager ? agentManager.getCoreInfo() : { version: null, supportedTypes: [], globalCorePresent: false }) + ipcMain.handle('agents:add', (_e, config) => requireManager().addAgent(config)) + ipcMain.handle('agents:remove', (_e, name) => requireManager().removeAgent(name)) + ipcMain.handle('agents:update', (_e, name, config) => requireManager().updateAgent(name, config)) + + ipcMain.handle('agents:start', (_e, name) => requireManager().startAgent(name)) + ipcMain.handle('agents:stop', (_e, name) => requireManager().stopAgent(name)) + ipcMain.handle('agents:start-all', () => requireManager().startAll()) + ipcMain.handle('agents:stop-all', () => requireManager().stopAll()) + ipcMain.handle('agents:status', () => agentManager ? agentManager.getAllStatus() : {}) + ipcMain.handle('agents:logs', (_e, name, lines) => requireManager().getLogs(name, lines)) + ipcMain.handle('agents:tail-logs', (_e, name, lines, offset) => { + if (!agentManager) return { lines: [], size: 0 } + try { return agentManager.tailLogs(name, lines, offset) } catch { return { lines: [], size: 0 } } + }) + ipcMain.handle('agents:clear-logs-range', (_e, start, end) => requireManager().clearLogsInRange(start, end)) + + ipcMain.handle('agents:install-type', (_e, agentType) => requireManager().installAgentType(agentType)) + ipcMain.handle('agents:install-type-streaming', async (_e, agentType) => { + const verb = agentManager?.getInstalledVersion(agentType) ? 'update' : 'install' + return runInstallWithPhases(agentType, verb, (cb) => + requireManager().installAgentTypeStreaming(agentType, cb), + ) + }) + ipcMain.handle('agents:uninstall-type', (_e, agentType) => requireManager().uninstallAgentType(agentType)) + ipcMain.handle('agents:uninstall-type-streaming', async (_e, agentType) => { + return runInstallWithPhases(agentType, 'uninstall', (cb) => + requireManager().uninstallAgentTypeStreaming(agentType, cb), + ) + }) + + ipcMain.handle('agents:installed-list', () => agentManager ? agentManager.listInstalledAgents() : []) + ipcMain.handle('agents:check-updates', async () => { + if (!agentManager) return [] + try { return await agentManager.checkAgentUpdates() } catch { return [] } + }) + ipcMain.handle('agents:rollback', async (_e, agentType) => { + if (!agentManager) return { success: false, error: 'Launcher initializing' } + return runInstallWithPhases(agentType, 'rollback', (cb) => + agentManager!.rollbackAgentType(agentType, cb), + ) + }) + ipcMain.handle('agents:changelog', async (_e, agentType) => { + if (!agentManager) return { versions: [], error: 'Launcher initializing' } + try { return await agentManager.getAgentChangelog(agentType) } + catch (e: unknown) { return { versions: [], error: (e as Error).message } } + }) + ipcMain.handle('agents:check-type', (_e, agentType) => { + if (!agentManager) return { installed: false, binary: null } + try { return agentManager.checkAgentType(agentType) } catch { return { installed: false, binary: null } } + }) + ipcMain.handle('agents:catalog', async () => { + if (!agentManager) return [] + try { + try { (agentManager._connector!.registry as Record)._catalog = null } catch {} + return await agentManager.getCatalog() + } catch { return [] } + }) + + ipcMain.handle('agents:env-fields', (_e, agentType) => requireManager().getEnvFields(agentType)) + ipcMain.handle('agents:get-env', (_e, agentType) => requireManager().getAgentEnv(agentType)) + ipcMain.handle('agents:save-env', (_e, agentType, env) => requireManager().saveAgentEnv(agentType, env)) + ipcMain.handle('agents:get-instance-env', (_e, agentName) => requireManager().getAgentInstanceEnv(agentName)) + ipcMain.handle('agents:save-instance-env', (_e, agentName, env) => requireManager().saveAgentInstanceEnv(agentName, env)) + ipcMain.handle('agents:test-llm', (_e, env) => requireManager().testLLM(env)) + ipcMain.handle('agents:signal-reload', () => requireManager().signalReload()) + + ipcMain.handle('workspace:connect', (_e, agentName, slug) => requireManager().connectWorkspace(agentName, slug)) + ipcMain.handle('workspace:disconnect', (_e, agentName) => requireManager().disconnectWorkspace(agentName)) + ipcMain.handle('workspace:remove', (_e, slug) => requireManager().removeWorkspace(slug)) + ipcMain.handle('workspace:list', () => agentManager ? agentManager.getNetworks() : []) + ipcMain.handle('workspace:create', (_e, name) => requireManager().createWorkspace(name)) + + ipcMain.handle('settings:get', (_e, key) => store.get(key)) + ipcMain.handle('settings:set', (_e, key, value) => store.set(key, value)) + + ipcMain.handle('agents:health-check', (_e, type) => { + if (!agentManager) return null + try { return agentManager.healthCheck(type) } catch { return null } + }) + + ipcMain.handle('core:update', async () => { + const npmUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'npm.cmd' : 'npm') + const npmBin = fs.existsSync(npmUnified) ? npmUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'npm') + try { + execSync(`"${npmBin}" install --prefix "${PORTABLE_NODE_DIR}" ${CORE_PKG}@latest --ignore-scripts`, { + stdio: 'ignore', timeout: 120000, + env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, + }) + const corePkgPath = path.join(GLOBAL_MODULES, CORE_PKG, 'package.json') + try { coreVersion = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version } catch {} + if (agentManager) { + try { await agentManager.stopAll() } catch {} + agentManager._ensureDaemon().catch(() => {}) + } + return { success: true, version: coreVersion } + } catch (e: unknown) { + return { success: false, error: (e as Error).message } + } + }) + + ipcMain.handle('shell:open-external', (_e, url) => shell.openExternal(url)) + ipcMain.handle('shell:open-terminal', (_e, cmd) => { + const { spawn } = require('child_process') + if (process.platform === 'win32') { + const { execSync: exec } = require('child_process') + const home = process.env.USERPROFILE || os.homedir() + const portableNode = path.join(home, '.openagents', 'nodejs') + const npmBin = path.join(process.env.APPDATA || '', 'npm') + const runtimeBins: string[] = [] + try { + const rd = path.join(home, '.openagents', 'runtimes') + for (const d of fs.readdirSync(rd, { withFileTypes: true })) { + if (d.isDirectory()) runtimeBins.push(path.join(rd, d.name, 'node_modules', '.bin')) + } + } catch {} + const allBins = [...runtimeBins, path.join(portableNode, 'node_modules', '.bin'), portableNode, npmBin].join(';') + const setPath = `set PATH=${allBins};%PATH%` + exec(`start "" cmd /K "${setPath} && ${cmd}"`, { stdio: 'ignore', shell: true }) + } else if (process.platform === 'darwin') { + const home = os.homedir() + const portableNode = path.join(home, '.openagents', 'nodejs') + const portableNodeBin = path.join(portableNode, 'bin') + const runtimeBins: string[] = [] + try { + const rd = path.join(home, '.openagents', 'runtimes') + for (const d of fs.readdirSync(rd, { withFileTypes: true })) { + if (d.isDirectory()) runtimeBins.push(path.join(rd, d.name, 'node_modules', '.bin')) + } + } catch {} + const allBins = [...runtimeBins, path.join(portableNode, 'node_modules', '.bin'), portableNodeBin, portableNode, '/usr/local/bin'].join(':') + const setPath = `export PATH=${allBins}:$PATH` + const fullCmd = `${setPath} && ${cmd}`.replace(/"/g, '\\"') + spawn('osascript', ['-e', `tell app "Terminal" to do script "${fullCmd}"`], { detached: true, stdio: 'ignore' }) + } else { + const terminals = ['x-terminal-emulator', 'gnome-terminal', 'xterm'] + for (const term of terminals) { + try { spawn(term, ['-e', cmd], { detached: true, stdio: 'ignore' }); return } catch {} + } + } + }) + ipcMain.handle('shell:exec', (_e, cmd) => { + const { execSync: exec } = require('child_process') + const sh = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : true + return exec(cmd, { encoding: 'utf-8', timeout: 30000, shell: sh }) + }) + + ipcMain.handle('icons:get-dir', () => { + const coreIconsDir = path.join(GLOBAL_MODULES, CORE_PKG, 'icons') + if (fs.existsSync(coreIconsDir)) return coreIconsDir + return null + }) + ipcMain.handle('icons:get-path', (_e, name) => { + const slug = (name || '').toLowerCase().replace(/[^a-z0-9-]/g, '') + const coreIcon = path.join(GLOBAL_MODULES, CORE_PKG, 'icons', `${slug}.svg`) + if (fs.existsSync(coreIcon)) return coreIcon + return null + }) + ipcMain.handle('debug:env', () => ({ + ComSpec: process.env.ComSpec, + SystemRoot: process.env.SystemRoot, + PATH: (process.env.PATH || '').slice(0, 500), + platform: process.platform, + })) +} + +const gotLock = app.requestSingleInstanceLock() +if (!gotLock) { + app.quit() +} else { + app.on('second-instance', () => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.show() + mainWindow.focus() + } + }) +} + +app.whenReady().then(async () => { + if (process.platform !== 'darwin') Menu.setApplicationMenu(null) + + setupIPC() + createTray() + + const nodeExists = fs.existsSync(path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node')) + || fs.existsSync(path.join(PORTABLE_NODE_DIR, 'bin', 'node')) + + let splash: BrowserWindow | null = null + + if (isHeadless && process.platform === 'darwin' && app.dock) app.dock.hide() + + if (!isHeadless) { + splash = new BrowserWindow({ + width: 420, height: 260, frame: false, resizable: false, center: true, + alwaysOnTop: true, transparent: false, skipTaskbar: true, + webPreferences: { nodeIntegration: false, contextIsolation: true }, + }) + const splashHtml = `data:text/html, + +
OpenAgents Launcher
+
${!nodeExists ? 'Preparing first launch...' : 'Starting...'}
+
+
+
+
+ ` + splash.loadURL(splashHtml) + splash.show() + } + + const updateSplash = (msg: string, pct: number, detail?: string): void => { + if (splash && !splash.isDestroyed()) { + splash.webContents.executeJavaScript(` + document.getElementById('msg').textContent='${msg.replace(/'/g, "\\'")}'; + document.getElementById('bar').style.width='${pct}%'; + document.getElementById('detail').textContent='${(detail || '').replace(/'/g, "\\'")}'; + `).catch(() => {}) + } + } + + if (!nodeExists) { + slog('Node.js not found — starting download') + updateSplash('Downloading Node.js runtime...', 20, 'This only happens once') + try { + await downloadNodejs(PORTABLE_NODE_DIR, (pct, detail) => { + updateSplash('Downloading Node.js...', 20 + pct * 0.5, detail) + }) + updateSplash('Node.js installed', 70) + } catch (e: unknown) { + slog(`Node.js install FAILED: ${(e as Error).message}`) + updateSplash('Setup failed: ' + (e as Error).message, 50, 'Check ~/.openagents/startup.log') + await new Promise(r => setTimeout(r, 5000)) + } + } else { + updateSplash('Starting...', 50) + } + + const npmCliPath = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm', 'bin', 'npm-cli.js') + if (!fs.existsSync(npmCliPath)) { + slog('npm not found — installing...') + updateSplash('Installing npm...', 55) + try { + const https = require('https') + const npmVersion = '10.9.2' + const npmTgz = path.join(os.tmpdir(), `npm-${npmVersion}.tgz`) + const npmModDir = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm') + await downloadFile(https, `https://registry.npmjs.org/npm/-/npm-${npmVersion}.tgz`, npmTgz, null) + fs.mkdirSync(npmModDir, { recursive: true }) + execSync(`tar -xzf "${npmTgz}" -C "${npmModDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }) + try { fs.unlinkSync(npmTgz) } catch {} + if (process.platform === 'win32') { + const nodeExe = path.join(PORTABLE_NODE_DIR, 'node.exe') + fs.writeFileSync(path.join(PORTABLE_NODE_DIR, 'npm.cmd'), + `@echo off\r\n"${nodeExe}" "${path.join(npmModDir, 'bin', 'npm-cli.js')}" %*\r\n`) + } + slog('npm installed') + } catch (e: unknown) { + slog('npm install failed: ' + (e as Error).message) + } + } + + updateSplash('Checking for updates...', 60) + _updateSplash = updateSplash + + if (process.platform === 'win32') { + const pathDirs = (process.env.PATH || '').toLowerCase().split(';') + const candidates = [ + PORTABLE_NODE_DIR, + path.join(process.env.APPDATA || '', 'npm'), + path.join(process.env.ProgramFiles || 'C:\\Program Files', 'nodejs'), + path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs'), + ].filter(d => { + try { return d && fs.existsSync(d) && !pathDirs.includes(d.toLowerCase()) } catch { return false } + }) + if (candidates.length) process.env.PATH += ';' + candidates.join(';') + } else { + const binDir = path.join(PORTABLE_NODE_DIR, 'bin') + if (fs.existsSync(binDir) && !(process.env.PATH || '').includes(binDir)) { + process.env.PATH = binDir + ':' + (process.env.PATH || '') + } + } + + await ensureCoreLibrary() + + if (fs.existsSync(GLOBAL_MODULES) && !require('module').globalPaths.includes(GLOBAL_MODULES)) { + require('module').globalPaths.push(GLOBAL_MODULES) + } + + if (splash && !splash.isDestroyed()) { + splash.webContents.executeJavaScript(` + document.getElementById('msg').textContent='Ready!'; + document.getElementById('bar').style.width='100%'; + `).catch(() => {}) + await new Promise(r => setTimeout(r, 500)) + splash.close() + splash = null + } + + agentManager = new AgentManager(store) + agentManager!._ensureDaemon().catch(() => {}) + + setInterval(() => updateTrayMenu(), 5000) + + if (!isHeadless) createWindow() + + const FOUR_HOURS = 4 * 60 * 60 * 1000 + const ONE_HOUR = 60 * 60 * 1000 + setInterval(() => checkCoreUpdate().catch(() => {}), FOUR_HOURS) + setTimeout(() => checkCoreUpdate().catch(() => {}), 30000) + + setTimeout(() => refreshAgentUpdates(), 45000) + setInterval(() => refreshAgentUpdates(), ONE_HOUR) +}) + +app.on('window-all-closed', () => { /* keep running in tray */ }) + +app.on('activate', () => { + if (!isHeadless) createWindow() +}) + +app.on('before-quit', () => { + (app as typeof app & { isQuitting: boolean }).isQuitting = true + try { if (agentManager) agentManager.stopAll() } catch {} +}) diff --git a/packages/launcher/src/main/main.js b/packages/launcher/src/main/main.js deleted file mode 100644 index 57adc2d88..000000000 --- a/packages/launcher/src/main/main.js +++ /dev/null @@ -1,873 +0,0 @@ -const { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage, shell, dialog } = require('electron'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const { execSync } = require('child_process'); - -const isHeadless = process.argv.includes('--headless'); -if (process.argv.includes('--disable-gpu') || isHeadless) { - app.disableHardwareAcceleration(); -} - -// AgentManager is loaded lazily after core library is ensured - -// ── Core library resolution ── -// Use the globally installed core library at ~/.openagents/nodejs/ instead of -// the bundled copy. This allows independent updates without rebuilding the app. -const PORTABLE_NODE_DIR = path.join(os.homedir(), '.openagents', 'nodejs'); -const GLOBAL_MODULES = path.join(PORTABLE_NODE_DIR, 'node_modules'); -const CORE_PKG = '@openagents-org/agent-launcher'; - -// Add global modules to Node's resolution path so require(CORE_PKG) finds it -if (fs.existsSync(GLOBAL_MODULES) && !require('module').globalPaths.includes(GLOBAL_MODULES)) { - require('module').globalPaths.push(GLOBAL_MODULES); -} - -const { Store } = require('./store'); - -const store = new Store(); -let mainWindow = null; -let tray = null; -let agentManager = null; -let coreVersion = null; - -// ── Startup log (helps debug first-launch issues) ── -const STARTUP_LOG = path.join(os.homedir(), '.openagents', 'startup.log'); -function slog(msg) { - try { - fs.mkdirSync(path.dirname(STARTUP_LOG), { recursive: true }); - fs.appendFileSync(STARTUP_LOG, `${new Date().toISOString()} ${msg}\n`); - } catch {} - console.log('[startup]', msg); -} - -// ── File download helper ── -function downloadFile(https, url, destPath, onProgress) { - return new Promise((resolve, reject) => { - const doGet = (u) => { - https.get(u, (res) => { - if (res.statusCode === 302 || res.statusCode === 301) { - doGet(res.headers.location); - return; - } - if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; } - const total = parseInt(res.headers['content-length'] || '0'); - let downloaded = 0; - const file = fs.createWriteStream(destPath); - res.on('data', (chunk) => { - downloaded += chunk.length; - file.write(chunk); - if (total && onProgress) onProgress(Math.round(downloaded / total * 100), `${(downloaded/1e6).toFixed(1)} MB`); - }); - res.on('end', () => { file.end(resolve); }); - res.on('error', reject); - }).on('error', reject); - }; - doGet(url); - }); -} - -// ── Node.js download (no external deps — works from packaged app) ── -async function downloadNodejs(nodejsDir, onProgress) { - const https = require('https'); - const nodeVersion = 'v22.14.0'; - const arch = process.arch === 'arm64' ? 'arm64' : 'x64'; - - // Clean any previous failed install to avoid merge conflicts - try { fs.rmSync(nodejsDir, { recursive: true, force: true }); } catch {} - fs.mkdirSync(nodejsDir, { recursive: true }); - slog(`downloadNodejs: platform=${process.platform} arch=${arch} dir=${nodejsDir}`); - - if (process.platform === 'win32') { - // Download just node.exe (single binary, no zip extraction needed) - const nodeExeUrl = `https://nodejs.org/dist/${nodeVersion}/win-${arch}/node.exe`; - const nodeExeDest = path.join(nodejsDir, 'node.exe'); - slog(`Downloading: ${nodeExeUrl} → ${nodeExeDest}`); - - await downloadFile(https, nodeExeUrl, nodeExeDest, onProgress); - slog(`node.exe downloaded: ${fs.existsSync(nodeExeDest)}`); - - // Download npm separately (needed for installing agents) - // npm is distributed as a tarball — download and extract - const npmVersion = '10.9.2'; - const npmUrl = `https://registry.npmjs.org/npm/-/npm-${npmVersion}.tgz`; - const npmTgz = path.join(os.tmpdir(), `npm-${npmVersion}.tgz`); - const npmModDir = path.join(nodejsDir, 'node_modules', 'npm'); - slog(`Downloading npm: ${npmUrl}`); - - if (onProgress) onProgress(85, 'Installing npm...'); - await downloadFile(https, npmUrl, npmTgz, null); - - // Extract npm tarball using tar (built into Windows 10+) - fs.mkdirSync(npmModDir, { recursive: true }); - try { - execSync(`tar -xzf "${npmTgz}" -C "${npmModDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }); - slog('npm extracted successfully'); - } catch (e) { - slog(`npm extraction failed: ${e.message}`); - } - try { fs.unlinkSync(npmTgz); } catch {} - - // Create npm.cmd shim - const npmCliPath = path.join(npmModDir, 'bin', 'npm-cli.js'); - if (fs.existsSync(npmCliPath)) { - fs.writeFileSync(path.join(nodejsDir, 'npm.cmd'), - `@echo off\r\n"${nodeExeDest}" "${npmCliPath}" %*\r\n`); - fs.writeFileSync(path.join(nodejsDir, 'npx.cmd'), - `@echo off\r\n"${nodeExeDest}" "${path.join(npmModDir, 'bin', 'npx-cli.js')}" %*\r\n`); - slog('npm.cmd created'); - } else { - slog(`npm-cli.js not found at ${npmCliPath}`); - } - - } else { - // macOS/Linux: download tar.gz - const platName = process.platform === 'darwin' ? 'darwin' : 'linux'; - const ext = process.platform === 'darwin' ? 'tar.gz' : 'tar.xz'; - const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-${platName}-${arch}.${ext}`; - const tarPath = path.join(os.tmpdir(), `node-${nodeVersion}.${ext}`); - - await downloadFile(https, url, tarPath, onProgress); - - if (onProgress) onProgress(90, 'Extracting...'); - const flag = ext === 'tar.gz' ? '-xzf' : '-xJf'; - execSync(`tar ${flag} "${tarPath}" -C "${nodejsDir}" --strip-components=1`, { timeout: 120000 }); - try { fs.unlinkSync(tarPath); } catch {} - - // Unify paths with Windows: create symlinks in root so node/npm are at - // ~/.openagents/nodejs/node and ~/.openagents/nodejs/npm (same as Windows) - const binDir = path.join(nodejsDir, 'bin'); - for (const name of ['node', 'npm', 'npx']) { - const src = path.join(binDir, name); - const dest = path.join(nodejsDir, name); - if (fs.existsSync(src) && !fs.existsSync(dest)) { - try { fs.symlinkSync(src, dest); } catch {} - } - } - } - - if (onProgress) onProgress(100, 'Done'); -} - -// ── Core library management ── -const SKIP_DIRS = new Set(['.git', 'test', 'tests', 'docs', 'example', 'examples', '.github']); -function copyDirSync(src, dest) { - fs.mkdirSync(dest, { recursive: true }); - for (const entry of fs.readdirSync(src, { withFileTypes: true })) { - if (SKIP_DIRS.has(entry.name)) continue; - const s = path.join(src, entry.name); - const d = path.join(dest, entry.name); - if (entry.isDirectory()) copyDirSync(s, d); - else fs.copyFileSync(s, d); - } -} - -function findNpmCommand() { - const nodeUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node'); - const nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'node'); - if (!fs.existsSync(nodeBin)) return null; - // Always prefer node.exe + npm-cli.js (avoids cmd.exe Unicode path encoding issues) - const candidates = [ - path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm', 'bin', 'npm-cli.js'), - path.join(PORTABLE_NODE_DIR, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), - ]; - const npmCli = candidates.find(p => fs.existsSync(p)); - if (npmCli) return `"${nodeBin}" "${npmCli}"`; - // Fallback to npm.cmd (may fail on non-ASCII usernames) - if (process.platform !== 'win32') { - const npmBin = path.join(PORTABLE_NODE_DIR, 'bin', 'npm'); - if (fs.existsSync(npmBin)) return `"${npmBin}"`; - } - return null; -} - -/** - * Add a package to the prefix's package.json so npm --prefix won't prune it. - * Tarball-extracted packages aren't tracked by npm — any subsequent - * `npm install --prefix` would delete them as extraneous. - */ -function _addToPrefixPackageJson(pkg, version) { - const pkgJsonPath = path.join(PORTABLE_NODE_DIR, 'package.json'); - let data = {}; - try { data = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); } catch {} - if (!data.dependencies) data.dependencies = {}; - data.dependencies[pkg] = version; - try { fs.writeFileSync(pkgJsonPath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); } catch {} -} - -let _updateSplash = null; // set by app.whenReady, used by ensureCoreLibrary - -async function ensureCoreLibrary() { - const corePkgPath = path.join(GLOBAL_MODULES, CORE_PKG, 'package.json'); - let installedVersion = null; - - if (fs.existsSync(corePkgPath)) { - try { installedVersion = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version; } catch {} - } - - // Always try to update to latest on startup - // Use direct tarball download — avoids npm --prefix which prunes npm itself - const https = require('https'); - try { - // Check latest version from registry - const latestVersion = await new Promise((res, rej) => { - https.get(`https://registry.npmjs.org/${CORE_PKG}/latest`, r => { - let d = ''; r.on('data', c => d += c); - r.on('end', () => { try { res(JSON.parse(d).version); } catch { rej(new Error('parse error')); } }); - }).on('error', rej); - }); - - if (!installedVersion) { - slog('Core library not found — installing v' + latestVersion + '...'); - if (_updateSplash) _updateSplash('Installing core library...', 65, 'v' + latestVersion); - } else if (latestVersion !== installedVersion) { - slog('Core library v' + installedVersion + ' → v' + latestVersion + ' update available'); - if (_updateSplash) _updateSplash('Updating core library...', 65, 'v' + installedVersion + ' → v' + latestVersion); - } else { - slog('Core library v' + installedVersion + ' (already latest)'); - if (_updateSplash) _updateSplash('Core library up to date', 80, 'v' + installedVersion); - } - - if (!installedVersion || latestVersion !== installedVersion) { - // Download and extract tarball directly — no npm needed - const tgzUrl = `https://registry.npmjs.org/${CORE_PKG}/-/agent-launcher-${latestVersion}.tgz`; - const tgzPath = path.join(os.tmpdir(), `agent-launcher-${latestVersion}.tgz`); - const destDir = path.join(GLOBAL_MODULES, CORE_PKG); - - await downloadFile(https, tgzUrl, tgzPath, null); - // Remove old version - try { fs.rmSync(destDir, { recursive: true, force: true }); } catch {} - fs.mkdirSync(destDir, { recursive: true }); - execSync(`tar -xzf "${tgzPath}" -C "${destDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }); - try { fs.unlinkSync(tgzPath); } catch {} - - const newVersion = (() => { try { return JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version; } catch { return null; } })(); - if (newVersion) { - slog('Core library installed: v' + newVersion); - if (_updateSplash) _updateSplash('Core library ready', 80, 'v' + newVersion); - installedVersion = newVersion; - // Register in prefix package.json so npm --prefix won't prune it - _addToPrefixPackageJson(CORE_PKG, newVersion); - } - } - } catch (e) { - slog('Core update failed: ' + e.message); - if (!installedVersion) { - slog('Falling back to npm...'); - const npmCmd = findNpmCommand(); - if (npmCmd) { - try { - execSync(`${npmCmd} install --prefix "${PORTABLE_NODE_DIR}" ${CORE_PKG}@latest --ignore-scripts`, { - stdio: 'pipe', timeout: 120000, - env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, - }); - try { installedVersion = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version; } catch {} - } catch {} - } - } - } - - coreVersion = installedVersion; - - // npm --prefix prunes packages not in package.json — npm itself gets deleted. - // Reinstall npm if it was removed by the core update. - const npmCheck = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm', 'bin', 'npm-cli.js'); - if (!fs.existsSync(npmCheck)) { - slog('npm was removed by --prefix install — reinstalling...'); - try { - const https = require('https'); - const npmTgz = path.join(os.tmpdir(), 'npm-reinstall.tgz'); - const npmDir = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm'); - await downloadFile(https, 'https://registry.npmjs.org/npm/-/npm-10.9.2.tgz', npmTgz, null); - fs.mkdirSync(npmDir, { recursive: true }); - execSync(`tar -xzf "${npmTgz}" -C "${npmDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }); - try { fs.unlinkSync(npmTgz); } catch {} - slog('npm reinstalled'); - } catch (e) { - slog('npm reinstall failed: ' + e.message); - } - } - - // Reload agent manager with the (potentially updated) core - if (installedVersion && agentManager) { - agentManager.reloadCore(); - } -} - -async function checkCoreUpdate() { - const npmCmd = findNpmCommand(); - if (!npmCmd) { - slog('checkCoreUpdate: skipped — npm not found'); - return; - } - slog('checkCoreUpdate: using ' + npmCmd); - try { - const latest = execSync(`${npmCmd} view ${CORE_PKG} version`, { - encoding: 'utf-8', timeout: 15000, - env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, - }).trim(); - - if (coreVersion && latest && latest !== coreVersion) { - // Send update info to renderer (shown in sidebar, not a popup) - if (mainWindow) { - mainWindow.webContents.send('core-update-available', { current: coreVersion, latest }); - } - } - } catch {} -} - -function createWindow() { - if (mainWindow) { - // Restore Dock icon on macOS before showing window - if (process.platform === 'darwin' && app.dock) { - app.dock.show(); - } - mainWindow.show(); - mainWindow.focus(); - return; - } - - mainWindow = new BrowserWindow({ - width: 900, - height: 650, - minWidth: 700, - minHeight: 500, - title: 'OpenAgents Launcher', - autoHideMenuBar: true, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - contextIsolation: true, - nodeIntegration: false, - }, - show: false, - }); - - mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html')); - - mainWindow.once('ready-to-show', () => { - if (process.platform === 'darwin' && app.dock) { - app.dock.show(); - } - mainWindow.show(); - }); - - mainWindow.on('close', (e) => { - // Minimize to tray instead of closing - if (!app.isQuitting) { - e.preventDefault(); - mainWindow.hide(); - // Hide Dock icon on macOS so it doesn't distract - if (process.platform === 'darwin' && app.dock) { - app.dock.hide(); - } - } - }); - - mainWindow.on('closed', () => { - mainWindow = null; - }); -} - -function createTray() { - const assetsDir = path.join(__dirname, '..', '..', 'assets'); - let trayIcon; - - if (process.platform === 'darwin') { - // macOS: use Template icon (auto-adapts to dark/light menu bar) - trayIcon = nativeImage.createFromPath(path.join(assetsDir, 'tray-iconTemplate.png')); - } else { - // Windows/Linux: use color icon - trayIcon = nativeImage.createFromPath(path.join(assetsDir, 'tray-icon.png')); - } - - if (!trayIcon || trayIcon.isEmpty()) trayIcon = createPlaceholderIcon(); - - tray = new Tray(trayIcon); - tray.setToolTip('OpenAgents Launcher'); - - updateTrayMenu(); - - tray.on('click', () => { - createWindow(); - }); -} - -function createPlaceholderIcon() { - // Generate a 16x16 "OA" tray icon — purple circle with white center - const size = 16; - const canvas = Buffer.alloc(size * size * 4); - const cx = 7.5, cy = 7.5, r = 7, ri = 4; - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - const i = (y * size + x) * 4; - const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2); - if (d <= r) { - if (d <= ri) { - // White inner circle - canvas[i] = 0xFF; canvas[i+1] = 0xFF; canvas[i+2] = 0xFF; canvas[i+3] = 0xFF; - } else { - // Purple ring (#6C63FF) - canvas[i] = 0x6C; canvas[i+1] = 0x63; canvas[i+2] = 0xFF; canvas[i+3] = 0xFF; - } - } else { - // Transparent - canvas[i] = 0; canvas[i+1] = 0; canvas[i+2] = 0; canvas[i+3] = 0; - } - } - } - return nativeImage.createFromBuffer(canvas, { width: size, height: size }); -} - -function updateTrayMenu() { - if (!tray) return; - - const agents = agentManager ? agentManager.getAgents() : []; - const agentItems = agents.length > 0 - ? agents.map((a) => ({ - label: `${a.name} (${a.state})`, - enabled: false, - })) - : [{ label: 'No agents configured', enabled: false }]; - - const menu = Menu.buildFromTemplate([ - { label: 'Open Dashboard', click: () => createWindow() }, - { type: 'separator' }, - ...agentItems, - { type: 'separator' }, - { - label: 'Quit OpenAgents', - click: async () => { - const { dialog } = require('electron'); - const result = await dialog.showMessageBox({ - type: 'question', - buttons: ['Quit', 'Cancel'], - defaultId: 1, - title: 'Quit OpenAgents Launcher', - message: 'Quit OpenAgents Launcher?', - detail: 'The daemon will stop and all connected agents will go offline.', - }); - if (result.response === 0) { - app.isQuitting = true; - try { if (agentManager) await agentManager.stopAll(); } catch {} - app.quit(); - } - }, - }, - ]); - - tray.setContextMenu(menu); -} - -// ---- IPC Handlers ---- - -function setupIPC() { - // Runtime status (was Python, now Node.js agent-connector) - ipcMain.handle('python:status', () => ({ - pythonPath: null, - pythonFound: true, // No longer needed — always "found" since we're Node.js native - sdkInstalled: true, - sdkVersion: coreVersion || 'not installed', - launcherVersion: require('../../package.json').version, - runtime: 'node', - })); - ipcMain.handle('python:install', () => ({ success: true, message: 'No installation needed — using Node.js agent-connector' })); - - ipcMain.handle('runtime:info', async () => { - const info = { nodeVersion: null, npmVersion: null, coreVersion: coreVersion || null, latestVersion: null }; - // Node.js version — check unified path (symlink on Unix), then legacy bin/ - const nodeUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node'); - const nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'node'); - if (fs.existsSync(nodeBin)) { - try { info.nodeVersion = execSync(`"${nodeBin}" --version`, { encoding: 'utf-8', timeout: 5000 }).trim(); } catch {} - } - // npm version - const npmCmd = findNpmCommand(); - if (npmCmd) { - try { - info.npmVersion = execSync(`${npmCmd} --version`, { - encoding: 'utf-8', timeout: 5000, - env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, - }).trim(); - } catch {} - } - // Latest version (from background check or fetch now) - if (npmCmd) { - try { - info.latestVersion = execSync(`${npmCmd} view ${CORE_PKG} version`, { - encoding: 'utf-8', timeout: 10000, - env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, - }).trim(); - } catch {} - } - return info; - }); - - // Agent CRUD - ipcMain.handle('agents:list', () => agentManager.getAgents()); - ipcMain.handle('agents:supported-types', () => agentManager.getSupportedAgentTypes()); - ipcMain.handle('agents:core-info', () => agentManager.getCoreInfo()); - ipcMain.handle('agents:add', (_e, config) => agentManager.addAgent(config)); - ipcMain.handle('agents:remove', (_e, name) => agentManager.removeAgent(name)); - ipcMain.handle('agents:update', (_e, name, config) => agentManager.updateAgent(name, config)); - - // Agent lifecycle - ipcMain.handle('agents:start', (_e, name) => agentManager.startAgent(name)); - ipcMain.handle('agents:stop', (_e, name) => agentManager.stopAgent(name)); - ipcMain.handle('agents:start-all', () => agentManager.startAll()); - ipcMain.handle('agents:stop-all', () => agentManager.stopAll()); - ipcMain.handle('agents:status', () => agentManager.getAllStatus()); - ipcMain.handle('agents:logs', (_e, name, lines) => agentManager.getLogs(name, lines)); - ipcMain.handle('agents:tail-logs', (_e, name, lines, offset) => agentManager.tailLogs(name, lines, offset)); - ipcMain.handle('agents:clear-logs-range', (_e, start, end) => agentManager.clearLogsInRange(start, end)); - - // Agent install (openclaw, etc.) - ipcMain.handle('agents:install-type', (_e, agentType) => agentManager.installAgentType(agentType)); - ipcMain.handle('agents:install-type-streaming', async (_e, agentType) => { - return agentManager.installAgentTypeStreaming(agentType, (data) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('install:output', data); - } - }); - }); - ipcMain.handle('agents:uninstall-type', (_e, agentType) => agentManager.uninstallAgentType(agentType)); - ipcMain.handle('agents:uninstall-type-streaming', async (_e, agentType) => { - return agentManager.uninstallAgentTypeStreaming(agentType, (data) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('install:output', data); - } - }); - }); - ipcMain.handle('agents:check-type', (_e, agentType) => agentManager.checkAgentType(agentType)); - ipcMain.handle('agents:catalog', () => { - // Clear cached catalog to ensure fresh installed-status check - try { agentManager._connector.registry._catalog = null; } catch {} - return agentManager.getCatalog(); - }); - - // Agent configuration - ipcMain.handle('agents:env-fields', (_e, agentType) => agentManager.getEnvFields(agentType)); - ipcMain.handle('agents:get-env', (_e, agentType) => agentManager.getAgentEnv(agentType)); - ipcMain.handle('agents:save-env', (_e, agentType, env) => agentManager.saveAgentEnv(agentType, env)); - ipcMain.handle('agents:get-instance-env', (_e, agentName) => agentManager.getAgentInstanceEnv(agentName)); - ipcMain.handle('agents:save-instance-env', (_e, agentName, env) => agentManager.saveAgentInstanceEnv(agentName, env)); - ipcMain.handle('agents:test-llm', (_e, env) => agentManager.testLLM(env)); - ipcMain.handle('agents:signal-reload', () => agentManager.signalReload()); - - // Workspace connection - ipcMain.handle('workspace:connect', (_e, agentName, slug) => agentManager.connectWorkspace(agentName, slug)); - ipcMain.handle('workspace:disconnect', (_e, agentName) => agentManager.disconnectWorkspace(agentName)); - ipcMain.handle('workspace:remove', (_e, slug) => agentManager.removeWorkspace(slug)); - ipcMain.handle('workspace:list', () => agentManager.getNetworks()); - ipcMain.handle('workspace:create', (_e, name) => agentManager.createWorkspace(name)); - - // Settings - ipcMain.handle('settings:get', (_e, key) => store.get(key)); - ipcMain.handle('settings:set', (_e, key, value) => store.set(key, value)); - - // Health check - ipcMain.handle('agents:health-check', (_e, type) => agentManager.healthCheck(type)); - - // Core library update - ipcMain.handle('core:update', async () => { - const npmUnified = path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'npm.cmd' : 'npm'); - const npmBin = fs.existsSync(npmUnified) ? npmUnified : path.join(PORTABLE_NODE_DIR, 'bin', 'npm'); - try { - execSync(`"${npmBin}" install --prefix "${PORTABLE_NODE_DIR}" ${CORE_PKG}@latest --ignore-scripts`, { - stdio: 'ignore', timeout: 120000, - env: { ...process.env, PATH: PORTABLE_NODE_DIR + (process.platform === 'win32' ? ';' : ':') + (process.env.PATH || '') }, - }); - const corePkgPath = path.join(GLOBAL_MODULES, CORE_PKG, 'package.json'); - try { coreVersion = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8')).version; } catch {} - // Restart daemon - if (agentManager) { - try { await agentManager.stopAll(); } catch {} - agentManager._ensureDaemon().catch(() => {}); - } - return { success: true, version: coreVersion }; - } catch (e) { - return { success: false, error: e.message }; - } - }); - - // Shell - ipcMain.handle('shell:open-external', (_e, url) => shell.openExternal(url)); - ipcMain.handle('shell:open-terminal', (_e, cmd) => { - const { spawn } = require('child_process'); - const { getEnhancedEnv } = (() => { try { return require(path.join(os.homedir(), '.openagents', 'nodejs', 'node_modules', '@openagents-org', 'agent-launcher')).paths; } catch { try { return require('@openagents-org/agent-launcher').paths; } catch { return { getEnhancedEnv: () => process.env }; } } })(); - const env = getEnhancedEnv(); - if (process.platform === 'win32') { - // Open a visible terminal window with PATH set - const { execSync } = require('child_process'); - const home = process.env.USERPROFILE || require('os').homedir(); - const portableNode = path.join(home, '.openagents', 'nodejs'); - const npmBin = path.join(process.env.APPDATA || '', 'npm'); - // Include per-agent runtime bins + legacy shared bin - const runtimeBins = []; - try { - const rd = path.join(home, '.openagents', 'runtimes'); - for (const d of fs.readdirSync(rd, { withFileTypes: true })) { - if (d.isDirectory()) runtimeBins.push(path.join(rd, d.name, 'node_modules', '.bin')); - } - } catch {} - const allBins = [...runtimeBins, path.join(portableNode, 'node_modules', '.bin'), portableNode, npmBin].join(';'); - const setPath = `set PATH=${allBins};%PATH%`; - execSync(`start "" cmd /K "${setPath} && ${cmd}"`, { stdio: 'ignore', env, shell: true }); - } else if (process.platform === 'darwin') { - // Open Terminal.app with PATH set so agent binaries are found - const home = require('os').homedir(); - const portableNode = path.join(home, '.openagents', 'nodejs'); - const portableNodeBin = path.join(portableNode, 'bin'); - // Include per-agent runtime bins + legacy shared bin - const runtimeBins = []; - try { - const rd = path.join(home, '.openagents', 'runtimes'); - for (const d of fs.readdirSync(rd, { withFileTypes: true })) { - if (d.isDirectory()) runtimeBins.push(path.join(rd, d.name, 'node_modules', '.bin')); - } - } catch {} - const allBins = [...runtimeBins, path.join(portableNode, 'node_modules', '.bin'), portableNodeBin, portableNode, '/usr/local/bin'].join(':'); - const setPath = `export PATH=${allBins}:$PATH`; - const fullCmd = `${setPath} && ${cmd}`.replace(/"/g, '\\"'); - spawn('osascript', ['-e', `tell app "Terminal" to do script "${fullCmd}"`], { detached: true, stdio: 'ignore' }); - } else { - // Linux — try common terminal emulators - const terminals = ['x-terminal-emulator', 'gnome-terminal', 'xterm']; - for (const term of terminals) { - try { spawn(term, ['-e', cmd], { detached: true, stdio: 'ignore', env }); return; } catch {} - } - } - }); - ipcMain.handle('shell:exec', (_e, cmd) => { - const { execSync } = require('child_process'); - const { getEnhancedEnv } = (() => { try { return require(path.join(os.homedir(), '.openagents', 'nodejs', 'node_modules', '@openagents-org', 'agent-launcher')).paths; } catch { try { return require('@openagents-org/agent-launcher').paths; } catch { return { getEnhancedEnv: () => process.env }; } } })(); - const env = getEnhancedEnv(); - // Use ComSpec directly — guaranteed to be the correct path on this system - const shell = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : true; - return execSync(cmd, { encoding: 'utf-8', timeout: 30000, shell, env }); - }); - - // Debug: expose env for troubleshooting - // Icons — serve from core library, fall back to bundled - ipcMain.handle('icons:get-dir', () => { - const coreIconsDir = path.join(GLOBAL_MODULES, CORE_PKG, 'icons'); - if (fs.existsSync(coreIconsDir)) return coreIconsDir; - return null; - }); - - ipcMain.handle('icons:get-path', (_e, name) => { - const slug = (name || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); - const coreIcon = path.join(GLOBAL_MODULES, CORE_PKG, 'icons', `${slug}.svg`); - if (fs.existsSync(coreIcon)) return coreIcon; - return null; - }); - - ipcMain.handle('debug:env', () => { - return { - ComSpec: process.env.ComSpec, - SystemRoot: process.env.SystemRoot, - PATH: (process.env.PATH || '').slice(0, 500), - platform: process.platform, - }; - }); -} - -// ---- Single instance lock ---- - -const gotLock = app.requestSingleInstanceLock(); -if (!gotLock) { - // Another instance is already running — quit silently - // Note: dialog.showMessageBoxSync() cannot be used here because - // the app may not be ready yet, which crashes on Windows. - app.quit(); -} else { - // When a second instance tries to start, focus the existing window - app.on('second-instance', () => { - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.show(); - mainWindow.focus(); - } - }); -} - -// ---- App lifecycle ---- - -app.whenReady().then(async () => { - // Hide menu bar on Windows/Linux (keep on macOS for system conventions) - if (process.platform !== 'darwin') { - Menu.setApplicationMenu(null); - } - - createTray(); - - // ── Splash screen ── - const nodeExists = fs.existsSync(path.join(PORTABLE_NODE_DIR, process.platform === 'win32' ? 'node.exe' : 'node')) - || fs.existsSync(path.join(PORTABLE_NODE_DIR, 'bin', 'node')); - const coreExists = fs.existsSync(path.join(GLOBAL_MODULES, CORE_PKG, 'package.json')); - - // Always show splash — used for Node.js download, core install, AND core updates - let splash = null; - - if (isHeadless && process.platform === 'darwin' && app.dock) { - app.dock.hide(); - } - - if (!isHeadless) { - splash = new BrowserWindow({ - width: 420, height: 260, frame: false, resizable: false, center: true, - alwaysOnTop: true, transparent: false, skipTaskbar: true, - webPreferences: { nodeIntegration: false, contextIsolation: true }, - }); - const splashHtml = `data:text/html, - -
OpenAgents Launcher
-
${!nodeExists ? 'Preparing first launch...' : 'Starting...'}
-
-
-
-
- `; - splash.loadURL(splashHtml); - splash.show(); - } - - const updateSplash = (msg, pct, detail) => { - if (splash && !splash.isDestroyed()) { - splash.webContents.executeJavaScript(` - document.getElementById('msg').textContent='${msg.replace(/'/g, "\\'")}'; - document.getElementById('bar').style.width='${pct}%'; - document.getElementById('detail').textContent='${(detail || '').replace(/'/g, "\\'")}'; - `).catch(() => {}); - } - }; - - // Step 1: Install Node.js if needed - if (!nodeExists) { - slog('Node.js not found — starting download'); - updateSplash('Downloading Node.js runtime...', 20, 'This only happens once'); - try { - await downloadNodejs(PORTABLE_NODE_DIR, (pct, detail) => { - updateSplash('Downloading Node.js...', 20 + pct * 0.5, detail); - }); - const nodeExe = path.join(PORTABLE_NODE_DIR, 'node.exe'); - slog(`Download done. node.exe exists: ${fs.existsSync(nodeExe)}`); - updateSplash('Node.js installed', 70); - } catch (e) { - slog(`Node.js install FAILED: ${e.message}\n${e.stack}`); - updateSplash('Setup failed: ' + e.message, 50, 'Check ~/.openagents/startup.log'); - await new Promise(r => setTimeout(r, 5000)); - } - } else { - slog('Node.js already exists'); - updateSplash('Starting...', 50); - } - - // Step 1b: Ensure npm is installed (might be missing if old launcher installed node without npm) - const npmCliPath = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm', 'bin', 'npm-cli.js'); - if (!fs.existsSync(npmCliPath)) { - slog('npm not found — installing...'); - updateSplash('Installing npm...', 55); - try { - const https = require('https'); - const npmVersion = '10.9.2'; - const npmTgz = path.join(os.tmpdir(), `npm-${npmVersion}.tgz`); - const npmModDir = path.join(PORTABLE_NODE_DIR, 'node_modules', 'npm'); - await downloadFile(https, `https://registry.npmjs.org/npm/-/npm-${npmVersion}.tgz`, npmTgz, null); - fs.mkdirSync(npmModDir, { recursive: true }); - execSync(`tar -xzf "${npmTgz}" -C "${npmModDir}" --strip-components=1`, { timeout: 60000, stdio: 'pipe' }); - try { fs.unlinkSync(npmTgz); } catch {} - // Create npm.cmd shim on Windows - if (process.platform === 'win32') { - const nodeExe = path.join(PORTABLE_NODE_DIR, 'node.exe'); - fs.writeFileSync(path.join(PORTABLE_NODE_DIR, 'npm.cmd'), - `@echo off\r\n"${nodeExe}" "${path.join(npmModDir, 'bin', 'npm-cli.js')}" %*\r\n`); - } - slog('npm installed'); - } catch (e) { - slog('npm install failed: ' + e.message); - } - } - - // Step 2: Ensure core library (install or update) - updateSplash('Checking for updates...', 60); - _updateSplash = updateSplash; - - // ── PATH setup ── - if (process.platform === 'win32') { - const pathDirs = (process.env.PATH || '').toLowerCase().split(';'); - const candidates = [ - PORTABLE_NODE_DIR, - path.join(process.env.APPDATA || '', 'npm'), - path.join(process.env.ProgramFiles || 'C:\\Program Files', 'nodejs'), - path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs'), - ].filter(d => { - try { return d && fs.existsSync(d) && !pathDirs.includes(d.toLowerCase()); } - catch { return false; } - }); - if (candidates.length) { - process.env.PATH += ';' + candidates.join(';'); - } - } else { - const binDir = path.join(PORTABLE_NODE_DIR, 'bin'); - if (fs.existsSync(binDir) && !process.env.PATH.includes(binDir)) { - process.env.PATH = binDir + ':' + process.env.PATH; - } - } - - // Ensure core library is installed and check for updates - await ensureCoreLibrary(); - - // Add global modules path - if (fs.existsSync(GLOBAL_MODULES) && !require('module').globalPaths.includes(GLOBAL_MODULES)) { - require('module').globalPaths.push(GLOBAL_MODULES); - } - - // Close splash - if (splash && !splash.isDestroyed()) { - splash.webContents.executeJavaScript(` - document.getElementById('msg').textContent='Ready!'; - document.getElementById('bar').style.width='100%'; - `).catch(() => {}); - await new Promise(r => setTimeout(r, 500)); - splash.close(); - splash = null; - } - - const { AgentManager } = require('./agent-manager'); - agentManager = new AgentManager(store); - - // Start the daemon on app launch (long-lived background process) - agentManager._ensureDaemon().catch(() => {}); - - // Periodically update tray menu with agent status - setInterval(() => updateTrayMenu(), 5000); - - setupIPC(); - if (!isHeadless) { - createWindow(); - } - - // Check for core library updates periodically (every 4 hours) - // On startup, the auto-update already ran in ensureCoreLibrary. - // This periodic check catches updates while the app stays open for days. - const FOUR_HOURS = 4 * 60 * 60 * 1000; - setInterval(() => checkCoreUpdate().catch(() => {}), FOUR_HOURS); - // Also check once after 30s (in case the startup auto-update was slow) - setTimeout(() => checkCoreUpdate().catch(() => {}), 30000); -}); - -app.on('window-all-closed', () => { - // Don't quit — keep running in tray -}); - -app.on('activate', () => { - if (!isHeadless) { - createWindow(); - } -}); - -app.on('before-quit', () => { - app.isQuitting = true; - // Stop daemon — agents go offline when launcher quits - try { if (agentManager) agentManager.stopAll(); } catch {} -}); diff --git a/packages/launcher/src/main/preload.js b/packages/launcher/src/main/preload.js deleted file mode 100644 index e89fcd0a1..000000000 --- a/packages/launcher/src/main/preload.js +++ /dev/null @@ -1,72 +0,0 @@ -const { contextBridge, ipcRenderer } = require('electron'); - -contextBridge.exposeInMainWorld('api', { - // Python / SDK - pythonStatus: () => ipcRenderer.invoke('python:status'), - installSDK: () => ipcRenderer.invoke('python:install'), - runtimeInfo: () => ipcRenderer.invoke('runtime:info'), - - // Agents - listAgents: () => ipcRenderer.invoke('agents:list'), - getSupportedAgentTypes: () => ipcRenderer.invoke('agents:supported-types'), - getAgentCoreInfo: () => ipcRenderer.invoke('agents:core-info'), - addAgent: (config) => ipcRenderer.invoke('agents:add', config), - removeAgent: (name) => ipcRenderer.invoke('agents:remove', name), - updateAgent: (name, config) => ipcRenderer.invoke('agents:update', name, config), - - startAgent: (name) => ipcRenderer.invoke('agents:start', name), - stopAgent: (name) => ipcRenderer.invoke('agents:stop', name), - startAll: () => ipcRenderer.invoke('agents:start-all'), - stopAll: () => ipcRenderer.invoke('agents:stop-all'), - agentStatus: () => ipcRenderer.invoke('agents:status'), - agentLogs: (name, lines) => ipcRenderer.invoke('agents:logs', name, lines), - tailAgentLogs: (name, lines, offset) => ipcRenderer.invoke('agents:tail-logs', name, lines, offset), - clearLogsInRange: (start, end) => ipcRenderer.invoke('agents:clear-logs-range', start, end), - - // Agent type install & catalog - installAgentType: (type) => ipcRenderer.invoke('agents:install-type', type), - installAgentTypeStreaming: (type) => ipcRenderer.invoke('agents:install-type-streaming', type), - onInstallOutput: (callback) => ipcRenderer.on('install:output', (_e, data) => callback(data)), - removeInstallOutputListener: () => ipcRenderer.removeAllListeners('install:output'), - uninstallAgentType: (type) => ipcRenderer.invoke('agents:uninstall-type', type), - uninstallAgentTypeStreaming: (type) => ipcRenderer.invoke('agents:uninstall-type-streaming', type), - checkAgentType: (type) => ipcRenderer.invoke('agents:check-type', type), - getCatalog: () => ipcRenderer.invoke('agents:catalog'), - - // Agent configuration - getEnvFields: (type) => ipcRenderer.invoke('agents:env-fields', type), - getAgentEnv: (type) => ipcRenderer.invoke('agents:get-env', type), - saveAgentEnv: (type, env) => ipcRenderer.invoke('agents:save-env', type, env), - getAgentInstanceEnv: (name) => ipcRenderer.invoke('agents:get-instance-env', name), - saveAgentInstanceEnv: (name, env) => ipcRenderer.invoke('agents:save-instance-env', name, env), - testLLM: (env) => ipcRenderer.invoke('agents:test-llm', env), - signalReload: () => ipcRenderer.invoke('agents:signal-reload'), - - // Workspace - connectWorkspace: (agentName, slug) => ipcRenderer.invoke('workspace:connect', agentName, slug), - disconnectWorkspace: (agentName) => ipcRenderer.invoke('workspace:disconnect', agentName), - removeWorkspace: (slug) => ipcRenderer.invoke('workspace:remove', slug), - listWorkspaces: () => ipcRenderer.invoke('workspace:list'), - createWorkspace: (name) => ipcRenderer.invoke('workspace:create', name), - - // Settings - getSetting: (key) => ipcRenderer.invoke('settings:get', key), - setSetting: (key, value) => ipcRenderer.invoke('settings:set', key, value), - - // Health check - healthCheck: (type) => ipcRenderer.invoke('agents:health-check', type), - - // Shell - openExternal: (url) => ipcRenderer.invoke('shell:open-external', url), - shellExec: (cmd) => ipcRenderer.invoke('shell:exec', cmd), - openTerminal: (cmd) => ipcRenderer.invoke('shell:open-terminal', cmd), - updateCore: () => ipcRenderer.invoke('core:update'), - onCoreUpdate: (cb) => ipcRenderer.on('core-update-available', (_e, info) => cb(info)), - - // Icons - getIconPath: (name) => ipcRenderer.invoke('icons:get-path', name), - getIconsDir: () => ipcRenderer.invoke('icons:get-dir'), - - // Debug - debugEnv: () => ipcRenderer.invoke('debug:env'), -}); diff --git a/packages/launcher/src/main/python-manager.js b/packages/launcher/src/main/python-manager.js deleted file mode 100644 index 62d4ed9a5..000000000 --- a/packages/launcher/src/main/python-manager.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Manages Python environment and OpenAgents SDK installation. - * - * Checks for system Python or bundled Python, installs the SDK via pip, - * and provides the Python executable path for spawning agent processes. - */ - -const { exec, execSync } = require('child_process'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); - -class PythonManager { - constructor() { - this._pythonPath = null; - this._sdkInstalled = false; - this._sdkVersion = null; - this._detecting = false; - this._detect(); - } - - getStatus() { - return { - pythonPath: this._pythonPath, - pythonFound: !!this._pythonPath, - sdkInstalled: this._sdkInstalled, - sdkVersion: this._sdkVersion, - }; - } - - getPythonPath() { - return this._pythonPath; - } - - /** - * Run a command with proper quoting for paths with spaces on Windows. - */ - _execQuoted(pythonPath, args, opts) { - const isWin = process.platform === 'win32'; - if (isWin) { - // On Windows, quote the python path and use exec (shell) to handle spaces - const cmdLine = `"${pythonPath}" ${args.join(' ')}`; - return execSync(cmdLine, { shell: true, ...opts }).toString().trim(); - } else { - return execSync(`${pythonPath} ${args.join(' ')}`, opts).toString().trim(); - } - } - - _detect() { - if (this._detecting) return; - this._detecting = true; - - const candidates = process.platform === 'win32' - ? ['python', 'python3', 'py'] - : ['python3', 'python']; - - for (const cmd of candidates) { - try { - const version = execSync(`${cmd} --version`, { - encoding: 'utf-8', - timeout: 5000, - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - - if (version.includes('Python 3.')) { - // Get full path - const whichCmd = process.platform === 'win32' ? 'where' : 'which'; - const fullPath = execSync(`${whichCmd} ${cmd}`, { - encoding: 'utf-8', - timeout: 5000, - stdio: ['pipe', 'pipe', 'pipe'], - }).trim().split('\n')[0].trim(); - - this._pythonPath = fullPath || cmd; - break; - } - } catch { - continue; - } - } - - if (this._pythonPath) { - this._checkSDK(); - } - - this._detecting = false; - } - - _checkSDK() { - try { - const result = this._execQuoted( - this._pythonPath, - ['-c', '"import openagents; print(openagents.__version__)"'], - { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] } - ); - this._sdkInstalled = true; - this._sdkVersion = result; - } catch { - this._sdkInstalled = false; - this._sdkVersion = null; - } - } - - /** - * Install or upgrade the OpenAgents SDK via pip. - */ - installSDK() { - return new Promise((resolve, reject) => { - if (!this._pythonPath) { - reject(new Error('Python not found. Please install Python 3.10+ first.')); - return; - } - - const isWin = process.platform === 'win32'; - const args = ['-m', 'pip', 'install', '--upgrade', 'openagents']; - - if (isWin) { - const cmdLine = `"${this._pythonPath}" ${args.join(' ')}`; - exec(cmdLine, { - timeout: 120000, - encoding: 'utf-8', - shell: true, - }, (error, stdout, stderr) => { - if (error) { - reject(new Error(`pip install failed: ${(stderr || error.message).substring(0, 500)}`)); - return; - } - this._checkSDK(); - resolve({ success: true, version: this._sdkVersion, output: stdout }); - }); - } else { - exec(`${this._pythonPath} ${args.join(' ')}`, { - timeout: 120000, - encoding: 'utf-8', - }, (error, stdout, stderr) => { - if (error) { - reject(new Error(`pip install failed: ${(stderr || error.message).substring(0, 500)}`)); - return; - } - this._checkSDK(); - resolve({ success: true, version: this._sdkVersion, output: stdout }); - }); - } - }); - } -} - -module.exports = { PythonManager }; diff --git a/packages/launcher/src/main/store.js b/packages/launcher/src/main/store.js deleted file mode 100644 index aa9a7d119..000000000 --- a/packages/launcher/src/main/store.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Simple JSON file-based settings store. - * Replaces electron-store to avoid ESM compatibility issues. - */ - -const fs = require('fs'); -const path = require('path'); -const { app } = require('electron'); - -class Store { - constructor(defaults = {}) { - this._data = { ...defaults }; - this._pathResolved = false; - this._path = null; - } - - _ensurePath() { - if (!this._pathResolved) { - this._path = path.join(app.getPath('userData'), 'settings.json'); - this._pathResolved = true; - this._load(); - } - } - - _load() { - try { - if (this._path && fs.existsSync(this._path)) { - const raw = fs.readFileSync(this._path, 'utf-8'); - this._data = { ...this._data, ...JSON.parse(raw) }; - } - } catch {} - } - - _save() { - this._ensurePath(); - try { - const dir = path.dirname(this._path); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(this._path, JSON.stringify(this._data, null, 2), 'utf-8'); - } catch (err) { - console.error('Failed to save settings:', err); - } - } - - get(key) { - this._ensurePath(); - if (key === undefined) return { ...this._data }; - return this._data[key]; - } - - set(key, value) { - if (typeof key === 'object') { - Object.assign(this._data, key); - } else { - this._data[key] = value; - } - this._save(); - } - - delete(key) { - delete this._data[key]; - this._save(); - } - - has(key) { - return key in this._data; - } -} - -module.exports = { Store }; diff --git a/packages/launcher/src/main/store.ts b/packages/launcher/src/main/store.ts new file mode 100644 index 000000000..690a2fc03 --- /dev/null +++ b/packages/launcher/src/main/store.ts @@ -0,0 +1,65 @@ +import fs from 'fs' +import path from 'path' +import { app } from 'electron' + +export class Store { + private _data: Record = {} + private _pathResolved = false + private _path: string | null = null + + constructor(defaults: Record = {}) { + this._data = { ...defaults } + } + + private _ensurePath(): void { + if (!this._pathResolved) { + this._path = path.join(app.getPath('userData'), 'settings.json') + this._pathResolved = true + this._load() + } + } + + private _load(): void { + try { + if (this._path && fs.existsSync(this._path)) { + const raw = fs.readFileSync(this._path, 'utf-8') + this._data = { ...this._data, ...JSON.parse(raw) } + } + } catch {} + } + + private _save(): void { + this._ensurePath() + try { + const dir = path.dirname(this._path!) + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(this._path!, JSON.stringify(this._data, null, 2), 'utf-8') + } catch (err) { + console.error('Failed to save settings:', err) + } + } + + get(key?: string): unknown { + this._ensurePath() + if (key === undefined) return { ...this._data } + return this._data[key] + } + + set(key: string | Record, value?: unknown): void { + if (typeof key === 'object') { + Object.assign(this._data, key) + } else { + this._data[key] = value + } + this._save() + } + + delete(key: string): void { + delete this._data[key] + this._save() + } + + has(key: string): boolean { + return key in this._data + } +} diff --git a/packages/launcher/src/preload/index.ts b/packages/launcher/src/preload/index.ts new file mode 100644 index 000000000..30c485d4a --- /dev/null +++ b/packages/launcher/src/preload/index.ts @@ -0,0 +1,73 @@ +import { contextBridge, ipcRenderer } from 'electron' + +contextBridge.exposeInMainWorld('api', { + pythonStatus: () => ipcRenderer.invoke('python:status'), + installSDK: () => ipcRenderer.invoke('python:install'), + runtimeInfo: () => ipcRenderer.invoke('runtime:info'), + + listAgents: () => ipcRenderer.invoke('agents:list'), + getSupportedAgentTypes: () => ipcRenderer.invoke('agents:supported-types'), + getAgentCoreInfo: () => ipcRenderer.invoke('agents:core-info'), + addAgent: (config: unknown) => ipcRenderer.invoke('agents:add', config), + removeAgent: (name: string) => ipcRenderer.invoke('agents:remove', name), + updateAgent: (name: string, config: unknown) => ipcRenderer.invoke('agents:update', name, config), + + startAgent: (name: string) => ipcRenderer.invoke('agents:start', name), + stopAgent: (name: string) => ipcRenderer.invoke('agents:stop', name), + startAll: () => ipcRenderer.invoke('agents:start-all'), + stopAll: () => ipcRenderer.invoke('agents:stop-all'), + agentStatus: () => ipcRenderer.invoke('agents:status'), + agentLogs: (name: string, lines: number) => ipcRenderer.invoke('agents:logs', name, lines), + tailAgentLogs: (name: string, lines: number, offset: number) => ipcRenderer.invoke('agents:tail-logs', name, lines, offset), + clearLogsInRange: (start: string, end: string) => ipcRenderer.invoke('agents:clear-logs-range', start, end), + + installAgentType: (type: string) => ipcRenderer.invoke('agents:install-type', type), + installAgentTypeStreaming: (type: string) => ipcRenderer.invoke('agents:install-type-streaming', type), + onInstallOutput: (callback: (data: string) => void) => ipcRenderer.on('install:output', (_e, data) => callback(data)), + removeInstallOutputListener: () => ipcRenderer.removeAllListeners('install:output'), + onInstallProgress: (callback: (ev: unknown) => void) => ipcRenderer.on('install:progress', (_e, ev) => callback(ev)), + removeInstallProgressListener: () => ipcRenderer.removeAllListeners('install:progress'), + uninstallAgentType: (type: string) => ipcRenderer.invoke('agents:uninstall-type', type), + uninstallAgentTypeStreaming: (type: string) => ipcRenderer.invoke('agents:uninstall-type-streaming', type), + checkAgentType: (type: string) => ipcRenderer.invoke('agents:check-type', type), + getCatalog: () => ipcRenderer.invoke('agents:catalog'), + getInstalledAgents: () => ipcRenderer.invoke('agents:installed-list'), + checkAgentUpdates: () => ipcRenderer.invoke('agents:check-updates'), + rollbackAgentType: (type: string) => ipcRenderer.invoke('agents:rollback', type), + getAgentChangelog: (type: string) => ipcRenderer.invoke('agents:changelog', type), + + getEnvFields: (type: string) => ipcRenderer.invoke('agents:env-fields', type), + getAgentEnv: (type: string) => ipcRenderer.invoke('agents:get-env', type), + saveAgentEnv: (type: string, env: unknown) => ipcRenderer.invoke('agents:save-env', type, env), + getAgentInstanceEnv: (name: string) => ipcRenderer.invoke('agents:get-instance-env', name), + saveAgentInstanceEnv: (name: string, env: unknown) => ipcRenderer.invoke('agents:save-instance-env', name, env), + testLLM: (env: unknown) => ipcRenderer.invoke('agents:test-llm', env), + signalReload: () => ipcRenderer.invoke('agents:signal-reload'), + + connectWorkspace: (agentName: string, slug: string) => ipcRenderer.invoke('workspace:connect', agentName, slug), + disconnectWorkspace: (agentName: string) => ipcRenderer.invoke('workspace:disconnect', agentName), + removeWorkspace: (slug: string) => ipcRenderer.invoke('workspace:remove', slug), + listWorkspaces: () => ipcRenderer.invoke('workspace:list'), + createWorkspace: (name: string) => ipcRenderer.invoke('workspace:create', name), + + getSetting: (key: string) => ipcRenderer.invoke('settings:get', key), + setSetting: (key: string, value: unknown) => ipcRenderer.invoke('settings:set', key, value), + + healthCheck: (type: string) => ipcRenderer.invoke('agents:health-check', type), + + openExternal: (url: string) => ipcRenderer.invoke('shell:open-external', url), + shellExec: (cmd: string) => ipcRenderer.invoke('shell:exec', cmd), + openTerminal: (cmd: string) => ipcRenderer.invoke('shell:open-terminal', cmd), + updateCore: () => ipcRenderer.invoke('core:update'), + onCoreUpdate: (cb: (info: { current: string; latest: string }) => void) => + ipcRenderer.on('core-update-available', (_e, info) => cb(info)), + onAgentUpdatesChanged: (cb: (updates: Array<{ name: string; current: string | null; latest: string | null }>) => void) => + ipcRenderer.on('agent-updates-changed', (_e, updates) => cb(updates)), + onNavigateToInstall: (cb: (agentName: string) => void) => + ipcRenderer.on('navigate-to-install', (_e, name) => cb(name)), + + getIconPath: (name: string) => ipcRenderer.invoke('icons:get-path', name), + getIconsDir: () => ipcRenderer.invoke('icons:get-dir'), + + debugEnv: () => ipcRenderer.invoke('debug:env'), +}) diff --git a/packages/launcher/src/renderer/App.tsx b/packages/launcher/src/renderer/App.tsx new file mode 100644 index 000000000..29a4e9369 --- /dev/null +++ b/packages/launcher/src/renderer/App.tsx @@ -0,0 +1,78 @@ +import React, { useEffect } from "react" +import { useShallow } from "zustand/react/shallow" +import { useUiStore } from "./store/ui" +import { useAgentsStore } from "./store/agents" +import { useInstallStore } from "./store/install" +import Sidebar from "./components/Sidebar" +import { ToastContainer } from "./components/ui/Toast" +import Dashboard from "./pages/dashboard" +import Agents from "./pages/agents" +import Install from "./pages/install" +import Logs from "./pages/logs" +import Settings from "./pages/settings" +import { InstallMiniBanner } from "./components/InstallProgress" +import { useToasts } from "./hooks/useToast" +import { useInstallProgress } from "./hooks/useInstallProgress" + +export default function App(): React.JSX.Element { + const currentTab = useUiStore((s) => s.currentTab) + const setCurrentTab = useUiStore((s) => s.setCurrentTab) + const setCoreUpdateInfo = useAgentsStore((s) => s.setCoreUpdateInfo) + const { showToast } = useToasts() + + // Global install:progress + install:output subscription + useInstallProgress() + + const { jobs } = useInstallStore(useShallow((s) => ({ jobs: s.jobs }))) + + useEffect(() => { + window.api.onCoreUpdate((info) => setCoreUpdateInfo(info)) + window.api.onAgentUpdatesChanged((updates) => useInstallStore.getState().setUpdates(updates)) + window.api.onNavigateToInstall((name?: string) => { + setCurrentTab("install") + if (name) useUiStore.getState().setInstallFocusAgent(name) + }) + }, [setCoreUpdateInfo, setCurrentTab]) + + useEffect(() => { + const tabs = ["dashboard", "agents", "install", "logs", "settings"] + const handler = (e: KeyboardEvent): void => { + if (e.ctrlKey && e.key >= "1" && e.key <= "5") { + e.preventDefault() + useUiStore.getState().setCurrentTab(tabs[parseInt(e.key) - 1]) + } + } + document.addEventListener("keydown", handler) + return () => document.removeEventListener("keydown", handler) + }, []) + + const activeJob = Object.values(jobs) + .filter((j) => j.phase !== "done" && j.phase !== "error") + .sort((a, b) => b.startedAt - a.startedAt)[0] + + return ( +
+ + +
+ {currentTab === "dashboard" && ( + {}} + onOpenConnectWorkspace={() => {}} + /> + )} + {currentTab === "agents" && } + {currentTab === "install" && } + {currentTab === "logs" && } + {currentTab === "settings" && } +
+ + {activeJob && currentTab !== "install" && ( + setCurrentTab("install")} /> + )} + + +
+ ) +} diff --git a/packages/launcher/src/renderer/components/AgentIcon.tsx b/packages/launcher/src/renderer/components/AgentIcon.tsx new file mode 100644 index 000000000..df8b071ca --- /dev/null +++ b/packages/launcher/src/renderer/components/AgentIcon.tsx @@ -0,0 +1,46 @@ +import React from "react" + +const BUNDLED_SLUGS = new Set([ + "aider", + "amp", + "claude", + "cline", + "codex", + "copilot", + "cursor", + "default", + "gemini", + "goose", + "kimi", + "nanoclaw", + "openai", + "openclaw", + "opencode", + "swebench", + "yaml-agent", +]) + +interface AgentIconProps { + type: string + size?: number + className?: string +} + +export default function AgentIcon({ + type, + size = 24, + className, +}: AgentIconProps): React.JSX.Element { + const slug = (type || "").toLowerCase().replace(/[^a-z0-9-]/g, "") + const iconSlug = BUNDLED_SLUGS.has(slug) ? slug : "default" + return ( + {type} + ) +} diff --git a/packages/launcher/src/renderer/components/InstallProgress.tsx b/packages/launcher/src/renderer/components/InstallProgress.tsx new file mode 100644 index 000000000..450698b0f --- /dev/null +++ b/packages/launcher/src/renderer/components/InstallProgress.tsx @@ -0,0 +1,83 @@ +import React from "react" +import { cn } from "../lib/utils" +import type { InstallPhase } from "../types" +import type { InstallJob } from "../store/install" + +const PHASES: Array<{ key: InstallPhase; label: string }> = [ + { key: "downloading", label: "Download" }, + { key: "installing", label: "Install" }, + { key: "verifying", label: "Verify" }, + { key: "done", label: "Done" }, +] + +function phaseIndex(phase: InstallPhase): number { + switch (phase) { + case "preparing": return 0 + case "downloading": return 0 + case "installing": return 1 + case "verifying": return 2 + case "done": return 3 + case "error": return -1 + default: return -1 + } +} + +interface PhaseBarProps { + phase: InstallPhase + detail?: string + errored?: boolean +} + +export function PhaseBar({ phase, detail, errored }: PhaseBarProps): React.JSX.Element { + const current = phaseIndex(phase) + return ( +
+ {PHASES.map((p, i) => { + const isActive = !errored && i === current + const isDone = !errored && i < current + const isError = errored && i === Math.max(current, 0) + return ( +
+
{p.label}
+
{isActive ? detail || "…" : isDone ? "✓" : isError ? "Failed" : ""}
+
+ ) + })} +
+ ) +} + +export function InstallMiniBanner({ job, onOpen }: { job: InstallJob; onOpen: () => void }): React.JSX.Element { + const idx = phaseIndex(job.phase) + const pct = job.phase === "done" ? 100 + : job.phase === "error" ? 100 + : Math.max(10, ((idx + 1) / PHASES.length) * 100 - 10) + const errored = job.phase === "error" + return ( + + ) +} diff --git a/packages/launcher/src/renderer/components/SetupWizard.tsx b/packages/launcher/src/renderer/components/SetupWizard.tsx new file mode 100644 index 000000000..387e052a3 --- /dev/null +++ b/packages/launcher/src/renderer/components/SetupWizard.tsx @@ -0,0 +1,195 @@ +import React, { useEffect, useState } from "react" +import { Modal, ModalTitle } from "./ui/Modal" +import { Button } from "./ui/Button" +import { Input } from "./ui/Input" +import { PasswordInput } from "./ui/PasswordInput" +import AgentIcon from "./AgentIcon" +import { cn } from "../lib/utils" +import { useUiStore } from "../store/ui" +import type { CatalogEntry, EnvField } from "../types" +import type { ToastType } from "../hooks/useToast" + +type Step = "configure" | "test" | "create" + +interface SetupWizardProps { + entry: CatalogEntry | null + open: boolean + onClose: () => void + showToast: (msg: string, type?: ToastType) => void +} + +export default function SetupWizard({ entry, open, onClose, showToast }: SetupWizardProps): React.JSX.Element | null { + const [step, setStep] = useState("configure") + const [envFields, setEnvFields] = useState([]) + const [envValues, setEnvValues] = useState>({}) + const [testing, setTesting] = useState(false) + const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null) + const [agentName, setAgentName] = useState("") + const [submitting, setSubmitting] = useState(false) + const setCurrentTab = useUiStore((s) => s.setCurrentTab) + + useEffect(() => { + if (!open || !entry) return + setStep("configure") + setTestResult(null) + setAgentName(`my-${entry.name}`) + ;(async () => { + try { + const [fields, saved] = await Promise.all([ + window.api.getEnvFields(entry.name).catch(() => [] as EnvField[]), + window.api.getAgentEnv(entry.name).catch(() => ({}) as Record), + ]) + setEnvFields(fields || []) + setEnvValues({ ...(saved || {}) }) + // If no config required, jump to create step + if (!fields || fields.length === 0) setStep("create") + } catch { + setEnvFields([]) + } + })() + }, [open, entry]) + + if (!entry) return null + + async function saveAndTest(): Promise { + if (!entry) return + setTesting(true) + setTestResult(null) + try { + await window.api.saveAgentEnv(entry.name, envValues) + try { + const r = await window.api.testLLM(envValues) + if (r.success) { + setTestResult({ ok: true, message: `OK — ${r.model || "model"} responded` }) + setStep("test") + } else { + setTestResult({ ok: false, message: r.error || "Test failed" }) + } + } catch (e: unknown) { + setTestResult({ ok: false, message: (e as Error).message }) + } + } finally { + setTesting(false) + } + } + + async function createAgent(): Promise { + if (!entry) return + setSubmitting(true) + try { + await window.api.addAgent({ name: agentName.trim() || `my-${entry.name}`, type: entry.name }) + showToast(`Created agent "${agentName}"`, "success") + onClose() + setCurrentTab("agents") + } catch (e: unknown) { + showToast(`Failed to create agent: ${(e as Error).message}`, "error") + } finally { + setSubmitting(false) + } + } + + const stepIndex: Record = { configure: 0, test: 1, create: 2 } + const idx = stepIndex[step] + + return ( + +
+ + Set up {entry.label || entry.name} +
+

+ A short wizard to get you from install to first agent run. +

+ +
+ {[ + { key: "configure" as Step, label: "API Key" }, + { key: "test" as Step, label: "Test" }, + { key: "create" as Step, label: "Create" }, + ].map((s, i) => ( + + {i > 0 &&
} +
i && "done")}> + {idx > i ? "✓" : i + 1} + {s.label} +
+ + ))} +
+ + {step === "configure" && ( + <> + {envFields.length === 0 ? ( +

No configuration required. You can create your first agent.

+ ) : ( + <> + {envFields.map((f) => { + const FieldInput = f.password ? PasswordInput : Input + return ( +
+ + setEnvValues({ ...envValues, [f.name]: e.target.value })} + placeholder={f.placeholder || `Enter ${f.name}…`} + /> +
+ ) + })} + {testResult && ( +

+ {testResult.message} +

+ )} + + )} +
+ {envFields.length === 0 ? ( + + ) : ( + + )} + +
+ + )} + + {step === "test" && ( + <> +

+ {testResult?.message || "Connection successful."} +

+

Now name your first agent instance.

+
+ + +
+ + )} + + {step === "create" && ( + <> +
+ + setAgentName(e.target.value)} + placeholder={`my-${entry.name}`} + /> +
+
+ + +
+ + )} + + ) +} diff --git a/packages/launcher/src/renderer/components/Sidebar.tsx b/packages/launcher/src/renderer/components/Sidebar.tsx new file mode 100644 index 000000000..278d47c36 --- /dev/null +++ b/packages/launcher/src/renderer/components/Sidebar.tsx @@ -0,0 +1,118 @@ +import React from "react" +import { cn } from "../lib/utils" +import { useUiStore } from "../store/ui" +import { useAgentsStore, useDaemonStatus } from "../store/agents" +import { useShallow } from "zustand/react/shallow" + +const NAV_ITEMS = [ + { id: "dashboard", label: "Dashboard", icon: "●" }, + { id: "agents", label: "Agents", icon: "⚙" }, + { id: "install", label: "Install", icon: "↓" }, + { id: "logs", label: "Logs", icon: "☰" }, + { id: "settings", label: "Settings", icon: "⚙" }, +] + +export default function Sidebar(): React.JSX.Element { + const { currentTab, setCurrentTab, goToInstallList } = useUiStore( + useShallow((s) => ({ + currentTab: s.currentTab, + setCurrentTab: s.setCurrentTab, + goToInstallList: s.goToInstallList, + })), + ) + const { coreVersion, launcherVersion, coreUpdateInfo } = useAgentsStore( + useShallow((s) => ({ coreVersion: s.coreVersion, launcherVersion: s.launcherVersion, coreUpdateInfo: s.coreUpdateInfo })), + ) + const daemonStatus = useDaemonStatus() + + const daemonLabel = + daemonStatus === "online" ? "Daemon: running" : + daemonStatus === "starting" ? "Daemon: starting" : + "Daemon: offline" + + return ( + + ) +} diff --git a/packages/launcher/src/renderer/components/ui/Badge.tsx b/packages/launcher/src/renderer/components/ui/Badge.tsx new file mode 100644 index 000000000..f61113f78 --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/Badge.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +export type BadgeVariant = + | "default" | "success" | "warning" | "danger" | "info" + | "success-sm" | "warning-sm" | "danger-sm" | "muted-sm" + +export interface BadgeProps extends React.HTMLAttributes { + variant?: BadgeVariant +} + +const variantClass: Record = { + "default": "bg-(--bg-input) text-(--text-secondary) text-[10px] font-semibold uppercase tracking-[0.03em] px-[10px] py-[3px] rounded-full", + "success": "bg-(--success-bg) text-(--success-text) text-[10px] font-semibold uppercase tracking-[0.03em] px-[10px] py-[3px] rounded-full", + "warning": "bg-(--warning-bg) text-(--warning-text) text-[10px] font-semibold uppercase tracking-[0.03em] px-[10px] py-[3px] rounded-full", + "danger": "bg-(--danger-bg) text-(--danger-text) text-[10px] font-semibold uppercase tracking-[0.03em] px-[10px] py-[3px] rounded-full", + "info": "bg-[#e0e7ff] text-[#3730a3] text-[10px] font-semibold uppercase tracking-[0.03em] px-[10px] py-[3px] rounded-full", + "success-sm": "bg-(--success-bg) text-(--success-text) text-[10px] font-medium px-[6px] py-[2px] rounded", + "warning-sm": "bg-(--warning-bg) text-(--warning-text) text-[10px] font-medium px-[6px] py-[2px] rounded", + "danger-sm": "bg-(--danger-bg) text-(--danger-text) text-[10px] font-medium px-[6px] py-[2px] rounded", + "muted-sm": "bg-[#f0f0f0] text-[#888] text-[10px] font-medium px-[6px] py-[2px] rounded", +} + +function Badge({ className, variant = "default", ...props }: BadgeProps): React.JSX.Element { + return ( + + ) +} + +export { Badge } diff --git a/packages/launcher/src/renderer/components/ui/Button.tsx b/packages/launcher/src/renderer/components/ui/Button.tsx new file mode 100644 index 000000000..24f22cb52 --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/Button.tsx @@ -0,0 +1,44 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: "default" | "primary" | "destructive" | "ghost" | "link" + size?: "default" | "sm" | "lg" | "icon" +} + +const variantClass: Record, string> = { + default: "bg-(--bg-card) text-(--text-primary) border-(--border) shadow-(--shadow-sm) hover:enabled:border-(--border-hover) hover:enabled:shadow-(--shadow-md) hover:enabled:bg-(--bg-card-hover)", + primary: "bg-(--accent) text-(--accent-text) font-semibold border-transparent shadow-[0_1px_4px_rgba(88,86,214,0.2)] hover:enabled:bg-(--accent-hover) hover:enabled:shadow-[0_3px_10px_rgba(88,86,214,0.25)]", + destructive: "bg-(--bg-card) text-(--danger-text) border-[rgba(255,59,48,0.2)] shadow-(--shadow-sm) hover:enabled:bg-(--danger-bg) hover:enabled:border-[rgba(255,59,48,0.35)]", + ghost: "bg-transparent border-transparent shadow-none hover:enabled:bg-(--bg-input)", + link: "border-transparent shadow-none text-(--accent) underline-offset-4 hover:enabled:underline px-0", +} + +const sizeClass: Record, string> = { + default: "px-4 py-[7px] text-[12px]", + sm: "px-3 py-[5px] text-[11px]", + lg: "px-5 py-[9px] text-[13px]", + icon: "h-8 w-8 p-0", +} + +const Button = React.forwardRef( + ({ className, variant = "default", size = "default", ...props }, ref) => ( + +
+ ) + }, +) +PasswordInput.displayName = "PasswordInput" + +export { PasswordInput } diff --git a/packages/launcher/src/renderer/components/ui/SearchInput.tsx b/packages/launcher/src/renderer/components/ui/SearchInput.tsx new file mode 100644 index 000000000..adbc80062 --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/SearchInput.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import { Search, X } from "lucide-react" +import { cn } from "../../lib/utils" + +export interface SearchInputProps extends React.InputHTMLAttributes { + onClear?: () => void +} + +const SearchInput = React.forwardRef( + ({ className, value, onClear, onChange, ...props }, ref) => ( +
+ + + {value && onClear && ( + + )} +
+ ), +) +SearchInput.displayName = "SearchInput" + +export { SearchInput } diff --git a/packages/launcher/src/renderer/components/ui/Select.tsx b/packages/launcher/src/renderer/components/ui/Select.tsx new file mode 100644 index 000000000..43ae4bf5b --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/Select.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +const Select = React.forwardRef>( + ({ className, children, ...props }, ref) => ( + + ), +) +Select.displayName = "Select" + +export { Select } diff --git a/packages/launcher/src/renderer/components/ui/Separator.tsx b/packages/launcher/src/renderer/components/ui/Separator.tsx new file mode 100644 index 000000000..8356ec4ba --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/Separator.tsx @@ -0,0 +1,19 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +export interface SeparatorProps extends React.HTMLAttributes { + orientation?: "horizontal" | "vertical" +} + +const Separator = React.forwardRef( + ({ orientation = "horizontal", className, ...props }, ref) => ( +
+ ), +) +Separator.displayName = "Separator" + +export { Separator } diff --git a/packages/launcher/src/renderer/components/ui/Skeleton.tsx b/packages/launcher/src/renderer/components/ui/Skeleton.tsx new file mode 100644 index 000000000..00ab4b723 --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/Skeleton.tsx @@ -0,0 +1,8 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +function Skeleton({ className, ...props }: React.HTMLAttributes): React.JSX.Element { + return
+} + +export { Skeleton } diff --git a/packages/launcher/src/renderer/components/ui/StatusDot.tsx b/packages/launcher/src/renderer/components/ui/StatusDot.tsx new file mode 100644 index 000000000..a8db55a67 --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/StatusDot.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import { cn } from "../../lib/utils" +import type { AgentState } from "../../types" + +interface StatusDotProps { + state: AgentState | string + className?: string +} + +export function statusClass(state: string): "online" | "starting" | "offline" { + if (["online", "running", "idle"].includes(state)) return "online" + if (["starting", "reconnecting"].includes(state)) return "starting" + return "offline" +} + +export function displayState(state: string): string { + if (state === "idle") return "running" + return state || "stopped" +} + +export default function StatusDot({ state, className }: StatusDotProps): React.JSX.Element { + const s = statusClass(state) + return ( + + ) +} diff --git a/packages/launcher/src/renderer/components/ui/Switch.tsx b/packages/launcher/src/renderer/components/ui/Switch.tsx new file mode 100644 index 000000000..0fc67b988 --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/Switch.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +export interface SwitchProps { + checked: boolean + onCheckedChange: (v: boolean) => void + id?: string + disabled?: boolean + className?: string +} + +const Switch = React.forwardRef( + ({ checked, onCheckedChange, id, disabled, className }, ref) => ( + + ), +) +Switch.displayName = "Switch" + +export { Switch } diff --git a/packages/launcher/src/renderer/components/ui/Tabs.tsx b/packages/launcher/src/renderer/components/ui/Tabs.tsx new file mode 100644 index 000000000..a85ffe34c --- /dev/null +++ b/packages/launcher/src/renderer/components/ui/Tabs.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +// ─── context ─────────────────────────────────────────────────────────────── +const TabsCtx = React.createContext<{ value: string; onChange: (v: string) => void } | null>(null) + +function useTabs(): { value: string; onChange: (v: string) => void } { + const ctx = React.useContext(TabsCtx) + if (!ctx) throw new Error("Tabs: must be used inside ") + return ctx +} + +// ─── Tabs (root) ─────────────────────────────────────────────────────────── +interface TabsProps { value: string; onValueChange: (v: string) => void; children: React.ReactNode; className?: string } +function Tabs({ value, onValueChange, children, className }: TabsProps): React.JSX.Element { + return ( + +
{children}
+
+ ) +} + +// ─── TabsList ────────────────────────────────────────────────────────────── +const TabsList = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +TabsList.displayName = "TabsList" + +// ─── TabsTrigger ─────────────────────────────────────────────────────────── +interface TabsTriggerProps extends React.ButtonHTMLAttributes { value: string } +const TabsTrigger = React.forwardRef( + ({ className, value, ...props }, ref) => { + const { value: active, onChange } = useTabs() + const isActive = active === value + return ( + -
-
- - - - -
-

Activity

-
-
- - - -
-

My Agents

-
- -
-
-
- - -
-

Install

- - -
-

Agent Runtimes

-

Select a runtime to install or update.

- -
-
Loading catalog...
-
- -
-
- - -
-

Logs

-
- - - - - -
-
No logs available.
-
- - -
-

Settings

-
-

General

-
- -
-
- -
-
-
-

Workspaces

-
Loading...
-
-
-

Runtime

-
- Node.js: - Checking... -
-
- npm: - Checking... -
-
- Core Library: - Checking... -
-
- Latest Available: - Checking... -
-
-
-

About

-

OpenAgents Launcher --

-

Documentation

-
-
- - -
- - - - - - + + + + OpenAgents Launcher + + +
+ + diff --git a/packages/launcher/src/renderer/main.tsx b/packages/launcher/src/renderer/main.tsx new file mode 100644 index 000000000..e41e3dad3 --- /dev/null +++ b/packages/launcher/src/renderer/main.tsx @@ -0,0 +1,10 @@ +import './globals.css' +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +) diff --git a/packages/launcher/src/renderer/pages/agents/AgentDetail.tsx b/packages/launcher/src/renderer/pages/agents/AgentDetail.tsx new file mode 100644 index 000000000..886bcfb81 --- /dev/null +++ b/packages/launcher/src/renderer/pages/agents/AgentDetail.tsx @@ -0,0 +1,492 @@ +import React, { useEffect, useState } from "react" +import { Button } from "../../components/ui/Button" +import { Input } from "../../components/ui/Input" +import { PasswordInput } from "../../components/ui/PasswordInput" +import { Badge } from "../../components/ui/Badge" +import { Modal, ModalTitle } from "../../components/ui/Modal" +import AgentIcon from "../../components/AgentIcon" +import { PhaseBar } from "../../components/InstallProgress" +import { useInstallStore } from "../../store/install" +import type { CatalogEntry, EnvField, AgentUpdateInfo, InstalledAgentRecord, HealthCheck } from "../../types" +import type { ToastType } from "../../hooks/useToast" + +const SECTION = "px-4.5 py-4 bg-(--bg-card) border border-(--border) rounded-(--radius) shadow-sm" +const SECTION_H4 = "text-xs font-semibold uppercase tracking-wider text-(--text-secondary) m-0 mb-2.5" +const DL = "grid grid-cols-[max-content_1fr] gap-x-3.5 gap-y-1.5 m-0 text-xs [&>dt]:text-(--text-tertiary) [&>dd]:m-0 [&>dd]:text-(--text-primary) [&>dd]:wrap-break-word" + +interface AgentDetailProps { + entry: CatalogEntry + onBack: () => void + onAfterInstall: (entry: CatalogEntry) => void + onOpenWizard?: (entry: CatalogEntry) => void + showToast: (message: string, type?: ToastType) => void +} + +interface ChangelogState { + versions: Array<{ version: string; date?: string }> + homepage?: string + error?: string + loading: boolean +} + +export default function AgentDetail({ + entry, + onBack, + onAfterInstall, + onOpenWizard, + showToast, +}: AgentDetailProps): React.JSX.Element { + const [envFields, setEnvFields] = useState([]) + const [envValues, setEnvValues] = useState>({}) + const [savingEnv, setSavingEnv] = useState(false) + const [testing, setTesting] = useState(false) + const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null) + const [changelog, setChangelog] = useState({ versions: [], loading: true }) + const [installed, setInstalled] = useState(null) + const [update, setUpdate] = useState(null) + const [health, setHealth] = useState(null) + const [showLog, setShowLog] = useState(false) + const [confirmingUninstall, setConfirmingUninstall] = useState(false) + + const job = useInstallStore((s) => s.jobs[entry.name]) + + // Re-fetch when a job for this agent reaches a terminal state so the + // header / version / rollback availability reflect the new install record. + const jobPhase = job?.phase + useEffect(() => { + let cancelled = false + ;(async () => { + try { + const [fields, typeSaved, list, updates, change, healthInfo] = await Promise.all([ + window.api.getEnvFields(entry.name).catch(() => [] as EnvField[]), + window.api.getAgentEnv(entry.name).catch(() => ({}) as Record), + window.api.getInstalledAgents().catch(() => []), + window.api.checkAgentUpdates().catch(() => []), + window.api.getAgentChangelog(entry.name).catch(() => ({ versions: [], homepage: undefined, error: undefined } as { versions: Array<{ version: string; date?: string }>; homepage?: string; error?: string })), + entry.installed ? window.api.healthCheck(entry.name).catch(() => null) : Promise.resolve(null), + ]) + if (cancelled) return + setEnvFields(fields || []) + setEnvValues({ ...(typeSaved || {}) }) + setInstalled(list.find((i) => i.name === entry.name) || null) + setUpdate(updates.find((u) => u.name === entry.name) || null) + setHealth(healthInfo) + setChangelog({ versions: change.versions || [], homepage: change.homepage, error: change.error, loading: false }) + } catch { + if (!cancelled) setChangelog((s) => ({ ...s, loading: false })) + } + })() + return () => { cancelled = true } + }, [entry.name, entry.installed, jobPhase]) + + // Reset scroll on entry change so deep dives don't inherit a previous scroll. + useEffect(() => { + document.querySelector("main")?.scrollTo({ top: 0 }) + }, [entry.name]) + + const isInstalled = entry.installed + const isManaged = entry.managed !== false + const isInstalling = !!job && job.phase !== "done" && job.phase !== "error" + + async function saveEnv(): Promise { + setSavingEnv(true) + try { + await window.api.saveAgentEnv(entry.name, envValues) + showToast("Configuration saved", "success") + } catch (e: unknown) { + showToast(`Error: ${(e as Error).message}`, "error") + } finally { + setSavingEnv(false) + } + } + + async function testConnection(): Promise { + setTesting(true) + setTestResult(null) + try { + const r = await window.api.testLLM(envValues) + if (r.success) setTestResult({ ok: true, message: `OK — ${r.model || ""} responded` }) + else setTestResult({ ok: false, message: r.error || "Test failed" }) + } catch (e: unknown) { + setTestResult({ ok: false, message: (e as Error).message }) + } finally { + setTesting(false) + } + } + + async function startInstall(): Promise { + const verb = isInstalled ? "update" : "install" + useInstallStore.getState().startJob({ agent: entry.name, verb }) + try { + await window.api.installAgentTypeStreaming(entry.name) + showToast(`${entry.label || entry.name} ${verb === "update" ? "updated" : "installed"}`, "success") + onAfterInstall(entry) + } catch (e: unknown) { + showToast(`${verb} failed: ${(e as Error).message}`, "error") + } + } + + async function startUninstall(): Promise { + setConfirmingUninstall(false) + useInstallStore.getState().startJob({ agent: entry.name, verb: "uninstall" }) + try { + await window.api.uninstallAgentTypeStreaming(entry.name) + showToast(`${entry.label || entry.name} uninstalled`, "success") + onAfterInstall(entry) + } catch (e: unknown) { + showToast(`Uninstall failed: ${(e as Error).message}`, "error") + } + } + + async function startRollback(): Promise { + if (!installed?.history?.length && !installed?.previousVersion) { + showToast("No previous version recorded", "warning") + return + } + useInstallStore.getState().startJob({ agent: entry.name, verb: "rollback" }) + try { + const r = await window.api.rollbackAgentType(entry.name) + if (r.success) { + showToast(`Rolled back to v${r.version}`, "success") + onAfterInstall(entry) + } else { + showToast(r.error || "Rollback failed", "error") + } + } catch (e: unknown) { + showToast(`Rollback failed: ${(e as Error).message}`, "error") + } + } + + const reqs = (entry.install?.requires || []).filter((x): x is string => !!x) + const homepage = entry.homepage || changelog.homepage + const screenshots = (entry.screenshots || []).filter(Boolean) + const demoUrl = entry.demo_url || entry.demo + // Current version: tracked record → live `binary --version` from healthCheck. + // Latest version: tracked update info → first changelog entry from npm. + const currentVersion = installed?.version || health?.version || null + const latestVersion = update?.latest || changelog.versions[0]?.version || null + const installedAtLabel = installed?.installedAt + ? new Date(installed.installedAt).toLocaleString() + : (entry.installed && !installed ? "External install" : null) + const hasUpdate = !!(currentVersion && latestVersion && currentVersion !== latestVersion) + const ua = typeof navigator !== "undefined" ? navigator.userAgent.toLowerCase() : "" + const platformKey: "macos" | "linux" | "windows" = + ua.includes("win") ? "windows" : ua.includes("mac") ? "macos" : "linux" + const platformInstallCmd = entry.install?.[platformKey] + const platforms = [ + entry.install?.macos && "macOS", + entry.install?.linux && "Linux", + entry.install?.windows && "Windows", + ].filter(Boolean) as string[] + + return ( +
+
+ +
+ +
+ +
+

+ {entry.label || entry.name}{" "} + {entry.featured && } +

+

{entry.description || "No description available"}

+
+ {isInstalled ? ( + isManaged + ? Installed + : Global + ) : ( + Not installed + )} + {hasUpdate && Update v{latestVersion} available} + {installed?.version && v{installed.version}} + {homepage && ( + { e.preventDefault(); window.api.openExternal(homepage) }} + > + {homepage.replace(/^https?:\/\//, "")} ↗ + + )} +
+
+
+ {!isInstalled && ( + + )} + {isInstalled && isManaged && ( + <> + + {onOpenWizard && ( + + )} + {(installed?.history?.length || installed?.previousVersion) && ( + + )} + + + )} +
+
+ + {job && job.verb !== "uninstall" && job.verb !== "rollback" && isInstalling && ( +
+

{job.verb === "update" ? "Update progress" : "Install progress"}

+ +
+ {job.detail || job.phase} + +
+ {showLog && ( +
{job.log}
+ )} +
+ )} + +
+

Overview

+ {entry.long_description ? ( +

+ {entry.long_description} +

+ ) : ( +

+ {entry.description || "No description available for this agent yet."} +

+ )} + {screenshots.length > 0 ? ( + + ) : ( +

+ No screenshots provided. + {homepage && <> See { e.preventDefault(); window.api.openExternal(homepage) }}>the project homepage for visuals.} +

+ )} +
+ {demoUrl ? ( + + ) : homepage ? ( + + ) : null} +
+
+ +
+
+

System requirements

+
+
Platforms
+
{platforms.length > 0 ? platforms.join(", ") : "Any"}
+
Mode
+
{entry.install?.api_only ? "Direct API (no binary)" : "Binary install"}
+ {entry.install?.binary && ( + <> +
Binary
+
{entry.install.binary}
+ + )} + {platformInstallCmd && ( + <> +
Install command
+
{platformInstallCmd}
+ + )} + {entry.check_ready?.login_command && ( + <> +
Login command
+
{entry.check_ready.login_command}
+ + )} +
+
+ +
+

Version

+
+
Current
+
{currentVersion ? `v${currentVersion}` : entry.installed ? "Installed (version unknown)" : "Not installed"}
+
Latest
+
{latestVersion ? `v${latestVersion}` : changelog.loading ? "Checking…" : "Unavailable"}
+ {installed?.previousVersion && ( + <> +
Previous
+
v{installed.previousVersion}
+ + )} + {installedAtLabel && ( + <> +
Installed
+
{installedAtLabel}
+ + )} + {health?.binary && ( + <> +
Location
+
{health.binary}
+ + )} +
+
+
+ +
+

Dependencies

+ {reqs.length === 0 ? ( +

This agent has no external dependencies.

+ ) : ( +
    + {reqs.map((dep) => ( +
  • + {dep} + + {dep === "nodejs" ? "Node.js runtime" : + dep === "git" ? "Git version control" : + dep === "python" ? "Python interpreter" : + "Required by this agent"} + +
  • + ))} +
+ )} +
+ +
+

Configuration

+ {envFields.length === 0 ? ( +

+ This agent does not expose environment variables. + {entry.check_ready?.login_command && ( + <> Authenticate via {entry.check_ready.login_command} instead. + )} +

+ ) : ( + <> +

Environment variables saved to ~/.openagents/env/.

+ {envFields.map((f) => { + const FieldInput = f.password ? PasswordInput : Input + return ( +
+ + setEnvValues({ ...envValues, [f.name]: e.target.value })} + placeholder={f.placeholder || `Enter ${f.name}…`} + /> +
+ ) + })} + {testResult && ( +

+ {testResult.message} +

+ )} +
+ + +
+ + )} +
+ +
+

Getting started

+
    +
  1. Install {entry.label || entry.name} from this page.
  2. + {envFields.length > 0 &&
  3. Configure required environment variables (API keys, model name).
  4. } + {entry.check_ready?.login_command && ( +
  5. + Run {entry.check_ready.login_command} to authenticate the CLI. +
  6. + )} +
  7. Go to the Agents tab and create a new agent instance of this type.
  8. +
  9. Start the agent, then open its workspace.
  10. +
+
+ +
+

Changelog

+ {changelog.loading ? ( + Loading… + ) : changelog.error ? ( +

Changelog unavailable: {changelog.error}

+ ) : changelog.versions.length === 0 ? ( +

No changelog data.

+ ) : ( +
    + {changelog.versions.map((v) => ( +
  • + v{v.version} + {v.date ? new Date(v.date).toLocaleDateString() : ""} +
  • + ))} +
+ )} +
+ + setConfirmingUninstall(false)}> +
+ + + Uninstall {entry.label || entry.name}? + +

+ This will remove {entry.label || entry.name} from your system. Configured agents of this type may stop working. +

+
+ + +
+
+
+
+ ) +} diff --git a/packages/launcher/src/renderer/pages/agents/index.tsx b/packages/launcher/src/renderer/pages/agents/index.tsx new file mode 100644 index 000000000..23a56956b --- /dev/null +++ b/packages/launcher/src/renderer/pages/agents/index.tsx @@ -0,0 +1,920 @@ +import React, { useEffect, useRef, useCallback, useState } from "react" +import { useAgentsStore } from "../../store/agents" +import { useUiStore } from "../../store/ui" +import { useShallow } from "zustand/react/shallow" +import AgentIcon from "../../components/AgentIcon" +import StatusDot, { displayState } from "../../components/ui/StatusDot" +import { Button } from "../../components/ui/Button" +import { Modal, ModalTitle } from "../../components/ui/Modal" +import { PasswordInput } from "../../components/ui/PasswordInput" +import type { Agent, CatalogEntry, EnvField, HealthCheck } from "../../types" +import type { ToastType } from "../../hooks/useToast" + +function formatHealthLabel(health: HealthCheck | null): string { + if (!health) return "Not configured" + if (!health.ready) return health.message || "Not configured" + const parts = ["Ready"] + if (health.auth_mode === "api_key") parts.push("API key") + else if (health.auth_mode === "cli_login") parts.push("CLI login") + if (health.execution_mode && health.execution_mode !== "unavailable") + parts.push(health.execution_mode) + return parts.join(" · ") +} + +interface AgentsProps { + showToast: (msg: string, type?: ToastType) => void +} + +const LIST_ITEM = "flex flex-col gap-3 px-[18px] py-4 mb-2.5 bg-(--bg-card) border border-(--border) rounded-(--radius) shadow-sm transition-all duration-200 hover:shadow-md hover:border-(--border-hover)" + +function SkeletonListItem(): React.JSX.Element { + return ( +
+
+
+
+ ) +} + +export default function Agents({ showToast }: AgentsProps): React.JSX.Element { + const { agents, setAgents, pendingAgentActions, addPendingAction, removePendingAction } = + useAgentsStore(useShallow((s) => ({ + agents: s.agents, setAgents: s.setAgents, + pendingAgentActions: s.pendingAgentActions, + addPendingAction: s.addPendingAction, removePendingAction: s.removePendingAction, + }))) + const [loading, setLoading] = useState(agents.length === 0) + const inFlight = useRef(false) + const queued = useRef(false) + const mounted = useRef(true) + + const [newAgentOpen, setNewAgentOpen] = useState(false) + const [configureOpen, setConfigureOpen] = useState(false) + const [configureAgent, setConfigureAgent] = useState<{ + name: string + type: string + } | null>(null) + const [connectWsOpen, setConnectWsOpen] = useState(false) + const [connectWsAgent, setConnectWsAgent] = useState("") + const [removeTarget, setRemoveTarget] = useState(null) + + useEffect(() => { + mounted.current = true + return () => { + mounted.current = false + } + }, []) + + const refresh = useCallback(async () => { + if (inFlight.current) { + queued.current = true + return + } + inFlight.current = true + try { + const data = await window.api.listAgents() + if (!mounted.current) return + setAgents(data) + setLoading(false) + } catch { + } finally { + inFlight.current = false + if (queued.current) { + queued.current = false + refresh() + } + } + }, [setAgents]) + + useEffect(() => { + refresh() + const interval = setInterval(refresh, 5000) + return () => clearInterval(interval) + }, [refresh]) + + const toggleAgent = async (agent: Agent): Promise => { + if (pendingAgentActions.has(agent.name)) return + addPendingAction(agent.name) + refresh() + try { + const isRunning = ["online", "running", "idle"].includes(agent.state) + if (isRunning) { + await window.api.stopAgent(agent.name) + showToast(`Stopping ${agent.name}...`, "info") + for (let i = 0; i < 5; i++) { + await new Promise((r) => setTimeout(r, 3000)) + const status = await window.api.agentStatus() + if (!status[agent.name] || status[agent.name].state === "stopped") { + showToast(`${agent.name} stopped`, "success") + break + } + refresh() + } + } else { + await window.api.startAgent(agent.name) + showToast(`Starting ${agent.name}...`, "info") + for (let i = 0; i < 10; i++) { + await new Promise((r) => setTimeout(r, 3000)) + const status = await window.api.agentStatus() + const s = status[agent.name] + if (s && ["running", "online"].includes(s.state)) { + showToast(`${agent.name} is now running`, "success") + break + } + refresh() + } + } + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } finally { + removePendingAction(agent.name) + refresh() + } + } + + const removeAgent = async (name: string): Promise => { + setRemoveTarget(null) + try { + await window.api.removeAgent(name) + showToast(`Agent '${name}' removed`, "success") + refresh() + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + const disconnectAgent = async (name: string): Promise => { + try { + await window.api.disconnectWorkspace(name) + showToast(`Disconnected ${name} from workspace`, "success") + window.api.signalReload() + refresh() + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + const openWorkspace = async (agent: Agent): Promise => { + try { + const workspaces = await window.api.listWorkspaces() + const ws = workspaces.find( + (w) => w.slug === agent.network || w.id === agent.network, + ) + const slug = (ws && ws.slug) || agent.network + let url = `https://workspace.openagents.org/${slug}` + if (ws && ws.token) url += `?token=${encodeURIComponent(ws.token)}` + window.api.openExternal(url) + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + return ( +
+

My Agents

+
+ +
+ + {loading ? ( +
+ + + +
+ ) : agents.length === 0 ? ( +

+ No agents configured. Click "+ New Agent" to get started. +

+ ) : ( +
+ {agents.map((agent) => { + const isRunning = ["online", "running", "idle"].includes( + agent.state, + ) + const isPending = pendingAgentActions.has(agent.name) + const health = agent.health || null + const readyLabel = formatHealthLabel(health) + const wsDisplay = agent.network + ? agent.networkName && agent.networkName !== agent.network + ? `${agent.network} (${agent.networkName})` + : agent.network + : "" + const envDisplay: string[] = [] + if (agent.env?.LLM_BASE_URL || agent.env?.OPENAI_BASE_URL) + envDisplay.push( + `API: ${agent.env.LLM_BASE_URL || agent.env.OPENAI_BASE_URL}`, + ) + if (agent.env?.LLM_MODEL || agent.env?.OPENCLAW_MODEL) + envDisplay.push( + `Model: ${agent.env.LLM_MODEL || agent.env.OPENCLAW_MODEL}`, + ) + + return ( +
+
+
+
+ +

{agent.name}

+
+ {agent.type} + + {agent.runtimeMismatch ? ( + + Launcher core update required + + ) : health?.ready ? ( + <>🔑 {readyLabel} + ) : ( + + ⚠ {readyLabel} + + )} + {envDisplay.length > 0 && + " · " + envDisplay.join(" · ")} + + {agent.lastError && ( + {agent.lastError} + )} +
+
+
+ + + {displayState(agent.state)} + +
+ {wsDisplay ? ( + {wsDisplay} + ) : ( + + Not connected + + )} +
+
+
+
+ + + {agent.network ? ( + <> + + + + ) : ( + + )} +
+ +
+
+ ) + })} +
+ )} + + setNewAgentOpen(false)} + showToast={showToast} + onCreated={(name, type) => { + setNewAgentOpen(false) + refresh() + setConfigureAgent({ name, type }) + setConfigureOpen(true) + }} + /> + + {configureAgent && ( + setConfigureOpen(false)} + showToast={showToast} + onSaved={refresh} + /> + )} + + setConnectWsOpen(false)} + showToast={showToast} + onConnected={refresh} + /> + + setRemoveTarget(null)}> +
+ a.name === removeTarget)?.type || ""} size={40} /> + + Remove {removeTarget}? + +

+ This will stop and remove {removeTarget}. +

+
+ + +
+
+
+
+ ) +} + +function NewAgentDialog({ + open, + onClose, + showToast, + onCreated, +}: { + open: boolean + onClose: () => void + showToast: (msg: string, type?: ToastType) => void + onCreated: (name: string, type: string) => void +}): React.JSX.Element { + const [catalog, setCatalog] = useState([]) + const [supportedTypes, setSupportedTypes] = useState([]) + const [selectedType, setSelectedType] = useState("") + const [agentName, setAgentName] = useState("") + const [agentPath, setAgentPath] = useState("") + const [loading, setLoading] = useState(false) + const setCurrentTab = useUiStore.getState().setCurrentTab + + useEffect(() => { + if (!open) return + setLoading(true) + Promise.all([window.api.getCatalog(), window.api.getSupportedAgentTypes()]) + .then(([cat, types]) => { + setCatalog(cat) + setSupportedTypes(types || []) + const supportedSet = new Set(types || []) + const installed = cat.filter( + (c) => c.installed && supportedSet.has(c.name), + ) + if (installed.length > 0) setSelectedType(installed[0].name) + setLoading(false) + }) + .catch(() => setLoading(false)) + }, [open]) + + useEffect(() => { + if (selectedType) { + const suffix = Math.random().toString(36).slice(2, 6) + setAgentName(`${selectedType}-${suffix}`) + } + }, [selectedType]) + + const supportedSet = new Set(supportedTypes) + const supportedInstalled = catalog.filter( + (c) => c.installed && supportedSet.has(c.name), + ) + + const doCreate = async (): Promise => { + const name = + agentName.trim() || + `${selectedType}-${Math.random().toString(36).slice(2, 6)}` + if (!/^[a-zA-Z0-9_-]+$/.test(name)) { + showToast( + "Agent name can only contain letters, numbers, hyphens, and underscores", + "warning", + ) + return + } + try { + await window.api.addAgent({ + name, + type: selectedType, + path: agentPath.trim() || undefined, + }) + showToast(`Agent '${name}' created`, "success") + onCreated(name, selectedType) + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + return ( + + New Agent + {loading ? ( +

Loading installed types...

+ ) : supportedInstalled.length === 0 ? ( + <> +

+ No Launcher-supported agent runtimes installed. Install one first. +

+
+ + +
+ + ) : ( + <> +
+ + +
+
+ + setAgentName(e.target.value)} + placeholder={`${selectedType}-xxxx`} + /> +
+
+ + setAgentPath(e.target.value)} + placeholder="/path/to/project" + /> +
+
+ + +
+ + )} +
+ ) +} + +function ConfigureDialog({ + open, + agentName, + agentType, + onClose, + showToast, + onSaved, +}: { + open: boolean + agentName: string + agentType: string + onClose: () => void + showToast: (msg: string, type?: ToastType) => void + onSaved: () => void +}): React.JSX.Element { + const [fields, setFields] = useState([]) + const [values, setValues] = useState>({}) + const [loginCmd, setLoginCmd] = useState(null) + const [loggedIn, setLoggedIn] = useState(false) + const [noConfig, setNoConfig] = useState(false) + const [loading, setLoading] = useState(true) + const [testResult, setTestResult] = useState(null) + const [testStatus, setTestStatus] = useState< + "idle" | "loading" | "ok" | "error" + >("idle") + + useEffect(() => { + if (!open) return + setLoading(true) + setTestResult(null) + setTestStatus("idle") + setNoConfig(false) + setLoginCmd(null) + Promise.all([ + window.api.getEnvFields(agentType), + window.api.getAgentEnv(agentType), + agentName + ? window.api.getAgentInstanceEnv(agentName) + : Promise.resolve({} as Record), + ]) + .then(([f, typeEnv, instanceEnv]) => { + if (f && f.length > 0) { + setFields(f) + const merged = { ...(typeEnv || {}), ...(instanceEnv || {}) } + const initial: Record = {} + f.forEach((field) => { + initial[field.name] = merged[field.name] || field.default || "" + }) + setValues(initial) + } else { + window.api.getCatalog().then((catalog) => { + const entry = catalog.find((c) => c.name === agentType) + const cmd = entry?.check_ready?.login_command || null + if (cmd) { + setLoginCmd(cmd) + window.api + .healthCheck(agentType) + .then((h) => setLoggedIn(h?.ready || false)) + .catch(() => {}) + } else { + setNoConfig(true) + } + }) + } + setLoading(false) + }) + .catch(() => setLoading(false)) + }, [open, agentName, agentType]) + + const save = async (): Promise => { + try { + if (agentName) { + await window.api.saveAgentInstanceEnv(agentName, values) + } else { + await window.api.saveAgentEnv(agentType, values) + } + showToast("Configuration saved", "success") + onSaved() + onClose() + } catch (err: unknown) { + showToast(`Error saving: ${(err as Error).message}`, "error") + } + } + + const testConnection = async (): Promise => { + setTestStatus("loading") + setTestResult(null) + try { + const result = await window.api.testLLM(values) + if (result.success) { + setTestStatus("ok") + setTestResult( + `OK — model: ${result.model}, response: "${result.response}"`, + ) + } else { + setTestStatus("error") + setTestResult(result.error || "Unknown error") + } + } catch (err: unknown) { + setTestStatus("error") + setTestResult((err as Error).message) + } + } + + return ( + + Configure {agentName || agentType} + {loading ? ( +

Loading configuration...

+ ) : noConfig ? ( + <> +

No configuration required for this agent type.

+ + + ) : loginCmd ? ( + <> +

This agent uses login-based authentication.

+
+ {loggedIn ? "✅" : "⚠️"} + + {loggedIn ? "Logged in" : "Not logged in"} + +
+
+ + +
+ + ) : ( + <> +

+ {agentName + ? "Settings saved for this agent. Type defaults remain available as fallbacks." + : "Settings saved to ~/.openagents/env/"} +

+
+ {fields.map((f) => ( +
+ + {f.password ? ( + + setValues((prev) => ({ + ...prev, + [f.name]: e.target.value, + })) + } + placeholder={f.placeholder || `Enter ${f.name}...`} + /> + ) : ( + + setValues((prev) => ({ + ...prev, + [f.name]: e.target.value, + })) + } + placeholder={f.placeholder || `Enter ${f.name}...`} + /> + )} +
+ ))} +
+ {testResult && ( +
+ {testResult} +
+ )} +
+ + + +
+ + )} +
+ ) +} + +function ConnectWorkspaceDialog({ + open, + agentName, + onClose, + showToast, + onConnected, +}: { + open: boolean + agentName: string + onClose: () => void + showToast: (msg: string, type?: ToastType) => void + onConnected: () => void +}): React.JSX.Element { + const [workspaces, setWorkspaces] = useState< + Array<{ + id: string + slug: string + name?: string + endpoint?: string + token?: string + }> + >([]) + const [view, setView] = useState<"list" | "create" | "token">("list") + const [newWsName, setNewWsName] = useState("") + const [token, setToken] = useState("") + + useEffect(() => { + if (!open) return + setView("list") + setNewWsName("") + setToken("") + window.api + .listWorkspaces() + .then(setWorkspaces) + .catch(() => {}) + }, [open]) + + const doConnect = async (slug: string): Promise => { + try { + showToast(`Connecting ${agentName} to workspace...`, "info") + await window.api.connectWorkspace(agentName, slug) + window.api.signalReload() + showToast(`Connected to ${slug}`, "success") + onConnected() + onClose() + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + const doCreate = async (): Promise => { + const name = newWsName.trim() + if (!name) { + showToast("Workspace name is required", "warning") + return + } + try { + showToast(`Creating workspace '${name}'...`, "info") + const result = await window.api.createWorkspace(name) + showToast(`Workspace '${name}' created`, "success") + if (result && result.token && agentName) { + await window.api.connectWorkspace(agentName, result.token) + window.api.signalReload() + showToast(`Connected ${agentName} to ${name}`, "success") + } + onConnected() + onClose() + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + const doJoinToken = async (): Promise => { + const t = token.trim() + if (!t) { + showToast("Token is required", "warning") + return + } + try { + showToast("Joining workspace...", "info") + await window.api.connectWorkspace(agentName, t) + window.api.signalReload() + showToast("Joined workspace", "success") + onConnected() + onClose() + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + return ( + + Connect '{agentName}' to Workspace + {view === "list" && ( + <> +
+ {workspaces.map((ws) => { + const display = ws.name || ws.slug || ws.id + const url = + ws.endpoint && + (ws.endpoint.includes("localhost") || + ws.endpoint.includes("127.0.0.1")) + ? `${ws.endpoint}/${ws.slug || ws.id}` + : `workspace.openagents.org/${ws.slug || ws.id}` + return ( + + ) + })} + + +
+ + + )} + {view === "create" && ( + <> +
+ + setNewWsName(e.target.value)} + placeholder="my-workspace" + autoFocus + /> +
+
+ + +
+ + )} + {view === "token" && ( + <> +
+ + setToken(e.target.value)} + placeholder="Paste token here..." + autoFocus + /> +
+
+ + +
+ + )} +
+ ) +} diff --git a/packages/launcher/src/renderer/pages/dashboard/index.tsx b/packages/launcher/src/renderer/pages/dashboard/index.tsx new file mode 100644 index 000000000..3bd0b10dd --- /dev/null +++ b/packages/launcher/src/renderer/pages/dashboard/index.tsx @@ -0,0 +1,322 @@ +import React, { useEffect, useRef, useCallback, useState } from "react" +import { useAgentsStore } from "../../store/agents" +import { useUiStore } from "../../store/ui" +import { useInstallStore } from "../../store/install" +import { useShallow } from "zustand/react/shallow" +import AgentIcon from "../../components/AgentIcon" +import StatusDot, { displayState } from "../../components/ui/StatusDot" +import { Button } from "../../components/ui/Button" +import type { Agent, HealthCheck, AgentUpdateInfo } from "../../types" +import type { ToastType } from "../../hooks/useToast" + +interface DashboardProps { + showToast: (message: string, type?: ToastType) => void + onOpenConfigure: (agentName: string, agentType: string) => void + onOpenConnectWorkspace: (agentName: string) => void +} + +function formatHealthLabel(health: HealthCheck | null): string { + if (!health) return "Not configured" + if (!health.ready) return health.message || "Not configured" + const parts = ["Ready"] + if (health.auth_mode === "api_key") parts.push("API key") + else if (health.auth_mode === "cli_login") parts.push("CLI login") + if (health.execution_mode && health.execution_mode !== "unavailable") + parts.push(health.execution_mode) + return parts.join(" · ") +} + +const CARD_BASE = "flex flex-col h-full bg-(--bg-card) border border-(--border) rounded-(--radius) px-[18px] py-4 shadow-sm transition-all duration-200 hover:shadow-md hover:border-(--border-hover)" + +function AgentCard({ + agent, + isPending, + onToggle, + onOpenWorkspace, +}: { + agent: Agent + isPending: boolean + onToggle: () => void + onOpenWorkspace: () => void +}): React.JSX.Element { + const isRunning = ["online", "running", "idle"].includes(agent.state) + const health = agent.health || null + const isConnected = !!agent.network + const isUnsupported = !!agent.runtimeMismatch + const wsLabel = agent.network + ? agent.networkName && agent.networkName !== agent.network + ? `${agent.network} (${agent.networkName})` + : agent.network + : "" + const configLabel = formatHealthLabel(health) + + return ( +
+
+ + {agent.name} + {agent.type} +
+
+ + {displayState(agent.state)} +
+
+ {isUnsupported ? ( + Launcher core update required + ) : health?.ready ? ( + {configLabel} + ) : ( + {configLabel} + )} + {isConnected ? ( + Connected: {wsLabel} + ) : ( + Not connected + )} +
+ {agent.lastError && ( +
{agent.lastError}
+ )} +
+ {isRunning ? ( + <> + + {isConnected && ( + + )} + + ) : ( + + )} +
+
+ ) +} + +function SkeletonCard(): React.JSX.Element { + return ( +
+
+
+
+
+ ) +} + +export default function Dashboard({ + showToast, +}: DashboardProps): React.JSX.Element { + const { agents, setAgents, pendingAgentActions, addPendingAction, removePendingAction, setCoreVersion, setLauncherVersion } = + useAgentsStore(useShallow((s) => ({ + agents: s.agents, setAgents: s.setAgents, + pendingAgentActions: s.pendingAgentActions, + addPendingAction: s.addPendingAction, removePendingAction: s.removePendingAction, + setCoreVersion: s.setCoreVersion, setLauncherVersion: s.setLauncherVersion, + }))) + const { activityLog, setCurrentTab, setInstallFocusAgent } = useUiStore(useShallow((s) => ({ activityLog: s.activityLog, setCurrentTab: s.setCurrentTab, setInstallFocusAgent: s.setInstallFocusAgent }))) + const { updates, setUpdates } = useInstallStore(useShallow((s) => ({ updates: s.updates, setUpdates: s.setUpdates }))) + + const inFlight = useRef(false) + const queued = useRef(false) + const mounted = useRef(true) + const [loading, setLoading] = useState(agents.length === 0) + const pendingUpdates = updates.filter((u: AgentUpdateInfo) => u.current && u.latest && u.current !== u.latest) + + useEffect(() => { + mounted.current = true + return () => { + mounted.current = false + } + }, []) + + const refresh = useCallback(async () => { + if (inFlight.current) { + queued.current = true + return + } + inFlight.current = true + try { + const data = await window.api.listAgents() + if (!mounted.current) return + setAgents(data) + setLoading(false) + + const status = await window.api.pythonStatus() + if (!mounted.current) return + setCoreVersion(status.sdkVersion) + setLauncherVersion(`v${status.launcherVersion}`) + } catch (err) { + console.error("Dashboard refresh error:", err) + } finally { + inFlight.current = false + if (queued.current) { + queued.current = false + refresh() + } + } + }, [setAgents, setCoreVersion, setLauncherVersion]) + + useEffect(() => { + refresh() + const interval = setInterval(refresh, 5000) + return () => clearInterval(interval) + }, [refresh]) + + useEffect(() => { + let cancelled = false + const load = async (): Promise => { + try { + const u = await window.api.checkAgentUpdates() + if (!cancelled) setUpdates(u) + } catch {} + } + load() + const id = setInterval(load, 60 * 60 * 1000) + return () => { cancelled = true; clearInterval(id) } + }, [setUpdates]) + + const toggleAgent = async (agent: Agent): Promise => { + if (pendingAgentActions.has(agent.name)) return + addPendingAction(agent.name) + refresh() + try { + const isRunning = ["online", "running", "idle"].includes(agent.state) + if (isRunning) { + await window.api.stopAgent(agent.name) + showToast(`Stopping ${agent.name}...`, "info") + for (let i = 0; i < 5; i++) { + await new Promise((r) => setTimeout(r, 3000)) + const status = await window.api.agentStatus() + const a = status[agent.name] + if (!a || a.state === "stopped") { + showToast(`${agent.name} stopped`, "success") + break + } + refresh() + } + } else { + await window.api.startAgent(agent.name) + showToast(`Starting ${agent.name}...`, "info") + for (let i = 0; i < 10; i++) { + await new Promise((r) => setTimeout(r, 3000)) + const status = await window.api.agentStatus() + const a = status[agent.name] + if (a && ["running", "online"].includes(a.state)) { + showToast(`${agent.name} is now running`, "success") + break + } + refresh() + } + } + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } finally { + removePendingAction(agent.name) + refresh() + } + } + + const openWorkspaceInBrowser = async (agent: Agent): Promise => { + try { + const workspaces = await window.api.listWorkspaces() + const ws = workspaces.find( + (w) => w.slug === agent.network || w.id === agent.network, + ) + const slug = (ws && ws.slug) || agent.network + let url = `https://workspace.openagents.org/${slug}` + if (ws && ws.token) url += `?token=${encodeURIComponent(ws.token)}` + window.api.openExternal(url) + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + return ( +
+

Dashboard

+ + {pendingUpdates.length > 0 && ( +
+ +
+
+ {pendingUpdates.length === 1 + ? `Update available for ${pendingUpdates[0].name}` + : `${pendingUpdates.length} agent updates available`} +
+
+ {pendingUpdates.slice(0, 3).map((u) => `${u.name} v${u.current} → v${u.latest}`).join(" · ")} +
+
+ +
+ )} + + {loading ? ( +
+ + +
+ ) : agents.length === 0 ? ( +
+
+

No agents configured yet.

+ +
+
+ ) : ( +
+ {agents.map((agent) => ( + toggleAgent(agent)} + onOpenWorkspace={() => openWorkspaceInBrowser(agent)} + /> + ))} +
+ )} + + {/* Activity log */} +
+

Activity

+
+ {activityLog.length === 0 ? ( + No activity yet. Start an agent to see events. + ) : ( + activityLog.map((entry, i) => ( +
+ {entry.time} + {entry.msg} +
+ )) + )} +
+
+
+ ) +} diff --git a/packages/launcher/src/renderer/pages/install/index.tsx b/packages/launcher/src/renderer/pages/install/index.tsx new file mode 100644 index 000000000..814557047 --- /dev/null +++ b/packages/launcher/src/renderer/pages/install/index.tsx @@ -0,0 +1,488 @@ +import React, { useEffect, useMemo, useState, useCallback } from "react" +import { Button } from "../../components/ui/Button" +import { Input } from "../../components/ui/Input" +import { Badge } from "../../components/ui/Badge" +import { Modal, ModalTitle } from "../../components/ui/Modal" +import AgentIcon from "../../components/AgentIcon" +import AgentDetail from "../agents/AgentDetail" +import SetupWizard from "../../components/SetupWizard" +import { cn } from "../../lib/utils" +import { useInstallStore, hasPendingUpdate, type InstallJob } from "../../store/install" +import { useUiStore } from "../../store/ui" +import type { CatalogEntry, AgentUpdateInfo, InstalledAgentRecord } from "../../types" +import type { ToastType } from "../../hooks/useToast" + +interface InstallProps { + showToast: (msg: string, type?: ToastType) => void +} + +interface ActionBarProps { + entry: CatalogEntry + job: InstallJob | undefined + hasUpdate: boolean + size?: "sm" | "default" + className?: string + onInstall: (entry: CatalogEntry, verb: "install" | "update") => void + onUninstall: (entry: CatalogEntry) => void +} + +function AgentActions({ + entry, + job, + hasUpdate, + size = "sm", + className, + onInstall, + onUninstall, +}: ActionBarProps): React.JSX.Element | null { + const stop = (e: React.MouseEvent): void => e.stopPropagation() + const isInstalled = entry.installed + const isManaged = entry.managed !== false + const busy = !!job && job.phase !== "done" && job.phase !== "error" + const wrapperClass = cn("shrink-0 flex gap-1.5", className) + + if (busy && job) { + const verb = job.verb === "uninstall" ? "Uninstalling…" : job.verb === "rollback" ? "Rolling back…" : job.verb === "update" ? "Updating…" : "Installing…" + return ( +
+ +
+ ) + } + + // Globally installed (not managed by launcher) — no actions available. + if (isInstalled && !isManaged) return null + + return ( +
+ {!isInstalled ? ( + + ) : ( + <> + {hasUpdate && ( + + )} + + + )} +
+ ) +} + +function SupportIcons({ support }: { support?: CatalogEntry["support"] }): React.JSX.Element { + const items: Array<{ key: string; icon: string; title: string; on: boolean }> = [ + { key: "install", icon: "⬇", title: "Install supported", on: !!support?.install }, + { key: "workspace", icon: "🌐", title: "Workspace supported", on: !!support?.workspace }, + { key: "collaboration", icon: "🤝", title: "Collaboration supported", on: !!support?.collaboration }, + ] + return ( + + {items.map((it) => ( + {it.icon} + ))} + + ) +} + +type SortKey = "featured" | "newest" | "popular" +type ViewMode = "grid" | "list" + +const CATEGORIES: Array<{ key: string; label: string; match: (e: CatalogEntry) => boolean }> = [ + { key: "all", label: "All", match: () => true }, + { key: "coding", label: "Coding", match: (e) => (e.tags || []).includes("coding") }, + { key: "open-source", label: "Open source", match: (e) => (e.tags || []).includes("open-source") }, + { key: "cli", label: "CLI", match: (e) => (e.tags || []).includes("cli") }, + { key: "ide-extension", label: "IDE extension", match: (e) => (e.tags || []).some((t) => t === "vscode" || t === "editor" || t === "ide-extension") }, +] + +const CATALOG_CARD = "flex flex-col gap-2.5 min-h-[158px] px-[18px] py-4 bg-(--bg-card) border border-(--border) rounded-(--radius) shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:border-(--border-hover) hover:-translate-y-px" + +const CATALOG_ROW = "flex items-center gap-3.5 px-4 py-3 bg-(--bg-card) border border-(--border) rounded-(--radius) shadow-sm transition-all duration-150 hover:shadow-md hover:border-(--border-hover) cursor-pointer" + +const TAG = "text-[10px] px-[7px] py-0.5 rounded-[10px] bg-(--bg-input) text-(--text-secondary)" +const TAG_UPDATE = "text-[10px] px-[7px] py-0.5 rounded-[10px] bg-(--warning-bg) text-(--warning-text)" + +function SkeletonCard(): React.JSX.Element { + return ( +
+
+
+
+
+ ) +} + +export default function Install({ showToast }: InstallProps): React.JSX.Element { + const [catalog, setCatalog] = useState([]) + const [loading, setLoading] = useState(true) + const [search, setSearch] = useState("") + const [category, setCategory] = useState("all") + const [sort, setSort] = useState("featured") + const [view, setView] = useState("grid") + const [selectedName, setSelectedName] = useState(null) + const [wizardEntry, setWizardEntry] = useState(null) + const [uninstallTarget, setUninstallTarget] = useState(null) + const setInstalled = useInstallStore((s) => s.setInstalled) + const setUpdates = useInstallStore((s) => s.setUpdates) + const updates = useInstallStore((s) => s.updates) + const installedList = useInstallStore((s) => s.installed) + const jobs = useInstallStore((s) => s.jobs) + const installFocusAgent = useUiStore((s) => s.installFocusAgent) + const setInstallFocusAgent = useUiStore((s) => s.setInstallFocusAgent) + const installListSignal = useUiStore((s) => s.installListSignal) + + // Deep-link: when triggered via tray menu or Dashboard banner, open the + // detail view for the requested agent and clear the request flag. + useEffect(() => { + if (installFocusAgent) { + setSelectedName(installFocusAgent) + setInstallFocusAgent(null) + } + }, [installFocusAgent, setInstallFocusAgent]) + + // Clicking the Install sidebar tab bumps installListSignal — always return + // to the marketplace list rather than resuming the previous detail view. + useEffect(() => { + if (installListSignal > 0) setSelectedName(null) + }, [installListSignal]) + + const loadAll = useCallback(async () => { + try { + const [cat, inst, upd] = await Promise.all([ + window.api.getCatalog(), + window.api.getInstalledAgents().catch(() => [] as InstalledAgentRecord[]), + window.api.checkAgentUpdates().catch(() => [] as AgentUpdateInfo[]), + ]) + setCatalog(cat) + setInstalled(inst) + setUpdates(upd) + setLoading(false) + } catch { + setLoading(false) + } + }, [setInstalled, setUpdates]) + + const handleInlineInstall = useCallback(async (entry: CatalogEntry, verb: "install" | "update"): Promise => { + // Route into the detail page first so the user sees full context + // (progress card, configuration, log) instead of just a card-level spinner. + setSelectedName(entry.name) + const wasInstalled = installedList.some((r) => r.name === entry.name) + useInstallStore.getState().startJob({ agent: entry.name, verb }) + try { + await window.api.installAgentTypeStreaming(entry.name) + showToast(`${entry.label || entry.name} ${verb === "update" ? "updated" : "installed"}`, "success") + if (!wasInstalled && verb === "install") setWizardEntry(entry) + } catch (e: unknown) { + showToast(`${verb} failed: ${(e as Error).message}`, "error") + } + }, [showToast, installedList]) + + const handleInlineUninstall = useCallback((entry: CatalogEntry): void => { + setUninstallTarget(entry) + }, []) + + const confirmUninstall = useCallback(async (): Promise => { + const entry = uninstallTarget + if (!entry) return + setUninstallTarget(null) + useInstallStore.getState().startJob({ agent: entry.name, verb: "uninstall" }) + try { + await window.api.uninstallAgentTypeStreaming(entry.name) + showToast(`${entry.label || entry.name} uninstalled`, "success") + } catch (e: unknown) { + showToast(`Uninstall failed: ${(e as Error).message}`, "error") + } finally { + await loadAll() + } + }, [uninstallTarget, showToast, loadAll]) + + // Refresh catalog when a job reaches a terminal state. + useEffect(() => { + const finished = Object.values(jobs).filter((j) => j.phase === "done" || j.phase === "error") + if (finished.length > 0) { + loadAll() + } + }, [jobs, loadAll]) + + useEffect(() => { + loadAll() + const id = setInterval(loadAll, 30000) + return () => clearInterval(id) + }, [loadAll]) + + const filteredSorted = useMemo(() => { + const cat = CATEGORIES.find((c) => c.key === category) || CATEGORIES[0] + const lowerSearch = search.toLowerCase() + const filtered = catalog.filter((c) => { + if (!cat.match(c)) return false + if (!search) return true + return `${c.name} ${c.label || ""} ${c.description || ""} ${(c.tags || []).join(" ")}` + .toLowerCase().includes(lowerSearch) + }) + + if (sort === "featured") { + filtered.sort((a, b) => { + const af = a.featured ? 1 : 0 + const bf = b.featured ? 1 : 0 + if (af !== bf) return bf - af + const ao = typeof a.order === "number" ? a.order : 999 + const bo = typeof b.order === "number" ? b.order : 999 + if (ao !== bo) return ao - bo + return (a.label || a.name).localeCompare(b.label || b.name) + }) + } else if (sort === "newest") { + filtered.sort((a, b) => { + const ar = installedList.find((r) => r.name === a.name)?.installedAt || "" + const br = installedList.find((r) => r.name === b.name)?.installedAt || "" + if (ar !== br) return br.localeCompare(ar) + return (a.label || a.name).localeCompare(b.label || b.name) + }) + } else if (sort === "popular") { + filtered.sort((a, b) => { + const ai = a.installed ? 1 : 0 + const bi = b.installed ? 1 : 0 + if (ai !== bi) return bi - ai + return (a.label || a.name).localeCompare(b.label || b.name) + }) + } + return filtered + }, [catalog, category, search, sort, installedList]) + + const selected = selectedName ? catalog.find((c) => c.name === selectedName) : null + if (selected) { + return ( + <> + setSelectedName(null)} + onAfterInstall={(e) => { + loadAll() + if (!installedList.find((r) => r.name === e.name)) { + // Newly installed — open the wizard + setWizardEntry(e) + } + }} + onOpenWizard={(e) => setWizardEntry(e)} + showToast={showToast} + /> + setWizardEntry(null)} showToast={showToast} /> + + ) + } + + return ( +
+
+

Agent Marketplace

+ {loading ? ( + + ) : ( + + {catalog.length} agents · {installedList.length} installed + + )} +
+ +
+
+ setSearch(e.target.value)} + placeholder="Search agents, tags, descriptions…" + /> +
+ +
+ + +
+
+ +
+ {CATEGORIES.map((c) => ( + + ))} +
+ + {loading ? ( +
+ +
+ ) : filteredSorted.length === 0 ? ( +

No agents match the current filters.

+ ) : view === "grid" ? ( +
+ {filteredSorted.map((c) => { + const hasUpdate = hasPendingUpdate(updates, c.name) + const job = jobs[c.name] + return ( +
setSelectedName(c.name)} + role="button" + tabIndex={0} + onKeyDown={(e) => { if (e.key === "Enter") setSelectedName(c.name) }} + > +
+ +
{c.label || c.name}
+ {c.featured && } +
+
{c.description || "No description."}
+
+ {(c.tags || []).slice(0, 3).map((t) => ( + {t} + ))} +
+ +
+
+ {c.installed ? ( + c.managed === false + ? Global + : Installed + ) : ( + Not installed + )} + {hasUpdate && Update} +
+
+ +
+ ) + })} +
+ ) : ( +
+ {filteredSorted.map((c) => { + const hasUpdate = hasPendingUpdate(updates, c.name) + const job = jobs[c.name] + return ( +
setSelectedName(c.name)} + role="button" + tabIndex={0} + > +
+ +
+ + {c.label || c.name}{" "} + {c.featured && } + + {c.description && {c.description}} +
+ {(c.tags || []).slice(0, 4).map((t) => ( + {t} + ))} +
+ +
+
+
+ {c.installed ? ( + c.managed === false + ? Global + : Installed + ) : ( + Not installed + )} + {hasUpdate && Update} +
+ +
+ ) + })} +
+ )} + + setUninstallTarget(null)}> +
+ + + Uninstall {uninstallTarget?.label || uninstallTarget?.name}? + +

+ This will remove {uninstallTarget?.label || uninstallTarget?.name} from your system. Configured agents of this type may stop working. +

+
+ + +
+
+
+
+ ) +} diff --git a/packages/launcher/src/renderer/pages/logs/index.tsx b/packages/launcher/src/renderer/pages/logs/index.tsx new file mode 100644 index 000000000..31c12aa6e --- /dev/null +++ b/packages/launcher/src/renderer/pages/logs/index.tsx @@ -0,0 +1,254 @@ +import React, { useEffect, useRef, useCallback, useState } from "react" +import { Button } from "../../components/ui/Button" +import { Modal, ModalTitle } from "../../components/ui/Modal" +import { useAgentsStore } from "../../store/agents" +import type { ToastType } from "../../hooks/useToast" + +const LOGS_INITIAL_LINES = 200 +const LOGS_MAX_BUFFER = 400 + +interface LogsProps { + showToast: (msg: string, type?: ToastType) => void +} + +function toDateTimeLocalValue(date: Date): string { + const pad = (v: number): string => String(v).padStart(2, "0") + return [ + date.getFullYear(), + "-", + pad(date.getMonth() + 1), + "-", + pad(date.getDate()), + "T", + pad(date.getHours()), + ":", + pad(date.getMinutes()), + ].join("") +} + +export default function Logs({ showToast }: LogsProps): React.JSX.Element { + const agents = useAgentsStore((s) => s.agents) + const [logLines, setLogLines] = useState([]) + const [agentFilter, setAgentFilter] = useState("") + const [autoRefresh, setAutoRefresh] = useState(true) + const [clearOpen, setClearOpen] = useState(false) + const [clearStart, setClearStart] = useState("") + const [clearEnd, setClearEnd] = useState("") + const [clearInFlight, setClearInFlight] = useState(false) + const [clearError, setClearError] = useState("") + + const logsOffset = useRef(0) + const filterRef = useRef("") + const logViewerRef = useRef(null) + const mounted = useRef(true) + + useEffect(() => { + mounted.current = true + return () => { + mounted.current = false + } + }, []) + + const refreshLogs = useCallback(async (reset = false) => { + if (!mounted.current) return + try { + const filter = filterRef.current + const shouldReset = reset || logsOffset.current === 0 + const result = await window.api.tailAgentLogs( + filter, + LOGS_INITIAL_LINES, + shouldReset ? 0 : logsOffset.current, + ) + if (!mounted.current) return + logsOffset.current = result.size || 0 + if (shouldReset) { + setLogLines(result.lines && result.lines.length > 0 ? result.lines : []) + setTimeout(() => { + if (logViewerRef.current) + logViewerRef.current.scrollTop = logViewerRef.current.scrollHeight + }, 0) + } else if (result.lines && result.lines.length > 0) { + setLogLines((prev) => { + const merged = [...prev, ...result.lines].slice(-LOGS_MAX_BUFFER) + setTimeout(() => { + if (logViewerRef.current) + logViewerRef.current.scrollTop = logViewerRef.current.scrollHeight + }, 0) + return merged + }) + } + } catch (err: unknown) { + if (mounted.current) + setLogLines([`Error loading logs: ${(err as Error).message}`]) + } + }, []) + + useEffect(() => { + logsOffset.current = 0 + refreshLogs(true) + }, [refreshLogs]) + + useEffect(() => { + if (!autoRefresh) return + const interval = setInterval(() => refreshLogs(false), 3000) + return () => clearInterval(interval) + }, [autoRefresh, refreshLogs]) + + const handleFilterChange = (value: string): void => { + setAgentFilter(value) + filterRef.current = value + logsOffset.current = 0 + refreshLogs(true) + } + + const copyLogs = (): void => { + navigator.clipboard + .writeText(logLines.join("\n")) + .then(() => showToast("Logs copied to clipboard", "success")) + .catch(() => showToast("Failed to copy logs", "error")) + } + + const openClearModal = (): void => { + const now = new Date() + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000) + setClearStart(toDateTimeLocalValue(oneHourAgo)) + setClearEnd(toDateTimeLocalValue(now)) + setClearError("") + setClearOpen(true) + } + + const doClearLogs = async (): Promise => { + if (clearInFlight) return + const start = clearStart ? new Date(clearStart) : null + const end = clearEnd ? new Date(clearEnd) : null + if (!start || isNaN(start.getTime()) || !end || isNaN(end.getTime())) { + setClearError("Please select a valid start and end time.") + return + } + if (start.getTime() > end.getTime()) { + setClearError("Start time must be before end time.") + return + } + setClearInFlight(true) + setClearError("") + try { + const result = await window.api.clearLogsInRange( + start.toISOString(), + end.toISOString(), + ) + setClearOpen(false) + logsOffset.current = 0 + await refreshLogs(true) + showToast( + `Deleted ${result.removed || 0} log lines from the selected range`, + "success", + ) + } catch (err: unknown) { + setClearError((err as Error).message || "Failed to clear logs.") + } finally { + setClearInFlight(false) + } + } + + return ( +
+

Logs

+ +
+ + + + + +
+ +
+        {logLines.length > 0
+          ? logLines.join("\n")
+          : "No logs available.\n\nLogs appear here after the daemon starts."}
+      
+ + setClearOpen(false)}> + Clear Logs +

+ Delete log entries from daemon.log{" "} + whose timestamps fall inside the selected time range. +

+
+ + setClearStart(e.target.value)} + /> +
+
+ + setClearEnd(e.target.value)} + /> +
+ {clearError && ( +

+ {clearError} +

+ )} +
+ + +
+
+
+ ) +} diff --git a/packages/launcher/src/renderer/pages/settings/index.tsx b/packages/launcher/src/renderer/pages/settings/index.tsx new file mode 100644 index 000000000..cbf1860b1 --- /dev/null +++ b/packages/launcher/src/renderer/pages/settings/index.tsx @@ -0,0 +1,314 @@ +import React, { useEffect, useState, useCallback, useRef } from "react" +import { Switch } from "../../components/ui/Switch" +import { Label } from "../../components/ui/Label" +import { Button } from "../../components/ui/Button" +import { Separator } from "../../components/ui/Separator" +import { Skeleton } from "../../components/ui/Skeleton" +import type { RuntimeInfo, Workspace } from "../../types" +import type { ToastType } from "../../hooks/useToast" + +interface SettingsProps { + showToast: (msg: string, type?: ToastType) => void +} + +export default function Settings({ + showToast, +}: SettingsProps): React.JSX.Element { + const [startOnBoot, setStartOnBoot] = useState(false) + const [minimizeToTray, setMinimizeToTray] = useState(false) + const [runtimeInfo, setRuntimeInfo] = useState(null) + const [workspaces, setWorkspaces] = useState([]) + const [launcherVersion, setLauncherVersion] = useState("--") + const mounted = useRef(true) + + useEffect(() => { + mounted.current = true + return () => { + mounted.current = false + } + }, []) + + const loadSettings = useCallback(async () => { + try { + const [boot, tray] = await Promise.all([ + window.api.getSetting("startOnBoot"), + window.api.getSetting("minimizeToTray"), + ]) + if (!mounted.current) return + if (boot !== undefined) setStartOnBoot(!!boot) + if (tray !== undefined) setMinimizeToTray(!!tray) + } catch {} + }, []) + + const loadRuntime = useCallback(async () => { + try { + const info = await window.api.runtimeInfo() + if (mounted.current) setRuntimeInfo(info) + return info + } catch { + return null + } + }, []) + + const loadWorkspaces = useCallback(async () => { + try { + const ws = await window.api.listWorkspaces() + if (mounted.current) setWorkspaces(ws) + } catch {} + }, []) + + const loadLauncherVersion = useCallback(async () => { + try { + const status = await window.api.pythonStatus() + if (mounted.current && status.launcherVersion) + setLauncherVersion(`v${status.launcherVersion}`) + } catch {} + }, []) + + useEffect(() => { + loadSettings() + loadLauncherVersion() + loadWorkspaces() + // Defer the slow runtime check so the page paints first, then keep + // re-polling at short intervals while the background latestVersion is + // still pending, then back off to the slow interval. + let shortPolls = 0 + let cancelled = false + let timer: ReturnType | null = null + const scheduleNext = (info: RuntimeInfo | null): void => { + if (cancelled) return + const stillLoading = !info || !info.latestVersion + const delay = stillLoading && shortPolls < 10 ? 2000 : 30000 + timer = setTimeout(async () => { + if (cancelled) return + shortPolls += 1 + const next = await loadRuntime() + loadWorkspaces() + scheduleNext(next ?? null) + }, delay) + } + const initial = setTimeout(async () => { + const info = await loadRuntime() + scheduleNext(info ?? null) + }, 0) + return () => { + cancelled = true + clearTimeout(initial) + if (timer) clearTimeout(timer) + } + }, [loadSettings, loadRuntime, loadWorkspaces, loadLauncherVersion]) + + const handleStartOnBoot = async (checked: boolean): Promise => { + setStartOnBoot(checked) + await window.api.setSetting("startOnBoot", checked) + } + + const handleMinimizeToTray = async (checked: boolean): Promise => { + setMinimizeToTray(checked) + await window.api.setSetting("minimizeToTray", checked) + } + + const removeWorkspace = async (slug: string): Promise => { + if ( + !confirm( + "This will remove the workspace locally and attempt to soft-delete it on the server.\nConnected agents will be disconnected.\n\nAre you sure?", + ) + ) + return + try { + showToast("Removing workspace...", "info") + await window.api.removeWorkspace(slug) + showToast("Workspace removed", "success") + loadWorkspaces() + } catch (err: unknown) { + showToast(`Error: ${(err as Error).message}`, "error") + } + } + + const runtimeRows: Array<{ + label: string + value: string + ok: boolean | null + loading: boolean + }> = [ + { + label: "Node.js:", + value: runtimeInfo?.nodeVersion || "Not installed", + ok: runtimeInfo ? !!runtimeInfo.nodeVersion : null, + loading: !runtimeInfo, + }, + { + label: "npm:", + value: runtimeInfo?.npmVersion + ? `v${runtimeInfo.npmVersion}` + : "Not installed", + ok: runtimeInfo ? !!runtimeInfo.npmVersion : null, + loading: !runtimeInfo, + }, + { + label: "Core Library:", + value: runtimeInfo?.coreVersion + ? `v${runtimeInfo.coreVersion}` + : "Not installed", + ok: runtimeInfo ? !!runtimeInfo.coreVersion : null, + loading: !runtimeInfo, + }, + { + label: "Latest Available:", + value: runtimeInfo?.latestVersion + ? `v${runtimeInfo.latestVersion}${ + runtimeInfo.coreVersion === runtimeInfo.latestVersion + ? " (up to date)" + : " (update available)" + }` + : "Unable to check", + ok: + runtimeInfo && runtimeInfo.latestVersion + ? runtimeInfo.coreVersion === runtimeInfo.latestVersion + : null, + loading: + !runtimeInfo || (!!runtimeInfo.npmVersion && !runtimeInfo.latestVersion), + }, + ] + + const runtimeColor = (ok: boolean | null): string | undefined => { + if (ok === null) return undefined + if (ok === true) return "var(--success-text)" + return "var(--danger-text)" + } + + return ( +
+

Settings

+ + {/* General — preserve launcher (modern) design */} +
+

General

+
+
+ + +
+ +
+ + +
+
+
+ + {/* Workspaces */} +
+

Workspaces

+ {workspaces.length === 0 ? ( + No workspaces configured. + ) : ( +
    + {workspaces.map((ws) => { + const slug = ws.slug || ws.id + const name = ws.name || slug + const url = `https://workspace.openagents.org/${slug}` + const fullUrl = ws.token + ? `${url}?token=${encodeURIComponent(ws.token)}` + : url + return ( +
  • + {name} + window.api.openExternal(fullUrl)} + > + {url} + + +
  • + ) + })} +
+ )} +
+ + {/* Runtime */} +
+

Runtime

+ {runtimeRows.map((row, idx) => ( +
+ {row.label} + {row.loading ? ( + + ) : ( + {row.value} + )} +
+ ))} +
+ + {/* About */} +
+

About

+

+ OpenAgents Launcher{" "} + {launcherVersion === "--" ? ( + + ) : ( + launcherVersion + )} +

+

+ { + e.preventDefault() + window.api.openExternal("https://docs.openagents.com") + }} + > + Documentation + +

+
+
+ ) +} diff --git a/packages/launcher/src/renderer/icons/aider.svg b/packages/launcher/src/renderer/public/icons/aider.svg similarity index 100% rename from packages/launcher/src/renderer/icons/aider.svg rename to packages/launcher/src/renderer/public/icons/aider.svg diff --git a/packages/launcher/src/renderer/icons/amp.svg b/packages/launcher/src/renderer/public/icons/amp.svg similarity index 100% rename from packages/launcher/src/renderer/icons/amp.svg rename to packages/launcher/src/renderer/public/icons/amp.svg diff --git a/packages/launcher/src/renderer/icons/claude.svg b/packages/launcher/src/renderer/public/icons/claude.svg similarity index 100% rename from packages/launcher/src/renderer/icons/claude.svg rename to packages/launcher/src/renderer/public/icons/claude.svg diff --git a/packages/launcher/src/renderer/icons/cline.svg b/packages/launcher/src/renderer/public/icons/cline.svg similarity index 100% rename from packages/launcher/src/renderer/icons/cline.svg rename to packages/launcher/src/renderer/public/icons/cline.svg diff --git a/packages/launcher/src/renderer/icons/codex.svg b/packages/launcher/src/renderer/public/icons/codex.svg similarity index 100% rename from packages/launcher/src/renderer/icons/codex.svg rename to packages/launcher/src/renderer/public/icons/codex.svg diff --git a/packages/launcher/src/renderer/icons/copilot.svg b/packages/launcher/src/renderer/public/icons/copilot.svg similarity index 100% rename from packages/launcher/src/renderer/icons/copilot.svg rename to packages/launcher/src/renderer/public/icons/copilot.svg diff --git a/packages/launcher/src/renderer/icons/cursor.svg b/packages/launcher/src/renderer/public/icons/cursor.svg similarity index 100% rename from packages/launcher/src/renderer/icons/cursor.svg rename to packages/launcher/src/renderer/public/icons/cursor.svg diff --git a/packages/launcher/src/renderer/icons/default.svg b/packages/launcher/src/renderer/public/icons/default.svg similarity index 100% rename from packages/launcher/src/renderer/icons/default.svg rename to packages/launcher/src/renderer/public/icons/default.svg diff --git a/packages/launcher/src/renderer/icons/gemini.svg b/packages/launcher/src/renderer/public/icons/gemini.svg similarity index 100% rename from packages/launcher/src/renderer/icons/gemini.svg rename to packages/launcher/src/renderer/public/icons/gemini.svg diff --git a/packages/launcher/src/renderer/icons/goose.svg b/packages/launcher/src/renderer/public/icons/goose.svg similarity index 100% rename from packages/launcher/src/renderer/icons/goose.svg rename to packages/launcher/src/renderer/public/icons/goose.svg diff --git a/packages/launcher/src/renderer/icons/kimi.svg b/packages/launcher/src/renderer/public/icons/kimi.svg similarity index 100% rename from packages/launcher/src/renderer/icons/kimi.svg rename to packages/launcher/src/renderer/public/icons/kimi.svg diff --git a/packages/launcher/src/renderer/icons/nanoclaw.svg b/packages/launcher/src/renderer/public/icons/nanoclaw.svg similarity index 100% rename from packages/launcher/src/renderer/icons/nanoclaw.svg rename to packages/launcher/src/renderer/public/icons/nanoclaw.svg diff --git a/packages/launcher/src/renderer/icons/openai.svg b/packages/launcher/src/renderer/public/icons/openai.svg similarity index 100% rename from packages/launcher/src/renderer/icons/openai.svg rename to packages/launcher/src/renderer/public/icons/openai.svg diff --git a/packages/launcher/src/renderer/icons/openclaw.svg b/packages/launcher/src/renderer/public/icons/openclaw.svg similarity index 100% rename from packages/launcher/src/renderer/icons/openclaw.svg rename to packages/launcher/src/renderer/public/icons/openclaw.svg diff --git a/packages/launcher/src/renderer/icons/opencode.svg b/packages/launcher/src/renderer/public/icons/opencode.svg similarity index 100% rename from packages/launcher/src/renderer/icons/opencode.svg rename to packages/launcher/src/renderer/public/icons/opencode.svg diff --git a/packages/launcher/src/renderer/icons/swebench.svg b/packages/launcher/src/renderer/public/icons/swebench.svg similarity index 100% rename from packages/launcher/src/renderer/icons/swebench.svg rename to packages/launcher/src/renderer/public/icons/swebench.svg diff --git a/packages/launcher/src/renderer/icons/yaml-agent.svg b/packages/launcher/src/renderer/public/icons/yaml-agent.svg similarity index 100% rename from packages/launcher/src/renderer/icons/yaml-agent.svg rename to packages/launcher/src/renderer/public/icons/yaml-agent.svg diff --git a/packages/launcher/src/renderer/renderer.js b/packages/launcher/src/renderer/renderer.js deleted file mode 100644 index 699c83675..000000000 --- a/packages/launcher/src/renderer/renderer.js +++ /dev/null @@ -1,1781 +0,0 @@ -// ---- Tab navigation ---- - -function switchTab(tabName) { - document.querySelectorAll('.nav-item').forEach((el) => { - el.classList.toggle('active', el.dataset.tab === tabName); - }); - document.querySelectorAll('.tab-content').forEach((el) => { - el.classList.toggle('active', el.id === `tab-${tabName}`); - }); -} - -document.querySelectorAll('.nav-item').forEach((el) => { - el.addEventListener('click', () => switchTab(el.dataset.tab)); -}); - -// Auto-refresh active tab every 5 seconds -let _currentTab = 'dashboard'; -const _origSwitchTab = switchTab; -let _refreshDashboardInFlight = null; -let _refreshDashboardQueued = false; -let _refreshAgentListInFlight = null; -let _refreshAgentListQueued = false; -const _pendingAgentActions = new Set(); -let _tabLoadToken = 0; -const TAB_LOAD_DELAY_MS = 120; -let _logsOffset = 0; -let _logsFilter = ''; -let _clearLogsInFlight = false; -const LOGS_INITIAL_LINES = 200; -const LOGS_MAX_BUFFER_LINES = 400; - -switchTab = function(tabName) { - _currentTab = tabName; - _origSwitchTab(tabName); - showTabSkeleton(tabName); - const token = ++_tabLoadToken; - requestAnimationFrame(() => { - setTimeout(() => loadTabContent(tabName, token), TAB_LOAD_DELAY_MS); - }); -}; -setInterval(() => { - if (_currentTab === 'dashboard') scheduleRefreshDashboard(); - else if (_currentTab === 'agents') scheduleRefreshAgentList(); -}, 5000); - -// Keyboard shortcuts: Ctrl+1..5 for tabs -const tabShortcuts = ['dashboard', 'agents', 'install', 'logs', 'settings']; -document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key >= '1' && e.key <= '5') { - e.preventDefault(); - switchTab(tabShortcuts[parseInt(e.key) - 1]); - } -}); - -// ---- Toast notifications ---- - -function showToast(message, type = 'info') { - let container = document.getElementById('toast-container'); - if (!container) { - container = document.createElement('div'); - container.id = 'toast-container'; - container.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;'; - document.body.appendChild(container); - } - const toast = document.createElement('div'); - const colors = { info: 'var(--accent)', success: 'var(--success)', error: 'var(--danger)', warning: 'var(--warning)' }; - toast.style.cssText = `background:var(--bg-card);border:1px solid ${colors[type] || colors.info};border-radius:var(--radius);padding:12px 18px;font-size:13px;color:var(--text-primary);box-shadow:0 4px 12px rgba(0,0,0,0.3);max-width:350px;animation:fadeIn 0.2s;`; - toast.textContent = message; - container.appendChild(toast); - setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 4000); -} - -// ---- Modal system ---- - -// ---- Agent icon helper ---- - -// Core library icons directory (resolved once at startup) -let _coreIconsDir = null; -(async () => { - try { _coreIconsDir = await window.api.getIconsDir(); } catch {} -})(); - -function showModal(html) { - document.getElementById('modal-content').innerHTML = html; - document.getElementById('modal-overlay').style.display = 'flex'; -} - -function closeModal() { - document.getElementById('modal-overlay').style.display = 'none'; - document.getElementById('modal-content').innerHTML = ''; -} - -document.getElementById('modal-overlay').addEventListener('click', (e) => { - if (e.target === e.currentTarget) closeModal(); -}); - -document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') closeModal(); -}); - -// ---- Agent Icon Helper ---- - -const BUNDLED_AGENT_ICON_SLUGS = new Set([ - 'aider', - 'amp', - 'claude', - 'cline', - 'codex', - 'copilot', - 'cursor', - 'default', - 'gemini', - 'goose', - 'kimi', - 'nanoclaw', - 'openai', - 'openclaw', - 'opencode', - 'swebench', - 'yaml-agent', -]); - -function agentIcon(type, size = 24) { - const slug = (type || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); - const iconSlug = BUNDLED_AGENT_ICON_SLUGS.has(slug) ? slug : 'default'; - return `${esc(type)}`; -} - -function formatHealthLabel(health) { - if (!health) return 'Not configured'; - if (!health.ready) return health.message || 'Not configured'; - const parts = ['Ready']; - if (health.auth_mode === 'api_key') parts.push('API key'); - else if (health.auth_mode === 'cli_login') parts.push('CLI login'); - if (health.execution_mode && health.execution_mode !== 'unavailable') { - parts.push(health.execution_mode); - } - return parts.join(' · '); -} - -// ---- Dashboard ---- - -function scheduleRefreshDashboard() { - if (_refreshDashboardInFlight) { - _refreshDashboardQueued = true; - return _refreshDashboardInFlight; - } - _refreshDashboardInFlight = (async () => { - try { - await refreshDashboard(); - } finally { - _refreshDashboardInFlight = null; - if (_refreshDashboardQueued) { - _refreshDashboardQueued = false; - scheduleRefreshDashboard(); - } - } - })(); - return _refreshDashboardInFlight; -} - -async function loadTabContent(tabName, token) { - if (token !== _tabLoadToken) return; - - if (tabName === 'dashboard') { - await scheduleRefreshDashboard(); - return; - } - - if (tabName === 'agents') { - await scheduleRefreshAgentList(); - return; - } - - if (tabName === 'install') { - if (token !== _tabLoadToken) return; - refreshInstallStatus(); - return; - } - - if (tabName === 'logs') { - if (token !== _tabLoadToken) return; - refreshLogs(); - return; - } - - if (tabName === 'settings') { - if (token !== _tabLoadToken) return; - refreshSettingsWorkspaces(); - refreshSettingsRuntime(); - } -} - -function showTabSkeleton(tabName) { - if (tabName === 'dashboard') { - const el = document.getElementById('agent-cards'); - if (el) { - el.innerHTML = ` -
-
-
-
-
-
-
-
-
-
`; - } - return; - } - - if (tabName === 'agents') { - const el = document.getElementById('agent-list'); - if (el) { - el.innerHTML = ` -
-
-
-
-
-
-
-
-
-
-
-
-
-
`; - } - return; - } - - if (tabName === 'install') { - const el = document.getElementById('catalog-table-container'); - if (el) { - el.innerHTML = ` -
-
-
-
-
-
-
-
-
-
`; - } - return; - } - - if (tabName === 'logs') { - _logsOffset = 0; - const el = document.getElementById('log-viewer'); - if (el) { - el.textContent = 'Loading logs...'; - } - return; - } - - if (tabName === 'settings') { - const ws = document.getElementById('settings-workspaces'); - if (ws) ws.innerHTML = '
Loading...
'; - const ids = ['settings-nodejs-version', 'settings-npm-version', 'settings-core-version', 'settings-core-latest']; - ids.forEach((id) => { - const el = document.getElementById(id); - if (el) el.textContent = 'Loading...'; - }); - } -} - -async function refreshDashboard() { - if (_currentTab !== 'dashboard') return; - let agents = []; - try { - agents = await window.api.listAgents() || []; - if (_currentTab !== 'dashboard') return; - const cardsEl = document.getElementById('agent-cards'); - - if (agents.length === 0) { - cardsEl.innerHTML = ` -
-

No agents configured yet.

- -
`; - } else { - cardsEl.innerHTML = agents.map((a) => { - const isRunning = a.state === 'online' || a.state === 'running' || a.state === 'idle'; - const health = a.health || {}; - const isConnected = !!a.network; - const isUnsupported = isUnsupportedAgent(a); - const wsLabel = a.network - ? (a.networkName && a.networkName !== a.network ? `${a.network} (${a.networkName})` : a.network) - : ''; - const configLabel = formatHealthLabel(health); - - // Status indicators - const configStatus = isUnsupported - ? 'Launcher core update required' - : (health.ready - ? `${esc(configLabel)}` - : `${esc(configLabel)}`); - const connectStatus = isConnected - ? `Connected: ${esc(wsLabel)}` - : 'Not connected'; - - // Simplified buttons - let buttons = ''; - if (isRunning) { - buttons += ``; - if (isConnected) { - buttons += ``; - } - } else { - buttons += ``; - } - - return ` -
-
- ${agentIcon(a.type)} - ${esc(a.name)} - ${esc(a.type)} -
-
- - ${esc(displayState(a.state))} -
-
- ${configStatus} ${connectStatus} -
- ${a.lastError ? `
${esc(a.lastError)}
` : ''} -
${buttons}
-
`; - }).join(''); - } - } catch (err) { - console.error('Dashboard refresh error:', err); - } - - // Update daemon status bar using the same agents data (no extra IPC call) - updateDaemonStatusFromAgents(agents); - - try { - const status = await window.api.pythonStatus(); - const banner = document.getElementById('setup-banner'); - const versionEl = document.getElementById('sdk-version'); - // Node.js native — always ready - banner.style.display = 'none'; - const launcherEl = document.getElementById('launcher-version'); - if (launcherEl && status.launcherVersion) launcherEl.textContent = `Launcher v${status.launcherVersion}`; - versionEl.textContent = `Core v${status.sdkVersion}`; - } catch {} -} - -function scheduleRefreshAgentList() { - if (_refreshAgentListInFlight) { - _refreshAgentListQueued = true; - return _refreshAgentListInFlight; - } - _refreshAgentListInFlight = (async () => { - try { - await refreshAgentList(); - } finally { - _refreshAgentListInFlight = null; - if (_refreshAgentListQueued) { - _refreshAgentListQueued = false; - scheduleRefreshAgentList(); - } - } - })(); - return _refreshAgentListInFlight; -} - -function updateDaemonStatusFromAgents(agents) { - const el = document.getElementById('daemon-status'); - const hasOnline = agents.some((a) => a.state === 'online' || a.state === 'running' || a.state === 'idle' || a.state === 'idle'); - const hasStarting = agents.some((a) => a.state === 'starting' || a.state === 'reconnecting'); - - if (hasOnline) { - el.innerHTML = 'Daemon: running'; - } else if (hasStarting) { - el.innerHTML = 'Daemon: starting'; - } else if (agents.length > 0) { - el.innerHTML = 'Daemon: stopped'; - } else { - el.innerHTML = 'Daemon: offline'; - } -} - -function renderAgentActionLabel(name, isRunning) { - if (_pendingAgentActions.has(name)) { - return isRunning ? 'Stopping...' : 'Starting...'; - } - return isRunning ? 'Stop' : 'Start'; -} - -async function updateDaemonStatus() { - try { - const agents = await window.api.listAgents() || []; - updateDaemonStatusFromAgents(agents); - } catch { - document.getElementById('daemon-status').innerHTML = - 'Daemon: offline'; - } -} - -async function toggleAgent(name, currentState) { - if (_pendingAgentActions.has(name)) return; - _pendingAgentActions.add(name); - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - - try { - if (currentState === 'online' || currentState === 'running' || currentState === 'idle') { - await window.api.stopAgent(name); - showToast(`Stopping ${name}...`, 'info'); - // Poll until stopped (up to 15s — daemon checks commands every 5s) - for (let i = 0; i < 5; i++) { - await new Promise(r => setTimeout(r, 3000)); - const status = await window.api.agentStatus(); - const agent = status[name]; - if (!agent || agent.state === 'stopped') { - showToast(`${name} stopped`, 'success'); - break; - } - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } - } else { - await window.api.startAgent(name); - showToast(`Starting ${name}...`, 'info'); - // Poll until running (up to 30s — daemon needs time to connect) - for (let i = 0; i < 10; i++) { - await new Promise(r => setTimeout(r, 3000)); - const status = await window.api.agentStatus(); - const agent = status[name]; - if (agent && (agent.state === 'running' || agent.state === 'online')) { - showToast(`${name} is now running`, 'success'); - break; - } - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } - } - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } finally { - _pendingAgentActions.delete(name); - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } -} - -// Daemon lifecycle is automatic — tied to Launcher app. -// Start All / Stop All buttons removed from UI. - -// ---- Agent Actions (context menu) ---- - -function showAgentActions(name, type, state, network) { - const isRunning = state === 'online' || state === 'running' || state === 'idle'; - const actions = []; - - if (isRunning) { - actions.push(``); - } else { - actions.push(``); - } - - actions.push(``); - actions.push(``); - - if (network) { - actions.push(``); - actions.push(``); - } else { - actions.push(``); - } - - actions.push(``); - - showModal(` -

Agent: ${esc(name)}

- - - `); -} - -async function disconnectAgent(name) { - try { - await window.api.disconnectWorkspace(name); - showToast(`Disconnected ${name} from workspace`, 'success'); - window.api.signalReload(); - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -async function openWorkspaceInBrowser(name) { - try { - const agents = await window.api.listAgents(); - const agent = agents.find((a) => a.name === name); - if (!agent || !agent.network) { - showToast('No workspace connected', 'warning'); - return; - } - // Look up workspace details (slug + token) - const workspaces = await window.api.listWorkspaces(); - const ws = workspaces.find((w) => w.slug === agent.network || w.id === agent.network); - const slug = (ws && ws.slug) || agent.network; - let url = `https://workspace.openagents.org/${slug}`; - if (ws && ws.token) url += `?token=${encodeURIComponent(ws.token)}`; - window.api.openExternal(url); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -// ---- Configure Agent Screen ---- - -async function openConfigureScreen(agentName, agentType) { - showModal(`
Loading configuration...
`); - - try { - const [fields, typeSaved, instanceSaved] = await Promise.all([ - window.api.getEnvFields(agentType), - window.api.getAgentEnv(agentType), - agentName ? window.api.getAgentInstanceEnv(agentName) : Promise.resolve({}), - ]); - const saved = { ...(typeSaved || {}), ...(instanceSaved || {}) }; - - if (!fields || fields.length === 0) { - // Check if agent type requires login (e.g., Claude Code) - const catalog = await window.api.getCatalog(); - const entry = catalog.find(c => c.name === agentType); - const checkReady = entry?.check_ready; - - if (checkReady?.login_command) { - // Agent uses login-based auth (not env vars) - let loggedIn = false; - try { - const health = await window.api.healthCheck(agentType); - loggedIn = health?.ready || false; - } catch {} - - showModal(` -

Configure ${esc(agentName || agentType)}

-

This agent uses login-based authentication.

-
- ${loggedIn ? '✅' : '⚠️'} - ${loggedIn ? 'Logged in' : 'Not logged in'} - ${!loggedIn ? `

${esc(checkReady.not_ready_message || 'Login required')}

` : ''} -
-
- - -
- `); - - document.getElementById('btn-agent-login').addEventListener('click', async () => { - const cmd = checkReady.login_command; - showToast(`Opening terminal for ${cmd}... Complete login in the new window.`, 'info'); - try { - // Open login command in a visible terminal window - await window.api.openTerminal(cmd); - // Give user time to complete login, then refresh - setTimeout(() => openConfigureScreen(agentName, agentType), 5000); - } catch (err) { - showToast(`Failed to open terminal: ${err.message}`, 'error'); - } - }); - return; - } - - showModal(` -

Configure ${esc(agentName || agentType)}

-

No configuration required for this agent type.

- - `); - return; - } - - const fieldsHtml = fields.map((f) => { - const current = saved[f.name] || f.default || ''; - const required = f.required ? ' *' : ''; - const inputType = f.password ? 'password' : 'text'; - return ` -
- - -
`; - }).join(''); - - showModal(` -

Configure ${esc(agentName || agentType)}

-

${agentName ? 'Settings saved for this agent. Type defaults remain available as fallbacks.' : 'Settings saved to ~/.openagents/env/'}

-
- ${fieldsHtml} -
-
- - `); - } catch (err) { - showModal(` -

Error

-

${esc(err.message)}

- - `); - } -} - -async function saveConfig(agentName, agentType) { - const fields = document.querySelectorAll('.configure-form input'); - const env = {}; - fields.forEach((input) => { - const name = input.id.replace('cfg-', ''); - const val = input.value.trim(); - if (val) env[name] = val; - }); - - try { - if (agentName) { - await window.api.saveAgentInstanceEnv(agentName, env); - } else { - await window.api.saveAgentEnv(agentType, env); - } - showToast('Configuration saved', 'success'); - closeModal(); - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } catch (err) { - showToast(`Error saving: ${err.message}`, 'error'); - } -} - -async function testLLMConfig(agentType) { - const fields = document.querySelectorAll('.configure-form input'); - const env = {}; - fields.forEach((input) => { - const name = input.id.replace('cfg-', ''); - const val = input.value.trim(); - if (val) env[name] = val; - }); - - const resultEl = document.getElementById('test-result'); - if (!resultEl) return; - - resultEl.innerHTML = 'Testing...'; - - try { - const result = await window.api.testLLM(env); - if (result.success) { - resultEl.innerHTML = `OK — model: ${esc(result.model)}, response: "${esc(result.response)}"`; - } else { - resultEl.innerHTML = `${esc(result.error)}`; - } - } catch (err) { - resultEl.innerHTML = `${esc(err.message)}`; - } -} - -// ---- Connect Workspace Screen ---- - -async function showConnectWorkspace(agentName) { - showModal(`
Loading workspaces...
`); - - try { - const networks = await window.api.listWorkspaces(); - - let rows = ''; - if (networks && networks.length > 0) { - rows = networks.map((n) => { - const display = n.name || n.slug || n.id; - const url = n.endpoint && (n.endpoint.includes('localhost') || n.endpoint.includes('127.0.0.1')) - ? `${n.endpoint}/${n.slug || n.id}` - : `workspace.openagents.org/${n.slug || n.id}`; - return ``; - }).join(''); - } - - showModal(` -

Connect '${esc(agentName)}' to Workspace

- - - `); - } catch (err) { - showModal(` -

Error

-

${esc(err.message)}

- - `); - } -} - -async function doConnectWorkspace(agentName, slug) { - try { - showToast(`Connecting ${agentName} to workspace...`, 'info'); - await window.api.connectWorkspace(agentName, slug); - window.api.signalReload(); - showToast(`Connected to ${slug}`, 'success'); - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -function showCreateWorkspace(agentName) { - showModal(` -

Create New Workspace

-
- - -
- - `); - setTimeout(() => { const el = document.getElementById('new-workspace-name'); if (el) el.focus(); }, 100); -} - -async function doCreateWorkspace(agentName) { - const name = document.getElementById('new-workspace-name')?.value?.trim(); - if (!name) { showToast('Workspace name is required', 'warning'); return; } - - closeModal(); - try { - showToast(`Creating workspace '${name}'...`, 'info'); - const result = await window.api.createWorkspace(name); - showToast(`Workspace '${name}' created`, 'success'); - - // Auto-connect the agent using the returned token - if (result && result.token && agentName) { - await window.api.connectWorkspace(agentName, result.token); - window.api.signalReload(); - showToast(`Connected ${agentName} to ${name}`, 'success'); - } - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -function showJoinWithToken(agentName) { - showModal(` -

Join Workspace with Token

-
- - -
- - `); - setTimeout(() => { const el = document.getElementById('workspace-token'); if (el) el.focus(); }, 100); -} - -async function doJoinWithToken(agentName) { - const token = document.getElementById('workspace-token')?.value?.trim(); - if (!token) { showToast('Token is required', 'warning'); return; } - - closeModal(); - try { - showToast('Joining workspace...', 'info'); - await window.api.connectWorkspace(agentName, token); - window.api.signalReload(); - showToast('Joined workspace', 'success'); - scheduleRefreshDashboard(); - scheduleRefreshAgentList(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -// ---- Agents tab ---- - -document.getElementById('btn-add-agent').addEventListener('click', () => showNewAgentDialog()); - -async function showNewAgentDialog() { - // First check which agent types are installed - showModal(`
Loading installed types...
`); - - try { - const [catalog, supportedTypes] = await Promise.all([ - window.api.getCatalog(), - window.api.getSupportedAgentTypes(), - ]); - const supportedSet = new Set(supportedTypes || []); - const installed = catalog.filter((c) => c.installed); - const supportedInstalled = installed.filter((c) => supportedSet.has(c.name)); - const unsupportedInstalled = installed.filter((c) => !supportedSet.has(c.name)); - - if (supportedInstalled.length === 0) { - const extraHint = unsupportedInstalled.length > 0 - ? `

Installed but not supported in Launcher yet: ${esc(unsupportedInstalled.map((c) => c.label || c.name).join(', '))}

` - : ''; - showModal(` -

New Agent

-

No Launcher-supported agent runtimes installed. Install one first.

- ${extraHint} - - `); - return; - } - - const typeOptions = supportedInstalled.map((c) => - `` - ).join(''); - - showModal(` -

New Agent

-
- - -
-
- - -
-
- - -
- - `); - - // Auto-generate name - const nameInput = document.getElementById('new-agent-name'); - const typeSelect = document.getElementById('new-agent-type'); - const generateName = () => { - const type = typeSelect.value; - const suffix = Math.random().toString(36).slice(2, 6); - nameInput.placeholder = `${type}-${suffix}`; - }; - typeSelect.addEventListener('change', generateName); - generateName(); - setTimeout(() => nameInput.focus(), 100); - } catch (err) { - showModal(` -

Error

-

${esc(err.message)}

- - `); - } -} - -async function doAddAgent() { - const type = document.getElementById('new-agent-type')?.value; - let name = document.getElementById('new-agent-name')?.value?.trim(); - const agentPath = document.getElementById('new-agent-path')?.value?.trim(); - - if (!name) { - name = document.getElementById('new-agent-name')?.placeholder || `${type}-${Math.random().toString(36).slice(2, 6)}`; - } - - if (!/^[a-zA-Z0-9_-]+$/.test(name)) { - showToast('Agent name can only contain letters, numbers, hyphens, and underscores', 'warning'); - return; - } - - closeModal(); - - try { - await window.api.addAgent({ name, type, path: agentPath || undefined }); - showToast(`Agent '${name}' created`, 'success'); - // Open configure screen for the new agent - openConfigureScreen(name, type); - scheduleRefreshAgentList(); - scheduleRefreshDashboard(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -async function refreshAgentList() { - if (_currentTab !== 'agents') return; - try { - const agents = await window.api.listAgents(); - if (_currentTab !== 'agents') return; - const listEl = document.getElementById('agent-list'); - - if (!agents || agents.length === 0) { - listEl.innerHTML = '

No agents configured. Click "+ New Agent" to get started.

'; - return; - } - - listEl.innerHTML = agents.map((a) => { - const isRunning = a.state === 'online' || a.state === 'running' || a.state === 'idle'; - const slug = a.network || ''; - const wsDisplay = slug ? (a.networkName && a.networkName !== slug ? `${slug} (${a.networkName})` : slug) : ''; - const health = a.health || {}; - const unsupported = isUnsupportedAgent(a); - const envDisplay = []; - if (a.env?.LLM_BASE_URL || a.env?.OPENAI_BASE_URL) envDisplay.push(`API: ${a.env.LLM_BASE_URL || a.env.OPENAI_BASE_URL}`); - if (a.env?.LLM_MODEL || a.env?.OPENCLAW_MODEL) envDisplay.push(`Model: ${a.env.LLM_MODEL || a.env.OPENCLAW_MODEL}`); - const readyLabel = formatHealthLabel(health); - - return ` -
-
-
-
- ${agentIcon(a.type, 28)} -

${esc(a.name)}

-
- ${esc(a.type)} - - ${unsupported ? 'Launcher core update required' : health.ready ? `🔑 ${esc(readyLabel)}` : `⚠ ${esc(readyLabel)}`} - ${envDisplay.length ? ' · ' + envDisplay.map(esc).join(' · ') : ''} - - ${a.lastError ? `${esc(a.lastError)}` : ''} -
-
- - ${esc(displayState(a.state))} - ${wsDisplay ? `${esc(wsDisplay)}` : 'Not connected'} -
-
-
-
- - - ${a.network - ? ` - ` - : `` - } -
- -
-
`; - }).join(''); - } catch (err) { - console.error('Agent list error:', err); - showToast('Failed to load agents', 'error'); - } -} - -async function removeAgent(name) { - if (!confirm(`Remove agent '${name}'? This will stop it if running.`)) return; - try { - await window.api.removeAgent(name); - showToast(`Agent '${name}' removed`, 'success'); - scheduleRefreshAgentList(); - scheduleRefreshDashboard(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -// ---- Install tab ---- - -async function refreshInstallStatus() { - - // Catalog - refreshCatalog(); -} - -async function refreshCatalog() { - const container = document.getElementById('catalog-table-container'); - - try { - const catalog = await window.api.getCatalog(); - const healthByName = {}; - await Promise.all(catalog.map(async (c) => { - try { - healthByName[c.name] = await window.api.healthCheck(c.name); - } catch { - healthByName[c.name] = null; - } - })); - - if (!catalog || catalog.length === 0) { - container.innerHTML = '

No agent runtimes available. Install the SDK first.

'; - return; - } - - const rows = catalog.map((c) => { - const health = healthByName[c.name] || {}; - const readiness = formatHealthLabel(health); - return ` -
-
- ${agentIcon(c.name, 28)} -
- ${esc(c.label || c.name)} - ${esc(c.description || '')} - ${esc(readiness)} - - - 🌐 - 🤝 - -
-
-
- ${c.installed - ? (c.managed === false - ? 'global' - : 'installed') - : 'not installed'} -
-
- - ${c.installed && c.managed !== false ? `` : ''} -
-
- `; - }).join(''); - - container.innerHTML = `
${rows}
`; - // Apply any existing search filter - const searchInput = document.getElementById('catalog-search-input'); - if (searchInput && searchInput.value) filterCatalog(searchInput.value); - } catch (err) { - container.innerHTML = `

Failed to load catalog: ${esc(err.message)}

`; - } -} - -function filterCatalog(query) { - const q = query.toLowerCase(); - document.querySelectorAll('.catalog-row').forEach((row) => { - const text = row.textContent.toLowerCase(); - row.style.display = text.includes(q) ? '' : 'none'; - }); -} - -async function installCatalogItem(name, isInstalled) { - const verb = isInstalled ? 'Update' : 'Install'; - - // Confirmation modal - const confirmed = await new Promise((resolve) => { - showModal(` -
- ${agentIcon(name, 40)} -

${verb} ${esc(name)}?

-

This will run npm install -g ${esc(name)}@latest on your system.

- -
- `); - document.getElementById('confirm-install-yes').addEventListener('click', () => { closeModal(); resolve(true); }); - document.getElementById('confirm-install-no').addEventListener('click', () => { closeModal(); resolve(false); }); - }); - if (!confirmed) return; - - // Switch to dedicated install view — hide tabs, show progress overlay - const content = document.getElementById('content'); - document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); - // Remove any previous progress view - const oldProgress = document.getElementById('install-progress-overlay'); - if (oldProgress) oldProgress.remove(); - - const progressView = document.createElement('div'); - progressView.id = 'install-progress-overlay'; - progressView.className = 'install-progress-view'; - progressView.innerHTML = ` -
- ${agentIcon(name, 32)} -
-

${verb} ${esc(name)}

-

Full installation log is shown below.

-
-
-

-      `;
-  content.appendChild(progressView);
-
-  const logEl = document.getElementById('install-live-log');
-  const doneBar = document.getElementById('install-done-bar');
-
-  // D22: Check dependencies
-  try {
-    const catalog = await window.api.getCatalog();
-    const entry = catalog.find(c => c.name === name);
-    if (entry && entry.requires) {
-      for (const dep of entry.requires) {
-        const depName = dep === 'nodejs' ? 'node' : dep;
-        logEl.textContent += `Checking dependency: ${dep}... `;
-        try {
-          const check = await window.api.healthCheck(depName);
-          if (check && check.installed) {
-            logEl.textContent += `OK (${check.version || 'found'})\n`;
-          } else {
-            logEl.textContent += `NOT FOUND\n\n⚠ Please install ${dep} first.\n`;
-            doneBar.style.display = 'block';
-            document.getElementById('install-back-btn').addEventListener('click', () => {
-              const overlay = document.getElementById('install-progress-overlay');
-              if (overlay) overlay.remove();
-              // switchTab will re-add .active to the correct tab
-              switchTab('install');
-            });
-            return;
-          }
-        } catch {
-          logEl.textContent += `OK (assumed)\n`;
-        }
-      }
-    }
-  } catch {}
-
-  logEl.textContent += `\n`;
-
-  // Listen for streaming output
-  let lastOutputTime = Date.now();
-  window.api.onInstallOutput((data) => {
-    // Remove progress spinner before appending real output
-    if (progressLine && logEl.textContent.endsWith(progressLine)) {
-      logEl.textContent = logEl.textContent.slice(0, -progressLine.length);
-    }
-    logEl.textContent += data;
-    logEl.scrollTop = logEl.scrollHeight;
-    lastOutputTime = Date.now();
-  });
-
-  // Show progress inside the log panel while npm is silent
-  const spinChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
-  let spinIdx = 0;
-  let progressLine = '';
-  const startTime = Date.now();
-  const timerInterval = setInterval(() => {
-    const elapsed = Math.floor((Date.now() - startTime) / 1000);
-    const silentFor = Math.floor((Date.now() - lastOutputTime) / 1000);
-    if (silentFor > 2) {
-      // Remove previous progress line if present
-      if (progressLine && logEl.textContent.endsWith(progressLine)) {
-        logEl.textContent = logEl.textContent.slice(0, -progressLine.length);
-      }
-      spinIdx = (spinIdx + 1) % spinChars.length;
-      progressLine = `${spinChars[spinIdx]} Downloading and installing packages... (${elapsed}s elapsed)`;
-      logEl.textContent += progressLine;
-      logEl.scrollTop = logEl.scrollHeight;
-    }
-  }, 200);
-
-  try {
-    await window.api.installAgentTypeStreaming(name);
-    logEl.textContent += `\n✓ ${name} installed successfully.\n`;
-  } catch (err) {
-    logEl.textContent += `\n✗ Error: ${err.message}\n`;
-  }
-
-  clearInterval(timerInterval);
-  if (progressLine && logEl.textContent.endsWith(progressLine)) {
-    logEl.textContent = logEl.textContent.slice(0, -progressLine.length);
-  }
-
-  window.api.removeInstallOutputListener();
-  doneBar.style.display = 'block';
-  document.getElementById('install-back-btn').addEventListener('click', () => {
-    // Remove progress overlay and restore tabs
-    const overlay = document.getElementById('install-progress-overlay');
-    if (overlay) overlay.remove();
-    // switchTab will re-add .active to the correct tab
-    switchTab('install');
-  });
-}
-
-async function uninstallCatalogItem(name) {
-  // Confirmation modal
-  const confirmed = await new Promise((resolve) => {
-    showModal(`
-      
- ${agentIcon(name, 40)} -

Uninstall ${esc(name)}?

-

This will remove ${esc(name)} from your system.

- -
- `); - document.getElementById('confirm-install-yes').addEventListener('click', () => { closeModal(); resolve(true); }); - document.getElementById('confirm-install-no').addEventListener('click', () => { closeModal(); resolve(false); }); - }); - if (!confirmed) return; - - const content = document.getElementById('content'); - document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); - const oldProgress = document.getElementById('install-progress-overlay'); - if (oldProgress) oldProgress.remove(); - - const progressView = document.createElement('div'); - progressView.id = 'install-progress-overlay'; - progressView.className = 'install-progress-view'; - progressView.innerHTML = ` -
- ${agentIcon(name, 32)} -
-

Uninstalling ${esc(name)}

-

Full uninstallation log is shown below.

-
-
-

-      `;
-  content.appendChild(progressView);
-
-  const logEl = document.getElementById('install-live-log');
-  const doneBar = document.getElementById('install-done-bar');
-
-  let lastOutputTime = Date.now();
-  window.api.onInstallOutput((data) => {
-    // Remove progress line before appending real output
-    if (progressLine && logEl.textContent.endsWith(progressLine)) {
-      logEl.textContent = logEl.textContent.slice(0, -progressLine.length);
-    }
-    logEl.textContent += data;
-    logEl.scrollTop = logEl.scrollHeight;
-    lastOutputTime = Date.now();
-  });
-
-  const spinChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
-  let spinIdx = 0;
-  let progressLine = '';
-  const startTime = Date.now();
-  const timerInterval = setInterval(() => {
-    const elapsed = Math.floor((Date.now() - startTime) / 1000);
-    const silentFor = Math.floor((Date.now() - lastOutputTime) / 1000);
-    if (silentFor > 2) {
-      if (progressLine && logEl.textContent.endsWith(progressLine)) {
-        logEl.textContent = logEl.textContent.slice(0, -progressLine.length);
-      }
-      spinIdx = (spinIdx + 1) % spinChars.length;
-      progressLine = `${spinChars[spinIdx]} Removing packages... (${elapsed}s elapsed)`;
-      logEl.textContent += progressLine;
-      logEl.scrollTop = logEl.scrollHeight;
-    }
-  }, 200);
-
-  try {
-    await window.api.uninstallAgentTypeStreaming(name);
-    logEl.textContent += `\n✓ ${name} uninstalled successfully.\n`;
-  } catch (err) {
-    logEl.textContent += `\n✗ Error: ${err.message}\n`;
-  }
-
-  clearInterval(timerInterval);
-  if (progressLine && logEl.textContent.endsWith(progressLine)) {
-    logEl.textContent = logEl.textContent.slice(0, -progressLine.length);
-  }
-  window.api.removeInstallOutputListener();
-  doneBar.style.display = 'block';
-  document.getElementById('install-back-btn').addEventListener('click', () => {
-    // Remove progress overlay and restore tabs
-    const overlay = document.getElementById('install-progress-overlay');
-    if (overlay) overlay.remove();
-    // switchTab will re-add .active to the correct tab
-    switchTab('install');
-  });
-}
-
-// SDK install button removed — agent-connector is bundled with the app
-
-// ---- Logs tab ----
-
-async function refreshLogs() {
-  if (_currentTab !== 'logs') return;
-  try {
-    const filter = document.getElementById('log-agent-filter').value;
-    const viewer = document.getElementById('log-viewer');
-    const reset = filter !== _logsFilter || _logsOffset === 0;
-
-    const result = reset
-      ? await window.api.tailAgentLogs(filter, LOGS_INITIAL_LINES, 0)
-      : await window.api.tailAgentLogs(filter, LOGS_INITIAL_LINES, _logsOffset);
-    if (_currentTab !== 'logs') return;
-
-    _logsFilter = filter;
-    _logsOffset = result.size || 0;
-
-    if (reset) {
-      if (result.lines && result.lines.length > 0) {
-        viewer.textContent = result.lines.join('\n');
-      } else {
-        viewer.textContent = 'No logs available.\n\nLogs appear here after the daemon starts.';
-      }
-      viewer.scrollTop = viewer.scrollHeight;
-    } else {
-      if (result.lines && result.lines.length > 0) {
-        const existing = viewer.textContent ? viewer.textContent.split('\n').filter(Boolean) : [];
-        const merged = existing.concat(result.lines).slice(-LOGS_MAX_BUFFER_LINES);
-        viewer.textContent = merged.join('\n');
-        viewer.scrollTop = viewer.scrollHeight;
-      }
-    }
-  } catch (err) {
-    document.getElementById('log-viewer').textContent = 'Error loading logs: ' + err.message;
-  }
-
-  // Populate agent filter dropdown
-  try {
-    const agents = await window.api.listAgents();
-    if (_currentTab !== 'logs') return;
-    const select = document.getElementById('log-agent-filter');
-    const current = select.value;
-    const existingOptions = new Set();
-    select.querySelectorAll('option').forEach((o) => existingOptions.add(o.value));
-
-    (agents || []).forEach((a) => {
-      if (!existingOptions.has(a.name)) {
-        const opt = document.createElement('option');
-        opt.value = a.name;
-        opt.textContent = a.name;
-        if (a.name === current) opt.selected = true;
-        select.appendChild(opt);
-      }
-    });
-  } catch {}
-}
-
-function toDateTimeLocalValue(date) {
-  const pad = (value) => String(value).padStart(2, '0');
-  return [
-    date.getFullYear(),
-    '-',
-    pad(date.getMonth() + 1),
-    '-',
-    pad(date.getDate()),
-    'T',
-    pad(date.getHours()),
-    ':',
-    pad(date.getMinutes()),
-  ].join('');
-}
-
-function showClearLogsModal() {
-  const now = new Date();
-  const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
-  showModal(`
-    

Clear Logs

-

Delete log entries from daemon.log whose timestamps fall inside the selected time range.

-
- - -
-
- - -
-
- - `); - - const confirmBtn = document.getElementById('confirm-clear-logs'); - confirmBtn.addEventListener('click', clearLogsInRange); -} - -async function clearLogsInRange() { - if (_clearLogsInFlight) return; - - const startEl = document.getElementById('clear-logs-start'); - const endEl = document.getElementById('clear-logs-end'); - const errorEl = document.getElementById('clear-logs-error'); - const confirmBtn = document.getElementById('confirm-clear-logs'); - - const start = startEl?.value ? new Date(startEl.value) : null; - const end = endEl?.value ? new Date(endEl.value) : null; - - if (!start || Number.isNaN(start.getTime()) || !end || Number.isNaN(end.getTime())) { - if (errorEl) errorEl.textContent = 'Please select a valid start and end time.'; - return; - } - if (start.getTime() > end.getTime()) { - if (errorEl) errorEl.textContent = 'Start time must be before end time.'; - return; - } - - _clearLogsInFlight = true; - if (confirmBtn) { - confirmBtn.disabled = true; - confirmBtn.textContent = 'Deleting...'; - } - if (errorEl) errorEl.textContent = ''; - - try { - const result = await window.api.clearLogsInRange(start.toISOString(), end.toISOString()); - closeModal(); - _logsOffset = 0; - await refreshLogs(); - showToast(`Deleted ${result.removed || 0} log lines from the selected range`, 'success'); - } catch (err) { - if (errorEl) errorEl.textContent = err.message || 'Failed to clear logs.'; - if (confirmBtn) { - confirmBtn.disabled = false; - confirmBtn.textContent = 'Delete'; - } - } finally { - _clearLogsInFlight = false; - } -} - -document.getElementById('btn-refresh-logs').addEventListener('click', () => { - _logsOffset = 0; - refreshLogs(); -}); -document.getElementById('btn-clear-logs').addEventListener('click', () => { - showClearLogsModal(); -}); -document.getElementById('btn-copy-logs').addEventListener('click', () => { - const logs = document.getElementById('log-viewer').textContent; - navigator.clipboard.writeText(logs).then(() => { - showToast('Logs copied to clipboard', 'success'); - }).catch(() => { - showToast('Failed to copy logs', 'error'); - }); -}); -document.getElementById('log-agent-filter').addEventListener('change', () => { - _logsOffset = 0; - refreshLogs(); -}); -document.getElementById('catalog-search-input').addEventListener('input', (e) => filterCatalog(e.target.value)); - -// ---- Settings tab ---- - -document.getElementById('link-docs').addEventListener('click', (e) => { - e.preventDefault(); - window.api.openExternal('https://docs.openagents.com'); -}); - -(async () => { - try { - const startOnBoot = await window.api.getSetting('startOnBoot'); - const minimizeToTray = await window.api.getSetting('minimizeToTray'); - if (startOnBoot !== undefined) document.getElementById('setting-start-on-boot').checked = !!startOnBoot; - if (minimizeToTray !== undefined) document.getElementById('setting-minimize-to-tray').checked = !!minimizeToTray; - } catch {} -})(); - -document.getElementById('setting-start-on-boot').addEventListener('change', (e) => { - window.api.setSetting('startOnBoot', e.target.checked); -}); -document.getElementById('setting-minimize-to-tray').addEventListener('change', (e) => { - window.api.setSetting('minimizeToTray', e.target.checked); -}); - -// ---- Utilities ---- - -function esc(str) { - if (str == null) return ''; - const div = document.createElement('div'); - div.textContent = String(str); - return div.innerHTML; -} - -function displayState(state) { - if (state === 'idle') return 'running'; - return state || 'stopped'; -} - -function statusClass(state) { - if (state === 'online' || state === 'running' || state === 'idle' || state === 'idle') return 'online'; - if (state === 'starting' || state === 'reconnecting') return 'starting'; - return 'offline'; -} - -function isUnsupportedAgent(agent) { - return !!agent?.runtimeMismatch; -} - -// ---- D25: Activity log ---- - -const activityEntries = []; -const MAX_ACTIVITY = 50; - -function addActivity(msg) { - const now = new Date(); - const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - activityEntries.unshift({ time, msg }); - if (activityEntries.length > MAX_ACTIVITY) activityEntries.length = MAX_ACTIVITY; - renderActivity(); -} - -function renderActivity() { - const el = document.getElementById('activity-log'); - if (!el) return; - if (activityEntries.length === 0) { - el.innerHTML = 'No activity yet. Start an agent to see events.'; - return; - } - el.innerHTML = activityEntries.map(e => - `
${esc(e.time)}${esc(e.msg)}
` - ).join(''); -} - -// Override showToast to also add to activity log -const _origShowToast = showToast; -showToast = function(message, type) { - _origShowToast(message, type); - addActivity(message); -}; - -// ---- D28: Auto-refresh logs ---- - -let logAutoRefreshInterval = null; - -function startLogAutoRefresh() { - stopLogAutoRefresh(); - logAutoRefreshInterval = setInterval(() => { - const autoEl = document.getElementById('log-auto-refresh'); - const activeTab = document.querySelector('.nav-item.active'); - if (autoEl && autoEl.checked && activeTab && activeTab.dataset.tab === 'logs') { - refreshLogs(); - } - }, 3000); -} - -function stopLogAutoRefresh() { - if (logAutoRefreshInterval) { clearInterval(logAutoRefreshInterval); logAutoRefreshInterval = null; } -} - -startLogAutoRefresh(); - -// ---- D29: Workspace URL display in Settings ---- - -async function refreshSettingsWorkspaces() { - if (_currentTab !== 'settings') return; - const el = document.getElementById('settings-workspaces'); - if (!el) return; - try { - const workspaces = await window.api.listWorkspaces(); - if (_currentTab !== 'settings') return; - if (!workspaces || workspaces.length === 0) { - el.innerHTML = 'No workspaces configured.'; - return; - } - el.innerHTML = `
    ${workspaces.map(w => { - const slug = w.slug || w.id; - const name = w.name || slug; - const url = `https://workspace.openagents.org/${slug}`; - return `
  • - ${esc(name)} - ${esc(url)} - -
  • `; - }).join('')}
`; - } catch { - el.innerHTML = 'Failed to load workspaces.'; - } -} - -async function refreshSettingsRuntime() { - if (_currentTab !== 'settings') return; - try { - const info = await window.api.runtimeInfo(); - if (_currentTab !== 'settings') return; - const set = (id, value, color) => { - const el = document.getElementById(id); - if (el) { el.textContent = value; el.style.color = color; } - }; - set('settings-nodejs-version', info.nodeVersion || 'Not installed', info.nodeVersion ? 'var(--success)' : 'var(--danger)'); - set('settings-npm-version', info.npmVersion ? `v${info.npmVersion}` : 'Not installed', info.npmVersion ? 'var(--success)' : 'var(--danger)'); - set('settings-core-version', info.coreVersion ? `v${info.coreVersion}` : 'Not installed', info.coreVersion ? 'var(--success)' : 'var(--danger)'); - if (info.latestVersion) { - const upToDate = info.coreVersion === info.latestVersion; - set('settings-core-latest', `v${info.latestVersion}${upToDate ? ' (up to date)' : ' (update available)'}`, upToDate ? 'var(--success)' : 'var(--warning)'); - } else { - set('settings-core-latest', 'Unable to check', 'var(--text-muted)'); - } - } catch {} -} - -// ---- Update About version ---- - -(async () => { - try { - const status = await window.api.pythonStatus(); - const aboutEl = document.getElementById('about-version'); - if (aboutEl) aboutEl.textContent = `v${status.sdkVersion}`; - } catch {} -})(); - -// ---- Periodic refresh ---- - -setInterval(() => { - const activeTab = document.querySelector('.nav-item.active'); - if (activeTab) { - const tab = activeTab.dataset.tab; - if (tab === 'dashboard') scheduleRefreshDashboard(); - if (tab === 'settings') { refreshSettingsWorkspaces(); refreshSettingsRuntime(); } - } - updateDaemonStatus(); -}, 5000); - -// ---- Core library update banner ---- -if (window.api.onCoreUpdate) { - window.api.onCoreUpdate(({ current, latest }) => { - const banner = document.getElementById('update-banner'); - if (!banner) return; - banner.style.display = 'block'; - banner.innerHTML = ` -
-
Update available
-
v${current} → v${latest}
- -
`; - document.getElementById('btn-update-core').addEventListener('click', async () => { - const btn = document.getElementById('btn-update-core'); - btn.textContent = 'Updating...'; - btn.disabled = true; - try { - const result = await window.api.updateCore(); - if (result.success) { - banner.innerHTML = `
Updated to v${result.version}
`; - document.getElementById('sdk-version').textContent = 'Core: v' + result.version; - setTimeout(() => { banner.style.display = 'none'; }, 5000); - } else { - showToast('Update failed: ' + result.error, 'error'); - btn.textContent = 'Retry'; - btn.disabled = false; - } - } catch (err) { - showToast('Update failed: ' + err.message, 'error'); - btn.textContent = 'Retry'; - btn.disabled = false; - } - }); - }); -} - -// ---- Delegated click handler ---- -// CSP blocks inline onclick; use data-action attributes + delegation instead. - -document.addEventListener('click', (e) => { - const btn = e.target.closest('[data-action]'); - if (!btn) return; - if (btn.disabled) return; - - const action = btn.dataset.action; - const name = btn.dataset.name || ''; - const type = btn.dataset.type || ''; - const state = btn.dataset.state || ''; - const network = btn.dataset.network || ''; - const slug = btn.dataset.slug || ''; - const tab = btn.dataset.actionTab || ''; - - // Close modal for actions triggered from inside a modal - const inModal = !!btn.closest('.modal'); - const autoClose = ['switch-tab', 'toggle-agent', 'configure', 'disconnect', - 'open-ws', 'remove-agent', 'connect-workspace', 'do-connect-workspace', - 'show-create-workspace', 'show-join-token']; - if (inModal && autoClose.includes(action)) closeModal(); - - switch (action) { - case 'switch-tab': switchTab(tab); break; - case 'toggle-agent': toggleAgent(name, state); break; - case 'show-agent-actions': showAgentActions(name, type, state, network); break; - case 'configure': openConfigureScreen(name, type); break; - case 'disconnect': disconnectAgent(name); break; - case 'open-ws': openWorkspaceInBrowser(name); break; - case 'remove-agent': removeAgent(name); break; - case 'connect-workspace': showConnectWorkspace(name); break; - case 'do-connect-workspace': doConnectWorkspace(name, slug); break; - case 'show-create-workspace': showCreateWorkspace(name); break; - case 'show-join-token': showJoinWithToken(name); break; - case 'do-create-workspace': doCreateWorkspace(name); break; - case 'do-join-token': doJoinWithToken(name); break; - case 'remove-workspace': removeWorkspace(slug); break; - case 'do-add-agent': doAddAgent(); break; - case 'save-config': saveConfig(name, type); break; - case 'test-llm': testLLMConfig(type); break; - case 'close-modal': closeModal(); break; - case 'install-catalog': installCatalogItem(name, btn.dataset.installed === 'true'); break; - case 'uninstall-catalog': uninstallCatalogItem(name); break; - case 'open-external': window.api.openExternal(btn.dataset.url); break; - // D23: Login flow - case 'agent-login': agentLogin(type); break; - // D24: Daemon toggle - case 'toggle-daemon': toggleDaemon(); break; - } -}); - -// ---- D23: Agent login flow ---- - -async function agentLogin(agentType) { - let cmd = null; - try { - const catalog = await window.api.getCatalog(); - const entry = catalog.find((c) => c.name === agentType); - cmd = entry?.check_ready?.login_command || null; - } catch {} - if (!cmd) { - const loginCommands = { - claude: 'claude login', - openclaw: 'openclaw login', - codex: 'codex login', - copilot: 'github-copilot login', - }; - cmd = loginCommands[agentType]; - } - if (!cmd) { - showToast(`No login command for ${agentType}. Configure API key instead.`, 'info'); - openConfigureScreen('', agentType); - return; - } - showToast(`Opening ${agentType} login... Follow the prompts in the terminal.`, 'info'); - try { - await window.api.openTerminal(cmd); - showToast(`Login terminal opened. Complete login there, then return here.`, 'success'); - } catch (err) { - showToast(`Failed to open terminal: ${err.message}`, 'error'); - } -} - -// ---- D24: Daemon toggle ---- - -async function toggleDaemon() { - const el = document.getElementById('daemon-status'); - const isRunning = el && el.textContent.includes('running'); - try { - if (isRunning) { - await window.api.stopAll(); - showToast('Daemon stopped', 'info'); - } else { - await window.api.startAll(); - showToast('Daemon starting...', 'info'); - } - setTimeout(() => scheduleRefreshDashboard(), 2000); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -// ---- Workspace Deletion ---- - -async function removeWorkspace(slug) { - if (!confirm(`This will remove the workspace locally and attempt to soft-delete it on the server.\nConnected agents will be disconnected.\n\nAre you sure you want to proceed?`)) return; - try { - showToast(`Removing workspace...`, 'info'); - await window.api.removeWorkspace(slug); - showToast(`Workspace removed`, 'success'); - refreshSettingsWorkspaces(); - refreshAgentList(); - refreshDashboard(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -// ---- Initial load ---- - -scheduleRefreshDashboard(); -renderActivity(); - diff --git a/packages/launcher/src/renderer/store/agents.ts b/packages/launcher/src/renderer/store/agents.ts new file mode 100644 index 000000000..b9b8585ba --- /dev/null +++ b/packages/launcher/src/renderer/store/agents.ts @@ -0,0 +1,55 @@ +import { create } from 'zustand' +import { useShallow } from 'zustand/react/shallow' +import type { Agent } from '../types' + +interface AgentsState { + // Agent list — replaces legacy module-level agent array + agents: Agent[] + setAgents: (agents: Agent[]) => void + + // Pending start/stop — replaces legacy _pendingAgentActions Set + pendingAgentActions: Set + addPendingAction: (name: string) => void + removePendingAction: (name: string) => void + + // Version info (fetched alongside agent list) + coreVersion: string | null + setCoreVersion: (v: string | null) => void + launcherVersion: string | null + setLauncherVersion: (v: string | null) => void + + // Core update banner + coreUpdateInfo: { current: string; latest: string } | null + setCoreUpdateInfo: (info: { current: string; latest: string } | null) => void +} + +export const useAgentsStore = create((set) => ({ + agents: [], + setAgents: (agents) => set({ agents }), + + pendingAgentActions: new Set(), + addPendingAction: (name) => + set((state) => ({ pendingAgentActions: new Set(state.pendingAgentActions).add(name) })), + removePendingAction: (name) => + set((state) => { + const next = new Set(state.pendingAgentActions) + next.delete(name) + return { pendingAgentActions: next } + }), + + coreVersion: null, + setCoreVersion: (v) => set({ coreVersion: v }), + launcherVersion: null, + setLauncherVersion: (v) => set({ launcherVersion: v }), + + coreUpdateInfo: null, + setCoreUpdateInfo: (info) => set({ coreUpdateInfo: info }), +})) + +/** Derived selector — computed from agent states, no extra polling needed */ +export function useDaemonStatus(): 'online' | 'offline' | 'starting' { + const agents = useAgentsStore(useShallow((s) => s.agents)) + if (agents.some((a) => ['online', 'running', 'idle'].includes(a.state))) return 'online' + if (agents.some((a) => ['starting', 'reconnecting'].includes(a.state))) return 'starting' + return 'offline' +} diff --git a/packages/launcher/src/renderer/store/catalog.ts b/packages/launcher/src/renderer/store/catalog.ts new file mode 100644 index 000000000..5e2a42534 --- /dev/null +++ b/packages/launcher/src/renderer/store/catalog.ts @@ -0,0 +1,20 @@ +import { create } from 'zustand' +import type { CatalogEntry } from '../types' + +interface CatalogState { + // Full catalog — shared by Install page and NewAgent dialog + catalog: CatalogEntry[] + setCatalog: (catalog: CatalogEntry[]) => void + + // Supported types returned by getSupportedAgentTypes() + supportedTypes: string[] + setSupportedTypes: (types: string[]) => void +} + +export const useCatalogStore = create((set) => ({ + catalog: [], + setCatalog: (catalog) => set({ catalog }), + + supportedTypes: [], + setSupportedTypes: (types) => set({ supportedTypes: types }), +})) diff --git a/packages/launcher/src/renderer/store/index.ts b/packages/launcher/src/renderer/store/index.ts new file mode 100644 index 000000000..a9dc1b97b --- /dev/null +++ b/packages/launcher/src/renderer/store/index.ts @@ -0,0 +1,6 @@ +export { useUiStore } from './ui' +export { useAgentsStore, useDaemonStatus } from './agents' +export { useCatalogStore } from './catalog' +export { useWorkspacesStore } from './workspaces' +export { useLogsStore } from './logs' +export { useSettingsStore } from './settings' diff --git a/packages/launcher/src/renderer/store/install.ts b/packages/launcher/src/renderer/store/install.ts new file mode 100644 index 000000000..543e84916 --- /dev/null +++ b/packages/launcher/src/renderer/store/install.ts @@ -0,0 +1,78 @@ +import { create } from 'zustand' +import type { AgentUpdateInfo, InstallPhase, InstalledAgentRecord } from '../types' + +export interface InstallJob { + agent: string + verb: 'install' | 'update' | 'uninstall' | 'rollback' + phase: InstallPhase + detail: string + log: string + error?: string + startedAt: number +} + +interface InstallState { + jobs: Record + startJob: (job: Omit & { + phase?: InstallPhase + detail?: string + }) => void + updateJob: (agent: string, patch: Partial) => void + appendLog: (agent: string, chunk: string) => void + clearJob: (agent: string) => void + + installed: InstalledAgentRecord[] + setInstalled: (recs: InstalledAgentRecord[]) => void + + updates: AgentUpdateInfo[] + setUpdates: (updates: AgentUpdateInfo[]) => void +} + +export const useInstallStore = create((set) => ({ + jobs: {}, + startJob: (j) => + set((state) => ({ + jobs: { + ...state.jobs, + [j.agent]: { + agent: j.agent, + verb: j.verb, + phase: j.phase || 'preparing', + detail: j.detail || 'Starting…', + log: '', + startedAt: Date.now(), + }, + }, + })), + updateJob: (agent, patch) => + set((state) => { + const existing = state.jobs[agent] + if (!existing) return state + return { jobs: { ...state.jobs, [agent]: { ...existing, ...patch } } } + }), + appendLog: (agent, chunk) => + set((state) => { + const existing = state.jobs[agent] + if (!existing) return state + const next = (existing.log + chunk).slice(-20000) + return { jobs: { ...state.jobs, [agent]: { ...existing, log: next } } } + }), + clearJob: (agent) => + set((state) => { + const next = { ...state.jobs } + delete next[agent] + return { jobs: next } + }), + + installed: [], + setInstalled: (recs) => set({ installed: recs }), + + updates: [], + setUpdates: (updates) => set({ updates }), +})) + +export function hasPendingUpdate(updates: AgentUpdateInfo[], name: string): boolean { + const info = updates.find((u) => u.name === name) + if (!info || !info.current || !info.latest) return false + return info.current !== info.latest +} diff --git a/packages/launcher/src/renderer/store/logs.ts b/packages/launcher/src/renderer/store/logs.ts new file mode 100644 index 000000000..5da3b9c20 --- /dev/null +++ b/packages/launcher/src/renderer/store/logs.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand' + +interface LogsState { + // Active agent filter — replaces legacy _logsFilter + agentFilter: string + setAgentFilter: (filter: string) => void + + // File read position — replaces legacy _logsOffset (persisted across tab switches) + logsOffset: number + setLogsOffset: (offset: number) => void + resetLogsOffset: () => void + + // Clear operation guard — replaces legacy _clearLogsInFlight + clearInFlight: boolean + setClearInFlight: (v: boolean) => void +} + +export const useLogsStore = create((set) => ({ + agentFilter: '', + setAgentFilter: (filter) => set({ agentFilter: filter, logsOffset: 0 }), + + logsOffset: 0, + setLogsOffset: (offset) => set({ logsOffset: offset }), + resetLogsOffset: () => set({ logsOffset: 0 }), + + clearInFlight: false, + setClearInFlight: (v) => set({ clearInFlight: v }), +})) diff --git a/packages/launcher/src/renderer/store/settings.ts b/packages/launcher/src/renderer/store/settings.ts new file mode 100644 index 000000000..7ceb89b72 --- /dev/null +++ b/packages/launcher/src/renderer/store/settings.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand' +import type { RuntimeInfo } from '../types' + +interface SettingsState { + startOnBoot: boolean + setStartOnBoot: (v: boolean) => void + + minimizeToTray: boolean + setMinimizeToTray: (v: boolean) => void + + runtimeInfo: RuntimeInfo | null + setRuntimeInfo: (info: RuntimeInfo | null) => void +} + +export const useSettingsStore = create((set) => ({ + startOnBoot: false, + setStartOnBoot: (v) => set({ startOnBoot: v }), + + minimizeToTray: false, + setMinimizeToTray: (v) => set({ minimizeToTray: v }), + + runtimeInfo: null, + setRuntimeInfo: (info) => set({ runtimeInfo: info }), +})) diff --git a/packages/launcher/src/renderer/store/ui.ts b/packages/launcher/src/renderer/store/ui.ts new file mode 100644 index 000000000..066259fe0 --- /dev/null +++ b/packages/launcher/src/renderer/store/ui.ts @@ -0,0 +1,55 @@ +import { create } from 'zustand' + +interface ActivityEntry { + time: string + msg: string +} + +interface UiState { + // Active tab — replaces legacy _currentTab + currentTab: string + setCurrentTab: (tab: string) => void + + // Deep-link request: when set, the Install page should auto-open this agent's + // detail view (used by Dashboard banner click and tray-menu update items). + installFocusAgent: string | null + setInstallFocusAgent: (name: string | null) => void + + // Bumped each time the user explicitly clicks the Install sidebar tab. + // The Install page watches this and clears any open detail view so the + // user always lands on the marketplace list when entering via the tab. + installListSignal: number + goToInstallList: () => void + + // Activity log — replaces legacy activityEntries[] + activityLog: ActivityEntry[] + addActivity: (msg: string) => void + + // Cached icons directory path — replaces legacy _coreIconsDir + coreIconsDir: string | null + setCoreIconsDir: (dir: string | null) => void +} + +export const useUiStore = create((set) => ({ + currentTab: 'dashboard', + setCurrentTab: (tab) => set({ currentTab: tab }), + + installFocusAgent: null, + setInstallFocusAgent: (name) => set({ installFocusAgent: name }), + + installListSignal: 0, + goToInstallList: () => + set((s) => ({ currentTab: 'install', installListSignal: s.installListSignal + 1 })), + + activityLog: [], + addActivity: (msg) => { + const now = new Date() + const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + set((state) => ({ + activityLog: [{ time, msg }, ...state.activityLog].slice(0, 50), + })) + }, + + coreIconsDir: null, + setCoreIconsDir: (dir) => set({ coreIconsDir: dir }), +})) diff --git a/packages/launcher/src/renderer/store/workspaces.ts b/packages/launcher/src/renderer/store/workspaces.ts new file mode 100644 index 000000000..12bc564f9 --- /dev/null +++ b/packages/launcher/src/renderer/store/workspaces.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand' +import type { Workspace } from '../types' + +interface WorkspacesState { + // Workspace list — shared between Agents page and Settings page + workspaces: Workspace[] + setWorkspaces: (workspaces: Workspace[]) => void +} + +export const useWorkspacesStore = create((set) => ({ + workspaces: [], + setWorkspaces: (workspaces) => set({ workspaces }), +})) diff --git a/packages/launcher/src/renderer/styles.css b/packages/launcher/src/renderer/styles.css deleted file mode 100644 index 4b11092db..000000000 --- a/packages/launcher/src/renderer/styles.css +++ /dev/null @@ -1,1182 +0,0 @@ -/* OpenAgents Connector — Light Theme - * Clean, polished, Things-inspired - */ - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - --bg-primary: #f2f2f7; - --bg-secondary: #ffffff; - --bg-card: #ffffff; - --bg-card-hover: #fafafa; - --bg-input: #eeeef0; - --bg-sidebar: #ffffff; - - --text-primary: #1c1c1e; - --text-secondary: #636366; - --text-tertiary: #aeaeb2; - --text-link: #5856d6; - - --accent: #5856d6; - --accent-hover: #4a48c4; - --accent-bg: rgba(88, 86, 214, 0.06); - --accent-border: rgba(88, 86, 214, 0.15); - --accent-text: #ffffff; - - --success: #30d158; - --success-bg: rgba(48, 209, 88, 0.08); - --success-text: #248a3d; - --warning: #ff9f0a; - --warning-bg: rgba(255, 159, 10, 0.08); - --warning-text: #c77c0a; - --danger: #ff3b30; - --danger-bg: rgba(255, 59, 48, 0.06); - --danger-text: #d70015; - - --border: rgba(0, 0, 0, 0.06); - --border-hover: rgba(0, 0, 0, 0.12); - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04); - --shadow-md: 0 2px 10px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04); - --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.12), 0 4px 12px rgba(0, 0, 0, 0.06); - --radius: 12px; - --radius-sm: 8px; - --radius-lg: 16px; - - --ease: cubic-bezier(0.25, 0.1, 0.25, 1); -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - overflow: hidden; - height: 100vh; - -webkit-font-smoothing: antialiased; - font-size: 13px; - line-height: 1.5; - letter-spacing: -0.01em; -} - -#app { - display: flex; - height: 100vh; -} - -/* ============================================ - Sidebar — Things-style minimal - ============================================ */ -#sidebar { - width: 210px; - background: var(--bg-sidebar); - border-right: 1px solid var(--border); - display: flex; - flex-direction: column; - -webkit-app-region: drag; - user-select: none; -} - -.sidebar-header { - padding: 24px 20px 16px; -} - -.sidebar-header h2 { - font-size: 15px; - font-weight: 700; - letter-spacing: -0.02em; - color: var(--text-primary); -} - -.sidebar-header .version { - font-size: 11px; - color: var(--text-tertiary); - font-weight: 500; - margin-top: 1px; -} - -.nav-items { - list-style: none; - padding: 0 10px; - flex: 1; - -webkit-app-region: no-drag; -} - -.nav-item { - padding: 8px 12px; - margin-bottom: 2px; - cursor: pointer; - color: var(--text-secondary); - font-size: 13px; - font-weight: 500; - transition: all 0.18s var(--ease); - display: flex; - align-items: center; - gap: 10px; - border-radius: var(--radius-sm); -} - -.nav-item:hover { - background: var(--bg-primary); - color: var(--text-primary); -} - -.nav-item.active { - background: var(--accent); - color: #ffffff; - box-shadow: 0 2px 6px rgba(88, 86, 214, 0.25); -} - -.nav-icon { - font-size: 14px; - width: 18px; - text-align: center; - opacity: 0.55; -} - -.nav-item.active .nav-icon { - opacity: 1; -} - -.sidebar-footer { - padding: 14px 18px; - border-top: 1px solid var(--border); -} - -.version-info { - display: flex; - flex-direction: column; - gap: 2px; - margin-bottom: 10px; -} - -.version-line { - font-size: 10px; - color: var(--text-tertiary); - opacity: 0.7; -} - -.daemon-status { - display: flex; - align-items: center; - gap: 8px; - font-size: 11px; - font-weight: 500; - color: var(--text-tertiary); -} - -.status-dot { - width: 7px; - height: 7px; - border-radius: 50%; - display: inline-block; - flex-shrink: 0; -} - -.status-dot.online { - background: var(--success); - box-shadow: 0 0 0 3px rgba(48, 209, 88, 0.15); -} - -.status-dot.offline { - background: var(--text-tertiary); -} - -.status-dot.starting { - background: var(--warning); - animation: pulse 1.5s infinite; -} - -/* ============================================ - Main Content - ============================================ */ -#content { - flex: 1; - overflow-y: auto; - padding: 32px 36px; - background: var(--bg-primary); -} - -.tab-content { - display: none; - animation: fadeIn 0.25s var(--ease); -} - -.tab-content.active { - display: block; -} - -h1 { - font-size: 22px; - font-weight: 700; - margin-bottom: 24px; - letter-spacing: -0.03em; - color: var(--text-primary); -} - -/* ============================================ - Cards - ============================================ */ -.card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 18px 20px; - margin-bottom: 12px; - box-shadow: var(--shadow-sm); - transition: box-shadow 0.18s var(--ease), border-color 0.18s var(--ease); -} - -.card:hover { - box-shadow: var(--shadow-md); - border-color: var(--border-hover); -} - -.card h3 { - font-size: 13px; - font-weight: 600; - margin-bottom: 12px; - color: var(--text-primary); -} - -.card-grid { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 20px; -} - -.agent-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px 18px; - box-shadow: var(--shadow-sm); - transition: all 0.18s var(--ease); -} - -.agent-card:hover { - box-shadow: var(--shadow-md); - border-color: var(--border-hover); -} - -.agent-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - gap: 12px; -} - -.agent-icon { - flex-shrink: 0; - border-radius: 6px; -} - -.agent-card-name { - font-weight: 600; - font-size: 14px; - letter-spacing: -0.01em; -} - -.agent-card-type { - font-size: 11px; - font-weight: 500; - color: var(--text-tertiary); - background: var(--bg-input); - padding: 2px 10px; - border-radius: 20px; -} - -.agent-card-status { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text-secondary); - margin-bottom: 12px; -} - -.agent-card-error { - font-size: 11px; - color: var(--danger-text); - margin-bottom: 10px; - padding: 8px 12px; - background: var(--danger-bg); - border-radius: var(--radius-sm); - line-height: 1.4; -} - -.agent-card-actions { - display: flex; - gap: 8px; -} - -.empty-state { - text-align: center; - padding: 48px 24px; - color: var(--text-tertiary); -} - -.empty-state p { - margin-bottom: 16px; - font-size: 13px; -} - -/* ============================================ - Buttons — Things-style - ============================================ */ -.btn { - padding: 7px 16px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-card); - color: var(--text-primary); - cursor: pointer; - font-size: 12px; - font-weight: 500; - transition: all 0.15s var(--ease); - line-height: 1.4; - box-shadow: var(--shadow-sm); -} - -.btn:hover { - border-color: var(--border-hover); - box-shadow: var(--shadow-md); - background: var(--bg-card-hover); -} - -.btn:active { - transform: scale(0.97); - box-shadow: none; -} - -.btn-primary { - background: var(--accent); - border-color: transparent; - color: var(--accent-text); - font-weight: 600; - box-shadow: 0 1px 4px rgba(88, 86, 214, 0.2); -} - -.btn-primary:hover { - background: var(--accent-hover); - border-color: transparent; - box-shadow: 0 3px 10px rgba(88, 86, 214, 0.25); -} - -.btn-danger { - border-color: rgba(255, 59, 48, 0.2); - color: var(--danger-text); - background: var(--bg-card); -} - -.btn-danger:hover { - background: var(--danger-bg); - border-color: rgba(255, 59, 48, 0.35); -} - -.btn-sm { - padding: 5px 12px; - font-size: 11px; -} - -.btn:disabled { - opacity: 0.35; - cursor: not-allowed; - transform: none; -} - -/* ============================================ - Forms - ============================================ */ -.form-card { - max-width: 480px; -} - -.form-group { - margin-bottom: 16px; -} - -.form-group label { - display: block; - font-size: 11px; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 6px; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.form-group input[type="text"], -.form-group input[type="password"], -.form-group select { - width: 100%; - padding: 9px 14px; - background: var(--bg-input); - border: 1px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 13px; - outline: none; - transition: all 0.18s var(--ease); -} - -.form-group input:focus, -.form-group select:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-bg); - background: #ffffff; -} - -.form-group input[type="checkbox"] { - margin-right: 8px; - accent-color: var(--accent); -} - -.form-actions { - display: flex; - gap: 8px; - margin-top: 18px; -} - -.required { - color: var(--danger-text); -} - -.hint { - font-size: 12px; - color: var(--text-tertiary); - margin-bottom: 12px; -} - -/* ============================================ - Status Rows - ============================================ */ -.status-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 11px 0; - font-size: 13px; - border-bottom: 1px solid var(--border); -} - -.status-row:last-of-type { - border-bottom: none; - margin-bottom: 8px; -} - -/* ============================================ - Banners - ============================================ */ -.banner { - padding: 14px 18px; - border-radius: var(--radius); - margin-bottom: 18px; - font-size: 13px; -} - -.banner.warning { - background: var(--warning-bg); - border: 1px solid rgba(255, 159, 10, 0.15); - color: var(--warning-text); -} - -.banner a { - color: var(--accent); - cursor: pointer; - text-decoration: none; - font-weight: 500; -} - -.banner a:hover { - text-decoration: underline; -} - -/* ============================================ - Actions Bar - ============================================ */ -.actions-bar { - display: flex; - gap: 10px; -} - -/* ============================================ - Agents Toolbar & List - ============================================ */ -.agents-toolbar { - display: flex; - gap: 10px; - margin-bottom: 16px; -} - -.agent-list-item { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px 18px; - margin-bottom: 10px; - display: flex; - flex-direction: column; - gap: 12px; - box-shadow: var(--shadow-sm); - transition: all 0.18s var(--ease); -} - -.agent-list-top { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; -} - -.agent-list-status { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 4px; - flex-shrink: 0; -} - -.agent-state-text { - font-size: 13px; - font-weight: 600; -} - -.agent-ws-label { - font-size: 11px; - color: var(--text-secondary); -} - -.agent-ws-label.muted { - color: var(--text-tertiary); -} - -.agent-list-name-row { - display: flex; - align-items: center; - gap: 10px; -} - -.agent-list-name-row h4 { - margin: 0; -} - -.agent-type-label { - font-size: 12px; - color: var(--text-secondary); - display: block; - margin-bottom: 2px; -} - -.agent-list-bottom { - display: flex; - justify-content: space-between; - align-items: center; - padding-top: 10px; - border-top: 1px solid var(--border); -} - -.agent-list-item:hover { - box-shadow: var(--shadow-md); - border-color: var(--border-hover); -} - -.agent-list-info { - flex: 1; - min-width: 0; -} - -.agent-list-info h4 { - font-size: 14px; - font-weight: 600; - margin-bottom: 4px; -} - -.agent-list-info span { - font-size: 12px; - color: var(--text-secondary); - display: block; - margin-bottom: 2px; -} - -.agent-config-hint { - font-size: 11px !important; - color: var(--text-tertiary) !important; -} - -.agent-error { - font-size: 11px !important; - color: var(--danger-text) !important; -} - -.text-warning { - color: var(--warning-text); -} - -.text-danger { - color: var(--danger-text); -} - -.agent-list-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - justify-content: flex-end; -} - - -/* ============================================ - Logs - ============================================ */ -.log-controls { - display: flex; - gap: 10px; - margin-bottom: 12px; - align-items: center; -} - -.log-controls select { - padding: 7px 12px; - background: var(--bg-input); - border: 1px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 12px; -} - -.log-viewer { - background: #1a1a1e; - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px 18px; - font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace; - font-size: 11.5px; - line-height: 1.65; - max-height: calc(100vh - 200px); - overflow-y: auto; - white-space: pre-wrap; - word-break: break-all; - color: #d4d4d4; - box-shadow: var(--shadow-md); -} - -/* ============================================ - Install Tab - ============================================ */ -.install-output { - margin-top: 12px; - background: #1a1a1e; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 14px 16px; - font-family: 'SF Mono', monospace; - font-size: 11.5px; - max-height: 200px; - overflow-y: auto; - white-space: pre-wrap; - color: #d4d4d4; -} - -/* ============================================ - Catalog — Things-style list - ============================================ */ -.catalog-search { - margin-bottom: 14px; -} - -.catalog-search input { - width: 100%; - padding: 9px 14px; - background: var(--bg-input); - border: 1px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 13px; - outline: none; - transition: all 0.18s var(--ease); -} - -.catalog-search input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-bg); - background: #ffffff; -} - -.catalog-list { - display: flex; - flex-direction: column; - gap: 2px; -} - -.catalog-row { - display: flex; - align-items: center; - gap: 14px; - padding: 12px 16px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - transition: all 0.15s var(--ease); - box-shadow: var(--shadow-sm); -} - -.catalog-row:hover { - box-shadow: var(--shadow-md); - border-color: var(--border-hover); -} - -.catalog-row.installed { - opacity: 1; -} - -.catalog-info { - flex: 1; - min-width: 0; - display: flex; - align-items: center; - gap: 14px; -} - -.catalog-icon { - width: 32px; - height: 32px; - flex-shrink: 0; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-input); - padding: 4px; -} - -.catalog-icon img { - width: 22px; - height: 22px; - object-fit: contain; -} - -.catalog-text { - flex: 1; - min-width: 0; -} - -.catalog-name { - font-weight: 600; - font-size: 13px; - display: block; - color: var(--text-primary); -} - -.catalog-desc { - font-size: 11px; - color: var(--text-tertiary); - display: block; - margin-top: 1px; -} - -.catalog-status { - flex-shrink: 0; -} - -.catalog-actions { - flex-shrink: 0; - display: flex; - gap: 6px; -} - -.badge { - font-size: 10px; - font-weight: 600; - padding: 3px 10px; - border-radius: 20px; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -.badge-success { - background: var(--success-bg); - color: var(--success-text); -} - -.badge-warning { - background: var(--warning-bg); - color: var(--warning-text); -} - -.badge-info { - background: #e0e7ff; - color: #3730a3; -} - -.support-icons { - display: inline-flex; - gap: 4px; - margin-top: 2px; -} - -.support-icon { - font-size: 11px; - line-height: 1; -} - -.support-icon.on { - opacity: 1; -} - -.support-icon.off { - opacity: 0.2; -} - -.badge-success-sm, .badge-warning-sm, .badge-muted-sm, .badge-danger-sm { - font-size: 10px; - padding: 2px 6px; - border-radius: 4px; - font-weight: 500; -} -.badge-success-sm { background: var(--success-bg); color: var(--success-text); } -.badge-warning-sm { background: var(--warning-bg); color: var(--warning-text); } -.badge-muted-sm { background: #f0f0f0; color: #888; } -.badge-danger-sm { background: var(--danger-bg); color: var(--danger-text); } - -.agent-card-info { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin: 6px 0; -} - -.loading-text { - color: var(--text-tertiary); - font-size: 13px; - padding: 24px 0; -} - -.skeleton-card, -.skeleton-list-item { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px 18px; - box-shadow: var(--shadow-sm); - margin-bottom: 10px; -} - -.skeleton-list { - display: flex; - flex-direction: column; - gap: 10px; -} - -.skeleton-line { - height: 10px; - border-radius: 999px; - margin-bottom: 10px; - background: linear-gradient(90deg, #ececf1 25%, #f6f6f8 50%, #ececf1 75%); - background-size: 200% 100%; - animation: skeletonShimmer 1.1s linear infinite; -} - -.skeleton-line-lg { width: 62%; } -.skeleton-line-md { width: 42%; } -.skeleton-line-sm { width: 26%; margin-bottom: 0; } - -@keyframes skeletonShimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -/* ============================================ - Modal — Frosted glass - ============================================ */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.2); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - animation: fadeIn 0.15s var(--ease); -} - -.modal { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 28px; - min-width: 400px; - max-width: 520px; - max-height: 80vh; - overflow-y: auto; - box-shadow: var(--shadow-lg); - animation: modalIn 0.22s var(--ease); -} - -.modal h3 { - font-size: 17px; - font-weight: 700; - margin-bottom: 20px; - letter-spacing: -0.02em; -} - -.modal .form-group { - margin-bottom: 16px; -} - -.modal .form-group label { - font-size: 11px; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 6px; - display: block; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.modal .form-group input, -.modal .form-group select { - width: 100%; - padding: 9px 14px; - background: var(--bg-input); - border: 1px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 13px; - outline: none; - transition: all 0.18s var(--ease); -} - -.modal .form-group input:focus, -.modal .form-group select:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-bg); - background: #ffffff; -} - -.modal .form-group input[type="datetime-local"] { - min-height: 40px; -} - -.modal-button-row { - display: flex; - gap: 8px; - margin-top: 20px; -} - -.modal-actions-list { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 14px; -} - -.modal-action-btn { - text-align: left; - padding: 11px 16px; - font-size: 13px; - width: 100%; - border-radius: var(--radius-sm); -} - -.modal-action-btn:hover { - background: var(--accent-bg); - border-color: var(--accent-border); -} - -.modal-close-btn { - margin-top: 10px; - width: 100%; - text-align: center; -} - -.configure-form { - margin-bottom: 14px; -} - -#test-result { - min-height: 20px; - margin-bottom: 10px; - font-size: 12px; -} - -.test-loading { color: var(--text-secondary); } -.test-success { color: var(--success-text); } -.test-error { color: var(--danger-text); } - -/* ============================================ - Scrollbar — subtle - ============================================ */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.1); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(0, 0, 0, 0.18); -} - -/* ============================================ - Animations - ============================================ */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes modalIn { - from { opacity: 0; transform: scale(0.97) translateY(8px); } - to { opacity: 1; transform: scale(1) translateY(0); } -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* ============================================ - D25: Activity log panel - ============================================ */ -.activity-log { - max-height: 180px; - overflow-y: auto; - font-size: 11.5px; - line-height: 1.6; - color: var(--text-secondary); -} - -.activity-log-entry { - padding: 3px 0; - border-bottom: 1px solid var(--border); - display: flex; - gap: 8px; -} - -.activity-log-entry:last-child { - border-bottom: none; -} - -.activity-log-time { - color: var(--text-tertiary); - font-size: 10px; - flex-shrink: 0; - min-width: 50px; -} - -.activity-log-msg { - flex: 1; - min-width: 0; - word-break: break-word; -} - -/* ============================================ - D26: Responsive agent cards - ============================================ */ -@media (min-width: 700px) { - .card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - } -} - -@media (max-width: 500px) { - #sidebar { - width: 54px; - } - .sidebar-header h2, - .sidebar-header .version, - .nav-item span:not(.nav-icon) { - display: none; - } - .nav-item { justify-content: center; } - .nav-icon { margin: 0; } - #content { padding: 20px 16px; } - .agent-card-header { flex-wrap: wrap; } - .agent-list-item { flex-direction: column; } - .agent-list-actions { justify-content: flex-start; } -} - -/* ============================================ - D27: Loading spinner - ============================================ */ -.spinner { - display: inline-block; - width: 16px; - height: 16px; - border: 2px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 0.6s linear infinite; - vertical-align: middle; - margin-right: 6px; -} - -.btn-loading { - pointer-events: none; - opacity: 0.7; -} - -.btn-loading::before { - content: ''; - display: inline-block; - width: 12px; - height: 12px; - border: 2px solid rgba(255,255,255,0.3); - border-top-color: #fff; - border-radius: 50%; - animation: spin 0.6s linear infinite; - margin-right: 6px; - vertical-align: middle; -} - -/* ============================================ - D29: Workspace URL in Settings - ============================================ */ -.workspace-url-list { - list-style: none; - padding: 0; -} - -.workspace-url-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - border-bottom: 1px solid var(--border); - font-size: 12px; -} - -.workspace-url-item:last-child { - border-bottom: none; -} - -.workspace-url-name { - font-weight: 600; - color: var(--text-primary); -} - -.workspace-url-link { - color: var(--text-link); - cursor: pointer; - font-size: 11px; -} diff --git a/packages/launcher/src/renderer/types/index.ts b/packages/launcher/src/renderer/types/index.ts new file mode 100644 index 000000000..a3ccc1cb2 --- /dev/null +++ b/packages/launcher/src/renderer/types/index.ts @@ -0,0 +1,186 @@ +export type AgentState = 'online' | 'running' | 'idle' | 'starting' | 'reconnecting' | 'stopped' | 'error' + +export interface HealthCheck { + ready: boolean + installed?: boolean + binary?: string | null + version?: string | null + message?: string + auth_mode?: string + execution_mode?: string +} + +export interface Agent { + name: string + type: string + state: AgentState + health: HealthCheck | null + network?: string | null + networkName?: string | null + lastError?: string | null + runtimeMismatch?: boolean + restarts?: number + env?: Record + path?: string +} + +export interface EnvField { + name: string + description: string + required?: boolean + password?: boolean + placeholder?: string + default?: string +} + +export interface CatalogEntry { + name: string + label?: string + description?: string + homepage?: string + tags?: string[] + featured?: boolean + order?: number + builtin?: boolean + installed: boolean + managed?: boolean + location?: string + support?: { + install?: boolean + workspace?: boolean + collaboration?: boolean + } + requires?: string[] + install?: { + binary?: string + requires?: (string | null)[] + macos?: string + linux?: string + windows?: string + api_only?: boolean + } + check_ready?: { + login_command?: string + not_ready_message?: string + env_vars?: string[] + saved_env_key?: string + } + env_config?: EnvField[] + screenshots?: string[] + demo?: string + demo_url?: string + long_description?: string +} + +export interface InstalledAgentRecord { + name: string + version: string | null + installedAt: string + previousVersion?: string | null + history?: Array<{ version: string; installedAt: string }> +} + +export interface AgentUpdateInfo { + name: string + current: string | null + latest: string | null + changelog?: Array<{ version: string; date?: string }> +} + +export type InstallPhase = 'idle' | 'preparing' | 'downloading' | 'installing' | 'verifying' | 'done' | 'error' + +export interface InstallProgressEvent { + agent: string + verb: 'install' | 'update' | 'uninstall' | 'rollback' + phase: InstallPhase + detail?: string + log?: string + error?: string +} + +export interface Workspace { + id: string + slug: string + name?: string + endpoint?: string + token?: string +} + +export interface RuntimeInfo { + nodeVersion: string | null + npmVersion: string | null + coreVersion: string | null + latestVersion: string | null +} + +export interface PythonStatus { + pythonPath: string | null + pythonFound: boolean + sdkInstalled: boolean + sdkVersion: string + launcherVersion: string + runtime: string +} + +declare global { + interface Window { + api: { + pythonStatus(): Promise + installSDK(): Promise + runtimeInfo(): Promise + listAgents(): Promise + getSupportedAgentTypes(): Promise + getAgentCoreInfo(): Promise + addAgent(config: { name: string; type: string; path?: string }): Promise + removeAgent(name: string): Promise + updateAgent(name: string, config: unknown): Promise + startAgent(name: string): Promise + stopAgent(name: string): Promise + startAll(): Promise + stopAll(): Promise + agentStatus(): Promise> + agentLogs(name: string, lines: number): Promise<{ lines: string[] }> + tailAgentLogs(name: string, lines: number, offset: number): Promise<{ lines: string[]; size?: number }> + clearLogsInRange(start: string, end: string): Promise<{ removed: number; remaining: number }> + installAgentType(type: string): Promise + installAgentTypeStreaming(type: string): Promise + onInstallOutput(callback: (data: string) => void): void + removeInstallOutputListener(): void + onInstallProgress(callback: (ev: InstallProgressEvent) => void): void + removeInstallProgressListener(): void + uninstallAgentType(type: string): Promise + uninstallAgentTypeStreaming(type: string): Promise + checkAgentType(type: string): Promise<{ installed: boolean; binary: string | null }> + getCatalog(): Promise + getInstalledAgents(): Promise + checkAgentUpdates(): Promise + rollbackAgentType(type: string): Promise<{ success: boolean; version?: string | null; error?: string }> + getAgentChangelog(type: string): Promise<{ versions: Array<{ version: string; date?: string }>; homepage?: string; error?: string }> + getEnvFields(type: string): Promise + getAgentEnv(type: string): Promise> + saveAgentEnv(type: string, env: Record): Promise + getAgentInstanceEnv(name: string): Promise> + saveAgentInstanceEnv(name: string, env: Record): Promise + testLLM(env: Record): Promise<{ success: boolean; model?: string; response?: string; error?: string }> + signalReload(): Promise + connectWorkspace(agentName: string, slug: string): Promise + disconnectWorkspace(agentName: string): Promise + removeWorkspace(slug: string): Promise + listWorkspaces(): Promise + createWorkspace(name: string): Promise<{ token?: string; slug?: string }> + getSetting(key: string): Promise + setSetting(key: string, value: unknown): Promise + healthCheck(type: string): Promise + openExternal(url: string): Promise + shellExec(cmd: string): Promise + openTerminal(cmd: string): Promise + updateCore(): Promise<{ success: boolean; version?: string; error?: string }> + onCoreUpdate(cb: (info: { current: string; latest: string }) => void): void + onAgentUpdatesChanged(cb: (updates: AgentUpdateInfo[]) => void): void + onNavigateToInstall(cb: (agentName: string) => void): void + getIconPath(name: string): Promise + getIconsDir(): Promise + debugEnv(): Promise> + } + } +} diff --git a/packages/launcher/tsconfig.json b/packages/launcher/tsconfig.json new file mode 100644 index 000000000..155ebaa67 --- /dev/null +++ b/packages/launcher/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.web.json" } + ] +} diff --git a/packages/launcher/tsconfig.node.json b/packages/launcher/tsconfig.node.json new file mode 100644 index 000000000..9c5aa76e7 --- /dev/null +++ b/packages/launcher/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "composite": true, + "types": ["electron-vite/node", "node"] + }, + "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"] +} diff --git a/packages/launcher/tsconfig.web.json b/packages/launcher/tsconfig.web.json new file mode 100644 index 000000000..fc9bc2a51 --- /dev/null +++ b/packages/launcher/tsconfig.web.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "composite": true, + "baseUrl": ".", + "paths": { + "@renderer/*": ["src/renderer/*"] + } + }, + "include": ["src/renderer/**/*"] +}