diff options
54 files changed, 7782 insertions, 1 deletions
@@ -1,5 +1,9 @@ .env +stack.env doc/servers.drawio doc/Servers.drawio authelia/config/users_database.yml git-server/gerrit.config +admin-panel/node_modules/ +admin-panel/dist/ +admin-panel/.env diff --git a/admin-panel/.env.example b/admin-panel/.env.example new file mode 100644 index 0000000..4588b0f --- /dev/null +++ b/admin-panel/.env.example @@ -0,0 +1,6 @@ +ANTHROPIC_API_KEY=sk-ant-... +NODE_ENV=production +GIT_REPO_PATH=/repos +GERRIT_URL=http://gerrit:8080 +GERRIT_SSH_HOST=gerrit +GERRIT_SSH_PORT=29418 diff --git a/admin-panel/Dockerfile b/admin-panel/Dockerfile new file mode 100644 index 0000000..0e1e27a --- /dev/null +++ b/admin-panel/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine + +RUN apk add --no-cache git openssh-client + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --production=false +COPY . . +RUN npm run build + +EXPOSE 3000 +CMD ["node", "dist/server/index.js"] diff --git a/admin-panel/docker-compose.yaml b/admin-panel/docker-compose.yaml new file mode 100644 index 0000000..6df3c85 --- /dev/null +++ b/admin-panel/docker-compose.yaml @@ -0,0 +1,33 @@ +services: + admin-panel: + build: . + container_name: admin-panel + restart: always + environment: + - NODE_ENV=production + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - GIT_REPO_PATH=/repos + - GERRIT_URL=http://gerrit:8080 + - GERRIT_SSH_HOST=gerrit + - GERRIT_SSH_PORT=29418 + volumes: + - /var/admin-panel/data:/app/data + - /var/admin-panel/workspaces:/app/workspaces + - /var/git/repos:/repos + - /var/admin-panel/ssh:/app/.ssh:ro + networks: + admin_network: + ipv4_address: 172.27.0.2 + git-network: + ipv4_address: 172.22.0.5 + +networks: + admin_network: + name: admin_network + driver: bridge + ipam: + config: + - subnet: 172.27.0.0/16 + gateway: 172.27.0.1 + git-network: + external: true diff --git a/admin-panel/package-lock.json b/admin-panel/package-lock.json new file mode 100644 index 0000000..a14388d --- /dev/null +++ b/admin-panel/package-lock.json @@ -0,0 +1,3407 @@ +{ + "name": "admin-panel", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "admin-panel", + "version": "1.0.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0", + "express": "^4.21.0", + "simple-git": "^3.27.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/express": "^5.0.0", + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "concurrently": "^9.1.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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 + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "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, + "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", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "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.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-git": { + "version": "3.32.3", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.32.3.tgz", + "integrity": "sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw==", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/simple-git/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/simple-git/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "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 + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/admin-panel/package.json b/admin-panel/package.json new file mode 100644 index 0000000..8bc7960 --- /dev/null +++ b/admin-panel/package.json @@ -0,0 +1,28 @@ +{ + "name": "admin-panel", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently \"tsx watch src/server/index.ts\" \"vite\"", + "build": "vite build && tsc -p tsconfig.server.json", + "start": "node dist/server/index.js" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0", + "express": "^4.21.0", + "simple-git": "^3.27.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/express": "^5.0.0", + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "concurrently": "^9.1.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/admin-panel/src/client/components/agent-log.ts b/admin-panel/src/client/components/agent-log.ts new file mode 100644 index 0000000..5938426 --- /dev/null +++ b/admin-panel/src/client/components/agent-log.ts @@ -0,0 +1,2 @@ +// Agent log is inlined in task-modal.ts — this file exists for Phase 3 expansion +export {}; diff --git a/admin-panel/src/client/components/board.ts b/admin-panel/src/client/components/board.ts new file mode 100644 index 0000000..b611133 --- /dev/null +++ b/admin-panel/src/client/components/board.ts @@ -0,0 +1,123 @@ +import { api, type Task } from '../lib/api.js'; +import { renderTaskCard } from './task-card.js'; +import { showTaskModal } from './task-modal.js'; + +const COLUMNS = [ + { id: 'todo', label: 'Req' }, + { id: 'plan', label: 'Plan' }, + { id: 'plan_review', label: 'Plan Review' }, + { id: 'impl', label: 'Impl' }, + { id: 'impl_review', label: 'Impl Review' }, + { id: 'test', label: 'Test' }, + { id: 'done', label: 'Done' }, +]; + +export function renderBoard(container: HTMLElement, projectName: string, tasks: Task[]): void { + const tasksByStatus = new Map<string, Task[]>(); + for (const col of COLUMNS) { + tasksByStatus.set(col.id, []); + } + for (const task of tasks) { + const list = tasksByStatus.get(task.status); + if (list) list.push(task); + else tasksByStatus.get('todo')!.push(task); // fallback + } + + container.innerHTML = `<div class="board-container">${COLUMNS.map(col => { + const colTasks = tasksByStatus.get(col.id)!; + return ` + <div class="board-column" data-status="${col.id}"> + <div class="column-header"> + <h3>${col.label}</h3> + <span class="column-count">${colTasks.length}</span> + </div> + <div class="column-body" data-status="${col.id}"> + ${colTasks.map(task => renderTaskCard(task)).join('')} + </div> + </div> + `; + }).join('')}</div>`; + + // Setup drag-drop + setupDragDrop(container, projectName); + + // Setup card click + container.querySelectorAll('.task-card').forEach(card => { + card.addEventListener('click', (e) => { + // Don't open modal if dragging + if ((card as HTMLElement).classList.contains('dragging')) return; + const taskId = parseInt((card as HTMLElement).dataset.taskId!); + const task = tasks.find(t => t.id === taskId); + if (task) showTaskModal(projectName, task); + }); + }); +} + +function setupDragDrop(container: HTMLElement, projectName: string): void { + const cards = container.querySelectorAll('.task-card'); + const columnBodies = container.querySelectorAll('.column-body'); + + let draggedCard: HTMLElement | null = null; + + cards.forEach(card => { + const el = card as HTMLElement; + el.draggable = true; + + el.addEventListener('dragstart', (e) => { + draggedCard = el; + el.classList.add('dragging'); + (e as DragEvent).dataTransfer!.effectAllowed = 'move'; + (e as DragEvent).dataTransfer!.setData('text/plain', el.dataset.taskId!); + }); + + el.addEventListener('dragend', () => { + el.classList.remove('dragging'); + draggedCard = null; + columnBodies.forEach(cb => cb.classList.remove('drag-over')); + }); + }); + + columnBodies.forEach(body => { + body.addEventListener('dragover', (e) => { + e.preventDefault(); + (e as DragEvent).dataTransfer!.dropEffect = 'move'; + body.classList.add('drag-over'); + }); + + body.addEventListener('dragleave', () => { + body.classList.remove('drag-over'); + }); + + body.addEventListener('drop', async (e) => { + e.preventDefault(); + body.classList.remove('drag-over'); + + if (!draggedCard) return; + + const taskId = parseInt(draggedCard.dataset.taskId!); + const newStatus = (body as HTMLElement).dataset.status!; + + // Calculate rank based on drop position + const existingCards = Array.from(body.querySelectorAll('.task-card:not(.dragging)')); + let rank: number; + + if (existingCards.length === 0) { + rank = 1; + } else { + // Drop at end by default + const lastCard = existingCards[existingCards.length - 1] as HTMLElement; + const lastRank = parseFloat(lastCard.dataset.rank || '0'); + rank = lastRank + 1; + } + + // Optimistically move card + body.appendChild(draggedCard); + + try { + await api.reorderTask(projectName, taskId, rank, newStatus); + } catch (err) { + console.error('Reorder failed:', err); + } + }); + }); +} diff --git a/admin-panel/src/client/components/diff-viewer.ts b/admin-panel/src/client/components/diff-viewer.ts new file mode 100644 index 0000000..b746b75 --- /dev/null +++ b/admin-panel/src/client/components/diff-viewer.ts @@ -0,0 +1,2 @@ +// Diff viewer is inlined in task-modal.ts — this file exists for Phase 4 expansion +export {}; diff --git a/admin-panel/src/client/components/pipeline-controls.ts b/admin-panel/src/client/components/pipeline-controls.ts new file mode 100644 index 0000000..2b3e9a5 --- /dev/null +++ b/admin-panel/src/client/components/pipeline-controls.ts @@ -0,0 +1,2 @@ +// Pipeline controls are inlined in task-modal.ts — this file exists for Phase 3 expansion +export {}; diff --git a/admin-panel/src/client/components/project-form.ts b/admin-panel/src/client/components/project-form.ts new file mode 100644 index 0000000..d69f194 --- /dev/null +++ b/admin-panel/src/client/components/project-form.ts @@ -0,0 +1,2 @@ +// Project form is inlined in dashboard.ts — this file exists for future extraction if needed +export {}; diff --git a/admin-panel/src/client/components/task-card.ts b/admin-panel/src/client/components/task-card.ts new file mode 100644 index 0000000..a63e507 --- /dev/null +++ b/admin-panel/src/client/components/task-card.ts @@ -0,0 +1,40 @@ +import type { Task } from '../lib/api.js'; + +export function renderTaskCard(task: Task): string { + const tags = parseTags(task.tags); + const priorityClass = `badge-${task.priority}`; + const isActive = task.current_agent !== null; + + return ` + <div class="task-card ${isActive ? 'pipeline-active' : ''}" + data-task-id="${task.id}" + data-rank="${task.rank}" + draggable="true"> + <div class="task-card-header"> + <span class="task-id">#${task.id}</span> + ${task.current_agent ? `<span class="badge badge-agent">${escapeHtml(task.current_agent)}</span>` : ''} + </div> + <div class="task-title">${escapeHtml(task.title)}</div> + <div class="task-meta"> + <span class="badge ${priorityClass}">${task.priority}</span> + <span class="badge badge-level">L${task.level}</span> + ${tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')} + </div> + </div> + `; +} + +function parseTags(tagsStr: string): string[] { + try { + const parsed = JSON.parse(tagsStr); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/admin-panel/src/client/components/task-modal.ts b/admin-panel/src/client/components/task-modal.ts new file mode 100644 index 0000000..725d921 --- /dev/null +++ b/admin-panel/src/client/components/task-modal.ts @@ -0,0 +1,271 @@ +import { api, type Task } from '../lib/api.js'; +import { renderMarkdown } from '../lib/markdown.js'; + +const STATUS_ORDER = ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done']; +const STATUS_LABELS: Record<string, string> = { + todo: 'Req', plan: 'Plan', plan_review: 'Plan Review', + impl: 'Impl', impl_review: 'Impl Review', test: 'Test', done: 'Done' +}; + +const TABS = ['Requirements', 'Plan', 'Implementation', 'Reviews', 'Tests', 'Agent Log']; + +export function showTaskModal(projectName: string, task: Task): void { + document.querySelector('.modal-overlay')?.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` + <div class="modal task-modal"> + <div class="modal-header"> + <div class="top-row"> + <span class="task-id" style="font-size:14px">#${task.id}</span> + <button class="btn-icon close-btn" style="font-size:20px">×</button> + </div> + <h2 style="width:100%">${escapeHtml(task.title)}</h2> + <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"> + <span class="badge badge-${task.priority}">${task.priority}</span> + <span class="badge badge-level">L${task.level}</span> + <span class="badge" style="background:var(--border);color:var(--text-secondary)"> + ${STATUS_LABELS[task.status] || task.status} + </span> + ${task.current_agent ? `<span class="badge badge-agent">${escapeHtml(task.current_agent)}</span>` : ''} + ${parseTags(task.tags).map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')} + </div> + </div> + + ${renderLifecycleBar(task)} + + <div class="tabs" id="task-tabs"> + ${TABS.map((tab, i) => ` + <button class="tab ${i === 0 ? 'active' : ''}" data-tab="${i}">${tab}</button> + `).join('')} + </div> + + <div class="tab-content" id="tab-content"> + ${renderTabContent(0, task)} + </div> + + <div class="modal-footer"> + <div class="pipeline-controls"> + <button class="btn btn-primary btn-sm" id="run-pipeline-btn" title="Run full pipeline">Run Pipeline</button> + <button class="btn btn-secondary btn-sm" id="step-pipeline-btn" title="Run next step">Step</button> + <button class="btn btn-danger btn-sm" id="stop-pipeline-btn" title="Stop pipeline">Stop</button> + </div> + <div style="flex:1"></div> + <button class="btn btn-danger btn-sm" id="delete-task-btn">Delete</button> + </div> + </div> + `; + + document.body.appendChild(overlay); + + // Tab switching + overlay.querySelectorAll('.tab').forEach(tab => { + tab.addEventListener('click', () => { + overlay.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + const tabIdx = parseInt((tab as HTMLElement).dataset.tab!); + overlay.querySelector('#tab-content')!.innerHTML = renderTabContent(tabIdx, task); + }); + }); + + // Close + overlay.querySelectorAll('.close-btn').forEach(btn => { + btn.addEventListener('click', () => overlay.remove()); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + // Pipeline controls + overlay.querySelector('#run-pipeline-btn')?.addEventListener('click', async () => { + try { + await api.runPipeline(projectName, task.id); + } catch (err) { + alert((err as Error).message); + } + }); + + overlay.querySelector('#step-pipeline-btn')?.addEventListener('click', async () => { + try { + await api.stepPipeline(projectName, task.id); + } catch (err) { + alert((err as Error).message); + } + }); + + overlay.querySelector('#stop-pipeline-btn')?.addEventListener('click', async () => { + try { + await api.stopPipeline(projectName, task.id); + } catch (err) { + alert((err as Error).message); + } + }); + + // Delete + overlay.querySelector('#delete-task-btn')?.addEventListener('click', async () => { + if (!confirm(`Delete task #${task.id}?`)) return; + try { + await api.deleteTask(projectName, task.id); + overlay.remove(); + } catch (err) { + alert((err as Error).message); + } + }); +} + +function renderLifecycleBar(task: Task): string { + const currentIdx = STATUS_ORDER.indexOf(task.status); + return ` + <div class="lifecycle-bar"> + ${STATUS_ORDER.map((s, i) => { + let cls = 'lifecycle-step'; + if (i < currentIdx) cls += ' completed'; + else if (i === currentIdx) cls += ' current'; + return `<div class="${cls}" title="${STATUS_LABELS[s]}"></div>`; + }).join('')} + </div> + `; +} + +function renderTabContent(tabIdx: number, task: Task): string { + switch (tabIdx) { + case 0: // Requirements + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${task.description + ? `<div>${renderMarkdown(task.description)}</div>` + : '<p>No description provided.</p>' + } + ${task.done_when ? ` + <h4 style="margin-top:16px;color:var(--text-primary)">Done When</h4> + <div>${renderMarkdown(task.done_when)}</div> + ` : ''} + </div> + `; + + case 1: // Plan + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${task.plan + ? `<div>${renderMarkdown(task.plan)}</div>` + : '<p>No plan generated yet. Run the pipeline to create one.</p>' + } + ${task.decision_log ? ` + <h4 style="margin-top:16px;color:var(--text-primary)">Decision Log</h4> + <div>${renderMarkdown(task.decision_log)}</div> + ` : ''} + </div> + `; + + case 2: // Implementation + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${task.implementation_notes + ? `<div>${renderMarkdown(task.implementation_notes)}</div>` + : '<p>No implementation notes yet.</p>' + } + ${task.branch_name ? `<p style="margin-top:12px">Branch: <code>${escapeHtml(task.branch_name)}</code></p>` : ''} + ${task.diff ? ` + <h4 style="margin-top:16px;color:var(--text-primary)">Diff</h4> + <div class="diff-viewer">${renderDiff(task.diff)}</div> + ` : ''} + </div> + `; + + case 3: // Reviews + return ` + <div style="color:var(--text-secondary);font-size:13px"> + <h4 style="color:var(--text-primary)">Plan Reviews (${task.plan_review_count})</h4> + ${renderJsonArray(task.plan_review_comments, 'No plan reviews yet.')} + + <h4 style="margin-top:16px;color:var(--text-primary)">Code Reviews (${task.impl_review_count})</h4> + ${renderJsonArray(task.review_comments, 'No code reviews yet.')} + + ${task.gerrit_change_number ? ` + <p style="margin-top:16px">Gerrit Change: <a href="/gerrit/${task.gerrit_change_number}" style="color:var(--accent-glow)">#${task.gerrit_change_number}</a></p> + ` : ''} + </div> + `; + + case 4: // Tests + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${renderJsonArray(task.test_results, 'No test results yet. Run the pipeline to generate tests.')} + </div> + `; + + case 5: // Agent Log + return ` + <div class="agent-log"> + ${renderAgentLog(task.agent_log)} + </div> + `; + + default: + return ''; + } +} + +function renderDiff(diff: string): string { + return diff.split('\n').map(line => { + let cls = 'diff-line'; + if (line.startsWith('+')) cls += ' diff-add'; + else if (line.startsWith('-')) cls += ' diff-del'; + else if (line.startsWith('@@')) cls += ' diff-hunk'; + return `<div class="${cls}">${escapeHtml(line)}</div>`; + }).join(''); +} + +function renderJsonArray(jsonStr: string, emptyMsg: string): string { + try { + const items = JSON.parse(jsonStr); + if (!Array.isArray(items) || items.length === 0) return `<p>${emptyMsg}</p>`; + return items.map((item: unknown) => { + if (typeof item === 'string') return `<div class="log-entry"><div class="log-message">${escapeHtml(item)}</div></div>`; + if (typeof item === 'object' && item !== null) { + const obj = item as Record<string, unknown>; + return `<div class="log-entry"> + ${obj.agent ? `<div class="log-agent">${escapeHtml(String(obj.agent))}</div>` : ''} + <div class="log-message">${escapeHtml(String(obj.message || obj.comment || JSON.stringify(obj)))}</div> + </div>`; + } + return ''; + }).join(''); + } catch { + return `<p>${emptyMsg}</p>`; + } +} + +function renderAgentLog(logStr: string): string { + try { + const entries = JSON.parse(logStr); + if (!Array.isArray(entries) || entries.length === 0) { + return '<div class="empty-state"><p>No agent activity yet.</p></div>'; + } + return entries.map((entry: Record<string, unknown>) => ` + <div class="log-entry"> + <span class="log-time">${entry.timestamp ? new Date(String(entry.timestamp)).toLocaleTimeString() : ''}</span> + <span class="log-agent">${escapeHtml(String(entry.agent || ''))}</span> + <span class="log-message">${escapeHtml(String(entry.message || ''))}</span> + </div> + `).join(''); + } catch { + return '<div class="empty-state"><p>No agent activity yet.</p></div>'; + } +} + +function parseTags(tagsStr: string): string[] { + try { + const parsed = JSON.parse(tagsStr); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/admin-panel/src/client/index.html b/admin-panel/src/client/index.html new file mode 100644 index 0000000..58af369 --- /dev/null +++ b/admin-panel/src/client/index.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Bastion Admin</title> + <link rel="stylesheet" href="./styles/main.css" /> +</head> +<body> + <div id="app"> + <nav id="navbar"> + <a href="/" class="nav-brand">BASTION</a> + <div class="nav-links"> + <a href="/" data-route="/">Dashboard</a> + <a href="/settings" data-route="/settings">Settings</a> + </div> + </nav> + <main id="main-content"></main> + </div> + <script type="module" src="./main.ts"></script> +</body> +</html> diff --git a/admin-panel/src/client/lib/api.ts b/admin-panel/src/client/lib/api.ts new file mode 100644 index 0000000..d82693f --- /dev/null +++ b/admin-panel/src/client/lib/api.ts @@ -0,0 +1,155 @@ +const BASE = '/api'; + +async function request<T>(path: string, options?: RequestInit): Promise<T> { + const res = await fetch(`${BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...options, + }); + + if (res.status === 204) return undefined as T; + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || `Request failed: ${res.status}`); + } + return data as T; +} + +export const api = { + // Projects + getProjects: () => request<Project[]>('/projects'), + createProject: (data: CreateProject) => + request<Project>('/projects', { method: 'POST', body: JSON.stringify(data) }), + getProject: (name: string) => request<Project>(`/projects/${name}`), + updateProject: (name: string, data: Partial<Project>) => + request<Project>(`/projects/${name}`, { method: 'PATCH', body: JSON.stringify(data) }), + deleteProject: (name: string, deleteRepo = false) => + request<void>(`/projects/${name}?delete_repo=${deleteRepo}`, { method: 'DELETE' }), + + // Tasks + getTasks: (project: string) => request<Task[]>(`/projects/${project}/tasks`), + createTask: (project: string, data: CreateTask) => + request<Task>(`/projects/${project}/tasks`, { method: 'POST', body: JSON.stringify(data) }), + getTask: (project: string, id: number) => request<Task>(`/projects/${project}/tasks/${id}`), + updateTask: (project: string, id: number, data: Partial<Task>) => + request<Task>(`/projects/${project}/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), + deleteTask: (project: string, id: number) => + request<void>(`/projects/${project}/tasks/${id}`, { method: 'DELETE' }), + reorderTask: (project: string, id: number, rank: number, status?: string) => + request<Task>(`/projects/${project}/tasks/${id}/reorder`, { + method: 'PATCH', + body: JSON.stringify({ rank, status }), + }), + + // Pipeline + runPipeline: (project: string, taskId: number) => + request<void>(`/projects/${project}/pipeline/run/${taskId}`, { method: 'POST' }), + stepPipeline: (project: string, taskId: number) => + request<void>(`/projects/${project}/pipeline/step/${taskId}`, { method: 'POST' }), + stopPipeline: (project: string, taskId: number) => + request<void>(`/projects/${project}/pipeline/stop/${taskId}`, { method: 'POST' }), + getPipelineStatus: (project: string, taskId: number) => + request<PipelineStatus>(`/projects/${project}/pipeline/status/${taskId}`), + + // Dashboard + getDashboardStats: () => request<DashboardStats>('/dashboard/stats'), + getDashboardActivity: () => request<ActivityEntry[]>('/dashboard/activity'), + + // Settings + getSettings: () => request<Record<string, string>>('/settings'), + updateSettings: (data: Record<string, string>) => + request<void>('/settings', { method: 'PATCH', body: JSON.stringify(data) }), +}; + +// Types +export interface Project { + id: number; + name: string; + display_name: string; + description: string | null; + repo_name: string; + repo_path: string; + default_branch: string; + created_at: string; + updated_at: string; +} + +export interface CreateProject { + name: string; + display_name: string; + description?: string; +} + +export interface Task { + id: number; + title: string; + status: string; + priority: string; + level: number; + description: string | null; + plan: string | null; + decision_log: string | null; + done_when: string | null; + implementation_notes: string | null; + tags: string; + plan_review_comments: string; + review_comments: string; + test_results: string; + agent_log: string; + current_agent: string | null; + plan_review_count: number; + impl_review_count: number; + branch_name: string | null; + gerrit_change_id: string | null; + gerrit_change_number: number | null; + diff: string | null; + rank: number; + created_at: string; + started_at: string | null; + planned_at: string | null; + reviewed_at: string | null; + tested_at: string | null; + completed_at: string | null; +} + +export interface CreateTask { + title: string; + description?: string; + priority?: string; + level?: number; + tags?: string[]; +} + +export interface PipelineStatus { + running: boolean; + currentAgent: string | null; + jobs: PipelineJob[]; +} + +export interface PipelineJob { + id: number; + task_id: number; + agent: string; + status: string; + model: string; + started_at: string | null; + completed_at: string | null; + error: string | null; + tokens_used: number; +} + +export interface DashboardStats { + totalProjects: number; + totalTasks: number; + tasksByStatus: Record<string, number>; + completedToday: number; +} + +export interface ActivityEntry { + project: string; + taskId: number; + taskTitle: string; + agent: string; + action: string; + timestamp: string; +} diff --git a/admin-panel/src/client/lib/markdown.ts b/admin-panel/src/client/lib/markdown.ts new file mode 100644 index 0000000..c89fa77 --- /dev/null +++ b/admin-panel/src/client/lib/markdown.ts @@ -0,0 +1,39 @@ +// Minimal markdown renderer — handles the basics without pulling in a library +export function renderMarkdown(text: string): string { + if (!text) return ''; + + let html = escapeHtml(text); + + // Code blocks + html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="lang-$1">$2</code></pre>'); + + // Inline code + html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); + + // Headers + html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>'); + html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>'); + html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>'); + + // Bold and italic + html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); + html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); + + // Lists + html = html.replace(/^- (.+)$/gm, '<li>$1</li>'); + html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>'); + + // Line breaks + html = html.replace(/\n\n/g, '</p><p>'); + html = `<p>${html}</p>`; + html = html.replace(/<p><\/p>/g, ''); + + return html; +} + +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); +} diff --git a/admin-panel/src/client/lib/ws.ts b/admin-panel/src/client/lib/ws.ts new file mode 100644 index 0000000..14e0cec --- /dev/null +++ b/admin-panel/src/client/lib/ws.ts @@ -0,0 +1,79 @@ +type EventHandler = (data: unknown) => void; + +class WsClient { + private ws: WebSocket | null = null; + private handlers = new Map<string, Set<EventHandler>>(); + private reconnectTimer: ReturnType<typeof setTimeout> | null = null; + private subscribedProjects = new Set<string>(); + + connect(): void { + if (this.ws?.readyState === WebSocket.OPEN) return; + + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + this.ws = new WebSocket(`${protocol}//${location.host}/ws`); + + this.ws.onopen = () => { + // Re-subscribe to projects + for (const project of this.subscribedProjects) { + this.send({ type: 'subscribe', project }); + } + }; + + this.ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + const handlers = this.handlers.get(data.type); + if (handlers) { + for (const handler of handlers) { + handler(data); + } + } + // Also fire wildcard handlers + const wildcardHandlers = this.handlers.get('*'); + if (wildcardHandlers) { + for (const handler of wildcardHandlers) { + handler(data); + } + } + } catch { + // ignore + } + }; + + this.ws.onclose = () => { + this.reconnectTimer = setTimeout(() => this.connect(), 3000); + }; + } + + disconnect(): void { + if (this.reconnectTimer) clearTimeout(this.reconnectTimer); + this.ws?.close(); + this.ws = null; + } + + subscribe(project: string): void { + this.subscribedProjects.add(project); + this.send({ type: 'subscribe', project }); + } + + unsubscribe(project: string): void { + this.subscribedProjects.delete(project); + this.send({ type: 'unsubscribe', project }); + } + + on(event: string, handler: EventHandler): () => void { + if (!this.handlers.has(event)) { + this.handlers.set(event, new Set()); + } + this.handlers.get(event)!.add(handler); + return () => this.handlers.get(event)?.delete(handler); + } + + private send(data: unknown): void { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(data)); + } + } +} + +export const wsClient = new WsClient(); diff --git a/admin-panel/src/client/main.ts b/admin-panel/src/client/main.ts new file mode 100644 index 0000000..87f25db --- /dev/null +++ b/admin-panel/src/client/main.ts @@ -0,0 +1,63 @@ +import { renderDashboard } from './pages/dashboard.js'; +import { renderProjectPage } from './pages/project.js'; +import { renderSettings } from './pages/settings.js'; +import { wsClient } from './lib/ws.js'; + +const mainContent = document.getElementById('main-content')!; + +// Simple client-side router +type Route = { + pattern: RegExp; + handler: (container: HTMLElement, params: string[]) => Promise<void>; +}; + +const routes: Route[] = [ + { pattern: /^\/$/, handler: (c) => renderDashboard(c) }, + { pattern: /^\/project\/([a-z0-9-]+)$/, handler: (c, p) => renderProjectPage(c, p[1]) }, + { pattern: /^\/settings$/, handler: (c) => renderSettings(c) }, +]; + +function navigate(path: string): void { + for (const route of routes) { + const match = path.match(route.pattern); + if (match) { + updateNavActive(path); + route.handler(mainContent, Array.from(match)); + return; + } + } + // 404 + mainContent.innerHTML = ` + <div class="empty-state"> + <h3>Page not found</h3> + <p><a href="/" data-route="/" style="color:var(--accent-glow)">Back to Dashboard</a></p> + </div> + `; +} + +function updateNavActive(path: string): void { + document.querySelectorAll('.nav-links a').forEach(a => { + const route = (a as HTMLElement).dataset.route; + a.classList.toggle('active', route === path || (route === '/' && path.startsWith('/project/'))); + }); +} + +// Intercept link clicks for SPA navigation +document.addEventListener('click', (e) => { + const link = (e.target as HTMLElement).closest('[data-route]'); + if (link) { + e.preventDefault(); + const path = (link as HTMLElement).dataset.route!; + history.pushState(null, '', path); + navigate(path); + } +}); + +// Handle browser back/forward +window.addEventListener('popstate', () => { + navigate(location.pathname); +}); + +// Init +wsClient.connect(); +navigate(location.pathname); diff --git a/admin-panel/src/client/pages/dashboard.ts b/admin-panel/src/client/pages/dashboard.ts new file mode 100644 index 0000000..ded6d3b --- /dev/null +++ b/admin-panel/src/client/pages/dashboard.ts @@ -0,0 +1,150 @@ +import { api, type Project } from '../lib/api.js'; + +export async function renderDashboard(container: HTMLElement): Promise<void> { + container.innerHTML = ` + <div class="dashboard-header"> + <h1>Projects</h1> + <button class="btn btn-primary" id="new-project-btn">+ New Project</button> + </div> + <div class="project-grid" id="project-grid"> + <div class="empty-state"> + <h3>Loading...</h3> + </div> + </div> + `; + + const grid = container.querySelector('#project-grid')!; + const newBtn = container.querySelector('#new-project-btn')!; + + // Load projects + try { + const projects = await api.getProjects(); + renderProjects(grid, projects); + } catch (err) { + grid.innerHTML = `<div class="empty-state"><h3>Failed to load projects</h3><p>${(err as Error).message}</p></div>`; + } + + newBtn.addEventListener('click', () => showCreateDialog(grid)); +} + +function renderProjects(grid: Element, projects: Project[]): void { + if (projects.length === 0) { + grid.innerHTML = ` + <div class="empty-state"> + <h3>No projects yet</h3> + <p>Create your first project to get started.</p> + </div> + `; + return; + } + + grid.innerHTML = projects.map(p => ` + <a href="/project/${p.name}" class="project-card" data-route="/project/${p.name}"> + <h3>${escapeHtml(p.display_name)}</h3> + <div class="project-name">${escapeHtml(p.name)}</div> + ${p.description ? `<div class="project-desc">${escapeHtml(p.description)}</div>` : ''} + <div class="project-stats"> + <span class="stat"><span class="stat-dot todo"></span> ${escapeHtml(p.repo_name)}</span> + <span class="stat">${timeAgo(p.created_at)}</span> + </div> + </a> + `).join(''); +} + +function showCreateDialog(grid: Element): void { + // Remove existing modal if present + document.querySelector('.modal-overlay')?.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` + <div class="modal"> + <div class="modal-header"> + <h2>New Project</h2> + <button class="btn-icon close-btn">×</button> + </div> + <form id="create-project-form"> + <div class="form-group"> + <label for="proj-name">Project Name</label> + <input type="text" id="proj-name" placeholder="my-project" required pattern="[a-z0-9-]+" /> + </div> + <div class="form-group"> + <label for="proj-display">Display Name</label> + <input type="text" id="proj-display" placeholder="My Project" required /> + </div> + <div class="form-group"> + <label for="proj-desc">Description</label> + <textarea id="proj-desc" placeholder="What is this project about?"></textarea> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary close-btn">Cancel</button> + <button type="submit" class="btn btn-primary">Create Project</button> + </div> + </form> + </div> + `; + + document.body.appendChild(overlay); + + // Auto-generate name from display name + const displayInput = overlay.querySelector('#proj-display') as HTMLInputElement; + const nameInput = overlay.querySelector('#proj-name') as HTMLInputElement; + displayInput.addEventListener('input', () => { + nameInput.value = displayInput.value + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-'); + }); + + // Close handlers + overlay.querySelectorAll('.close-btn').forEach(btn => { + btn.addEventListener('click', () => overlay.remove()); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + // Submit + const form = overlay.querySelector('#create-project-form') as HTMLFormElement; + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement; + submitBtn.disabled = true; + submitBtn.textContent = 'Creating...'; + + try { + await api.createProject({ + name: nameInput.value, + display_name: displayInput.value, + description: (overlay.querySelector('#proj-desc') as HTMLTextAreaElement).value || undefined, + }); + overlay.remove(); + // Reload projects + const projects = await api.getProjects(); + renderProjects(grid, projects); + } catch (err) { + submitBtn.disabled = false; + submitBtn.textContent = 'Create Project'; + alert((err as Error).message); + } + }); + + displayInput.focus(); +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function timeAgo(dateStr: string): string { + const date = new Date(dateStr + 'Z'); + const now = new Date(); + const diff = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; +} diff --git a/admin-panel/src/client/pages/project.ts b/admin-panel/src/client/pages/project.ts new file mode 100644 index 0000000..5e01d64 --- /dev/null +++ b/admin-panel/src/client/pages/project.ts @@ -0,0 +1,167 @@ +import { api, type Task, type Project } from '../lib/api.js'; +import { wsClient } from '../lib/ws.js'; +import { renderBoard } from '../components/board.js'; + +let currentProject: string | null = null; +let cleanupWs: (() => void) | null = null; + +export async function renderProjectPage(container: HTMLElement, projectName: string): Promise<void> { + // Cleanup previous subscriptions + if (currentProject && currentProject !== projectName) { + wsClient.unsubscribe(currentProject); + } + if (cleanupWs) cleanupWs(); + + currentProject = projectName; + + container.innerHTML = ` + <div class="board-header"> + <div style="display:flex;align-items:center;gap:12px"> + <a href="/" data-route="/" class="btn btn-secondary btn-sm">← Back</a> + <h1 id="project-title">Loading...</h1> + </div> + <button class="btn btn-primary" id="new-task-btn">+ New Task</button> + </div> + <div id="board-root"></div> + `; + + const boardRoot = container.querySelector('#board-root') as HTMLElement; + const titleEl = container.querySelector('#project-title')!; + const newTaskBtn = container.querySelector('#new-task-btn')!; + + try { + const [project, tasks] = await Promise.all([ + api.getProject(projectName), + api.getTasks(projectName), + ]); + + titleEl.textContent = project.display_name; + renderBoard(boardRoot, projectName, tasks); + + // WebSocket subscription + wsClient.subscribe(projectName); + const unsub1 = wsClient.on('task:created', (data: unknown) => { + const event = data as { project: string; task: Task }; + if (event.project === projectName) { + refreshBoard(boardRoot, projectName); + } + }); + const unsub2 = wsClient.on('task:updated', (data: unknown) => { + const event = data as { project: string; task: Task }; + if (event.project === projectName) { + refreshBoard(boardRoot, projectName); + } + }); + const unsub3 = wsClient.on('task:deleted', (data: unknown) => { + const event = data as { project: string }; + if (event.project === projectName) { + refreshBoard(boardRoot, projectName); + } + }); + + cleanupWs = () => { unsub1(); unsub2(); unsub3(); }; + + } catch (err) { + boardRoot.innerHTML = `<div class="empty-state"><h3>Failed to load project</h3><p>${(err as Error).message}</p></div>`; + } + + newTaskBtn.addEventListener('click', () => showCreateTaskDialog(projectName, boardRoot)); +} + +async function refreshBoard(boardRoot: HTMLElement, projectName: string): Promise<void> { + try { + const tasks = await api.getTasks(projectName); + renderBoard(boardRoot, projectName, tasks); + } catch { + // silently fail on refresh + } +} + +function showCreateTaskDialog(projectName: string, boardRoot: HTMLElement): void { + document.querySelector('.modal-overlay')?.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` + <div class="modal"> + <div class="modal-header"> + <h2>New Task</h2> + <button class="btn-icon close-btn">×</button> + </div> + <form id="create-task-form"> + <div class="form-group"> + <label for="task-title">Title</label> + <input type="text" id="task-title" placeholder="What needs to be done?" required /> + </div> + <div class="form-group"> + <label for="task-desc">Description</label> + <textarea id="task-desc" rows="4" placeholder="Detailed requirements..."></textarea> + </div> + <div style="display:flex;gap:12px"> + <div class="form-group" style="flex:1"> + <label for="task-priority">Priority</label> + <select id="task-priority"> + <option value="low">Low</option> + <option value="medium" selected>Medium</option> + <option value="high">High</option> + </select> + </div> + <div class="form-group" style="flex:1"> + <label for="task-level">Level</label> + <select id="task-level"> + <option value="1">L1 — Quick</option> + <option value="2">L2 — Standard</option> + <option value="3" selected>L3 — Full</option> + </select> + </div> + </div> + <div class="form-group"> + <label for="task-tags">Tags (comma-separated)</label> + <input type="text" id="task-tags" placeholder="frontend, bug-fix" /> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary close-btn">Cancel</button> + <button type="submit" class="btn btn-primary">Create Task</button> + </div> + </form> + </div> + `; + + document.body.appendChild(overlay); + + overlay.querySelectorAll('.close-btn').forEach(btn => { + btn.addEventListener('click', () => overlay.remove()); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + const form = overlay.querySelector('#create-task-form') as HTMLFormElement; + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement; + submitBtn.disabled = true; + submitBtn.textContent = 'Creating...'; + + const tagsStr = (overlay.querySelector('#task-tags') as HTMLInputElement).value; + const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : []; + + try { + await api.createTask(projectName, { + title: (overlay.querySelector('#task-title') as HTMLInputElement).value, + description: (overlay.querySelector('#task-desc') as HTMLTextAreaElement).value || undefined, + priority: (overlay.querySelector('#task-priority') as HTMLSelectElement).value, + level: parseInt((overlay.querySelector('#task-level') as HTMLSelectElement).value), + tags, + }); + overlay.remove(); + await refreshBoard(boardRoot, projectName); + } catch (err) { + submitBtn.disabled = false; + submitBtn.textContent = 'Create Task'; + alert((err as Error).message); + } + }); + + (overlay.querySelector('#task-title') as HTMLInputElement).focus(); +} diff --git a/admin-panel/src/client/pages/settings.ts b/admin-panel/src/client/pages/settings.ts new file mode 100644 index 0000000..e78c69f --- /dev/null +++ b/admin-panel/src/client/pages/settings.ts @@ -0,0 +1,59 @@ +export async function renderSettings(container: HTMLElement): Promise<void> { + container.innerHTML = ` + <div class="settings-container"> + <h1 style="margin-bottom:24px">Settings</h1> + + <div class="settings-section"> + <h2>API Configuration</h2> + <div class="form-group"> + <label for="api-key">Anthropic API Key</label> + <input type="password" id="api-key" placeholder="sk-ant-..." style="width:100%" /> + <small style="color:var(--text-secondary);margin-top:4px;display:block"> + Configured via ANTHROPIC_API_KEY environment variable. + The API key is set in the container's environment, not stored in the database. + </small> + </div> + </div> + + <div class="settings-section"> + <h2>Agent Models</h2> + <p style="color:var(--text-secondary);margin-bottom:16px;font-size:13px"> + Configure which Claude model each agent uses. + </p> + <div class="form-group"> + <label>Planner (Plan generation)</label> + <select disabled><option>Claude Opus</option></select> + </div> + <div class="form-group"> + <label>Critic (Plan review)</label> + <select disabled><option>Claude Sonnet</option></select> + </div> + <div class="form-group"> + <label>Builder (Implementation)</label> + <select disabled><option>Claude Opus</option></select> + </div> + <div class="form-group"> + <label>Shield (Test writing)</label> + <select disabled><option>Claude Sonnet</option></select> + </div> + <div class="form-group"> + <label>Inspector (Code review)</label> + <select disabled><option>Claude Sonnet</option></select> + </div> + <div class="form-group"> + <label>Ranger (Test runner)</label> + <select disabled><option>Claude Sonnet</option></select> + </div> + <small style="color:var(--text-secondary)">Model configuration will be available when the AI pipeline is enabled.</small> + </div> + + <div class="settings-section"> + <h2>Git / SSH</h2> + <p style="color:var(--text-secondary);font-size:13px"> + SSH key for git-server and Gerrit access is mounted at <code>/app/.ssh/</code> in the container. + Ensure the public key is added to <code>/var/git_ssh_keys/</code> on the host. + </p> + </div> + </div> + `; +} diff --git a/admin-panel/src/client/styles/main.css b/admin-panel/src/client/styles/main.css new file mode 100644 index 0000000..d2fe35b --- /dev/null +++ b/admin-panel/src/client/styles/main.css @@ -0,0 +1,724 @@ +/* Cyberpunk Dark Theme */ +:root { + --bg-deep: #0A0D1F; + --bg-card: #1A1A2E; + --accent: #7B2FBE; + --accent-glow: #A855F7; + --alert: #FF2D78; + --text-secondary: #8B9BB4; + --text-primary: #E8ECF4; + --bg-input: #12152a; + --border: #2a2a4a; + --success: #22c55e; + --warning: #eab308; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + background: var(--bg-deep); + color: var(--text-primary); + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + font-size: 14px; + line-height: 1.5; +} + +#app { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Navbar */ +#navbar { + display: flex; + align-items: center; + padding: 0 24px; + height: 52px; + background: var(--bg-card); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.nav-brand { + font-size: 18px; + font-weight: 700; + letter-spacing: 3px; + color: var(--accent-glow); + text-decoration: none; + margin-right: 32px; +} + +.nav-links { + display: flex; + gap: 4px; +} + +.nav-links a { + color: var(--text-secondary); + text-decoration: none; + padding: 6px 14px; + border-radius: 6px; + font-size: 13px; + transition: all 0.15s; +} + +.nav-links a:hover, +.nav-links a.active { + color: var(--text-primary); + background: rgba(123, 47, 190, 0.15); +} + +.nav-links a.active { + color: var(--accent-glow); +} + +/* Main content */ +#main-content { + flex: 1; + overflow: auto; + padding: 24px; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-deep); +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} + +.btn-primary { + background: var(--accent); + color: white; +} + +.btn-primary:hover { + background: var(--accent-glow); + box-shadow: 0 0 20px rgba(168, 85, 247, 0.3); +} + +.btn-secondary { + background: var(--border); + color: var(--text-primary); +} + +.btn-secondary:hover { + background: var(--text-secondary); +} + +.btn-danger { + background: transparent; + color: var(--alert); + border: 1px solid var(--alert); +} + +.btn-danger:hover { + background: var(--alert); + color: white; +} + +.btn-sm { + padding: 4px 10px; + font-size: 12px; +} + +.btn-icon { + padding: 6px; + background: transparent; + color: var(--text-secondary); + border: none; + border-radius: 4px; + cursor: pointer; +} + +.btn-icon:hover { + color: var(--text-primary); + background: var(--border); +} + +/* Form elements */ +input, textarea, select { + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text-primary); + padding: 8px 12px; + border-radius: 6px; + font-size: 13px; + font-family: inherit; + outline: none; + transition: border-color 0.15s; +} + +input:focus, textarea:focus, select:focus { + border-color: var(--accent); +} + +textarea { + resize: vertical; + min-height: 80px; +} + +label { + display: block; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; +} + +/* Cards */ +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 20px; +} + +/* Modal */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.15s; +} + +.modal { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + width: 90%; + max-width: 540px; + max-height: 90vh; + overflow-y: auto; + padding: 24px; + animation: slideUp 0.2s; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.modal-header h2 { + font-size: 18px; + font-weight: 600; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--border); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* Dashboard */ +.dashboard-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.dashboard-header h1 { + font-size: 24px; + font-weight: 600; +} + +.project-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 16px; +} + +.project-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 20px; + cursor: pointer; + transition: all 0.15s; + text-decoration: none; + color: inherit; + display: block; +} + +.project-card:hover { + border-color: var(--accent); + box-shadow: 0 0 20px rgba(123, 47, 190, 0.15); + transform: translateY(-1px); +} + +.project-card h3 { + font-size: 16px; + font-weight: 600; + margin-bottom: 4px; +} + +.project-card .project-name { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 8px; + font-family: monospace; +} + +.project-card .project-desc { + color: var(--text-secondary); + font-size: 13px; + margin-bottom: 12px; +} + +.project-card .project-stats { + display: flex; + gap: 12px; + font-size: 12px; + color: var(--text-secondary); +} + +.project-card .stat { + display: flex; + align-items: center; + gap: 4px; +} + +.stat-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.stat-dot.todo { background: var(--text-secondary); } +.stat-dot.active { background: var(--accent-glow); } +.stat-dot.done { background: var(--success); } + +/* Kanban Board */ +.board-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.board-header h1 { + font-size: 20px; + font-weight: 600; +} + +.board-container { + display: flex; + gap: 12px; + overflow-x: auto; + height: calc(100vh - 140px); + padding-bottom: 16px; +} + +.board-column { + flex: 0 0 260px; + display: flex; + flex-direction: column; + background: rgba(26, 26, 46, 0.3); + border-radius: 8px; + overflow: hidden; +} + +.column-header { + padding: 12px 14px; + background: var(--bg-card); + border-bottom: 2px solid var(--accent-glow); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.column-header h3 { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; +} + +.column-count { + font-size: 11px; + color: var(--text-secondary); + background: var(--border); + padding: 1px 8px; + border-radius: 10px; +} + +.column-body { + flex: 1; + overflow-y: auto; + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.column-body.drag-over { + background: rgba(123, 47, 190, 0.08); +} + +/* Task Cards */ +.task-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: 6px; + padding: 12px; + cursor: pointer; + transition: all 0.15s; + user-select: none; +} + +.task-card:hover { + border-color: var(--accent); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); +} + +.task-card.dragging { + opacity: 0.5; + transform: rotate(2deg); +} + +.task-card.pipeline-active { + animation: pulseGlow 2s infinite; +} + +@keyframes pulseGlow { + 0%, 100% { box-shadow: 0 0 5px rgba(168, 85, 247, 0.2); } + 50% { box-shadow: 0 0 20px rgba(168, 85, 247, 0.5); } +} + +.task-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 6px; +} + +.task-id { + font-size: 11px; + color: var(--text-secondary); + font-family: monospace; +} + +.task-title { + font-size: 13px; + font-weight: 500; + line-height: 1.4; + margin-bottom: 8px; +} + +.task-meta { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.badge { + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + border-radius: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.badge-high { background: var(--alert); color: white; } +.badge-medium { background: var(--accent); color: white; } +.badge-low { background: var(--border); color: var(--text-secondary); } + +.badge-level { + background: var(--border); + color: var(--text-secondary); +} + +.badge-agent { + background: rgba(168, 85, 247, 0.2); + color: var(--accent-glow); +} + +.tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 3px; + background: var(--border); + color: var(--text-secondary); +} + +/* Task Detail Modal */ +.task-modal { + max-width: 800px; +} + +.task-modal .modal-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +.task-modal .modal-header .top-row { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; +} + +.task-modal .tabs { + display: flex; + gap: 2px; + border-bottom: 1px solid var(--border); + margin-bottom: 16px; +} + +.task-modal .tab { + padding: 8px 16px; + font-size: 13px; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.15s; + background: none; + border-top: none; + border-left: none; + border-right: none; +} + +.task-modal .tab:hover { + color: var(--text-primary); +} + +.task-modal .tab.active { + color: var(--accent-glow); + border-bottom-color: var(--accent-glow); +} + +.tab-content { + min-height: 200px; +} + +.lifecycle-bar { + display: flex; + gap: 4px; + margin-bottom: 16px; +} + +.lifecycle-step { + flex: 1; + height: 4px; + border-radius: 2px; + background: var(--border); +} + +.lifecycle-step.completed { + background: var(--accent-glow); +} + +.lifecycle-step.current { + background: var(--accent-glow); + animation: pulseGlow 2s infinite; +} + +/* Agent Log */ +.agent-log { + display: flex; + flex-direction: column; + gap: 12px; +} + +.log-entry { + display: flex; + gap: 12px; + padding: 12px; + background: var(--bg-deep); + border-radius: 6px; + font-size: 13px; +} + +.log-entry .log-time { + color: var(--text-secondary); + font-family: monospace; + font-size: 11px; + white-space: nowrap; +} + +.log-entry .log-agent { + color: var(--accent-glow); + font-weight: 500; + white-space: nowrap; +} + +.log-entry .log-message { + color: var(--text-secondary); +} + +/* Diff Viewer */ +.diff-viewer { + font-family: 'Consolas', 'Monaco', monospace; + font-size: 12px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + overflow: auto; + max-height: 500px; +} + +.diff-line { + padding: 1px 12px; + white-space: pre; +} + +.diff-add { + background: rgba(34, 197, 94, 0.1); + color: #4ade80; +} + +.diff-del { + background: rgba(255, 45, 120, 0.1); + color: #ff6b9d; +} + +.diff-hunk { + color: var(--accent-glow); + background: rgba(168, 85, 247, 0.05); +} + +/* Pipeline Controls */ +.pipeline-controls { + display: flex; + gap: 8px; + align-items: center; +} + +/* Settings Page */ +.settings-container { + max-width: 600px; +} + +.settings-section { + margin-bottom: 32px; +} + +.settings-section h2 { + font-size: 16px; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +/* Empty state */ +.empty-state { + text-align: center; + padding: 48px 24px; + color: var(--text-secondary); +} + +.empty-state h3 { + font-size: 16px; + margin-bottom: 8px; + color: var(--text-primary); +} + +/* Toast notifications */ +.toast-container { + position: fixed; + bottom: 24px; + right: 24px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 2000; +} + +.toast { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 12px 16px; + font-size: 13px; + animation: slideUp 0.2s; + max-width: 360px; +} + +.toast.error { + border-color: var(--alert); +} + +.toast.success { + border-color: var(--success); +} + +/* Responsive */ +@media (max-width: 768px) { + .board-container { + gap: 8px; + } + + .board-column { + flex: 0 0 220px; + } + + .project-grid { + grid-template-columns: 1fr; + } +} diff --git a/admin-panel/src/server/agents/base-agent.ts b/admin-panel/src/server/agents/base-agent.ts new file mode 100644 index 0000000..f9e49f7 --- /dev/null +++ b/admin-panel/src/server/agents/base-agent.ts @@ -0,0 +1,83 @@ +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { callClaude, type ClaudeResponse } from '../services/claude.service.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TEMPLATES_DIR = join(__dirname, '..', 'templates'); + +export interface AgentContext { + taskId: number; + title: string; + description: string; + plan?: string; + decisionLog?: string; + doneWhen?: string; + implementationNotes?: string; + diff?: string; + testResults?: string; + reviewComments?: string; + branchName?: string; + repoPath?: string; + workspacePath?: string; + fileList?: string; + fileContents?: string; +} + +export interface AgentResult { + success: boolean; + content: string; + tokensUsed: number; + model: string; + updates: Record<string, unknown>; // Fields to update on the task + verdict?: 'approve' | 'reject'; // For review agents + message: string; // Log message +} + +export abstract class BaseAgent { + abstract name: string; + abstract templateFile: string; + abstract model: string; + + protected loadTemplate(): string { + return readFileSync(join(TEMPLATES_DIR, this.templateFile), 'utf-8'); + } + + protected fillTemplate(template: string, ctx: AgentContext): string { + return template + .replace(/\{\{taskId\}\}/g, String(ctx.taskId)) + .replace(/\{\{title\}\}/g, ctx.title || '') + .replace(/\{\{description\}\}/g, ctx.description || '') + .replace(/\{\{plan\}\}/g, ctx.plan || '') + .replace(/\{\{decision_log\}\}/g, ctx.decisionLog || '') + .replace(/\{\{done_when\}\}/g, ctx.doneWhen || '') + .replace(/\{\{implementation_notes\}\}/g, ctx.implementationNotes || '') + .replace(/\{\{diff\}\}/g, ctx.diff || '') + .replace(/\{\{test_results\}\}/g, ctx.testResults || '') + .replace(/\{\{review_comments\}\}/g, ctx.reviewComments || '') + .replace(/\{\{branch_name\}\}/g, ctx.branchName || '') + .replace(/\{\{file_list\}\}/g, ctx.fileList || '') + .replace(/\{\{file_contents\}\}/g, ctx.fileContents || ''); + } + + async execute(ctx: AgentContext): Promise<AgentResult> { + const template = this.loadTemplate(); + const prompt = this.fillTemplate(template, ctx); + + const response = await callClaude({ + model: this.model, + systemPrompt: this.getSystemPrompt(), + userMessage: prompt, + maxTokens: this.getMaxTokens(), + }); + + return this.parseResponse(response, ctx); + } + + protected abstract getSystemPrompt(): string; + protected abstract parseResponse(response: ClaudeResponse, ctx: AgentContext): AgentResult; + + protected getMaxTokens(): number { + return 8192; + } +} diff --git a/admin-panel/src/server/agents/builder.ts b/admin-panel/src/server/agents/builder.ts new file mode 100644 index 0000000..4e555be --- /dev/null +++ b/admin-panel/src/server/agents/builder.ts @@ -0,0 +1,64 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class BuilderAgent extends BaseAgent { + name = 'builder'; + templateFile = 'builder.md'; + model = 'claude-opus-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are an expert software engineer. Write clean, correct, production-quality code. Follow the plan precisely. Output complete file contents — never use placeholders or ellipses.'; + } + + protected getMaxTokens(): number { + return 16384; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + + // Extract files from ```FILE: path``` blocks + const files = parseFileBlocks(content); + const summary = extractSection(content, 'Summary') || ''; + const implNotes = extractSection(content, 'Implementation Notes') || ''; + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates: { + implementation_notes: implNotes || summary, + status: 'impl_review', + // files are handled by pipeline.service.ts which reads them from content + }, + message: `Implemented ${files.length} file(s)`, + }; + } +} + +export interface FileChange { + path: string; + content: string; +} + +export function parseFileBlocks(text: string): FileChange[] { + const files: FileChange[] = []; + const regex = /```FILE:\s*(.+?)\n([\s\S]*?)```/g; + let match; + + while ((match = regex.exec(text)) !== null) { + files.push({ + path: match[1].trim(), + content: match[2], + }); + } + + return files; +} + +function extractSection(text: string, heading: string): string | null { + const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = text.match(regex); + return match ? match[1].trim() : null; +} diff --git a/admin-panel/src/server/agents/critic.ts b/admin-panel/src/server/agents/critic.ts new file mode 100644 index 0000000..b0613fb --- /dev/null +++ b/admin-panel/src/server/agents/critic.ts @@ -0,0 +1,55 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class CriticAgent extends BaseAgent { + name = 'critic'; + templateFile = 'critic.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a thorough plan reviewer. Be constructive but rigorous. Approve plans that are solid enough to implement, reject plans with significant gaps or flaws.'; + } + + protected parseResponse(response: ClaudeResponse, ctx: AgentContext): AgentResult { + const content = response.content; + const firstLine = content.split('\n')[0].trim().toUpperCase(); + const approved = firstLine.includes('APPROVE'); + const rejected = firstLine.includes('REJECT'); + const verdict: 'approve' | 'reject' = approved ? 'approve' : 'reject'; + + // Build review entry + const reviewEntry = { + agent: 'critic', + verdict, + content, + timestamp: new Date().toISOString(), + }; + + const updates: Record<string, unknown> = {}; + const currentReviews = ctx.reviewComments ? JSON.parse(ctx.reviewComments || '[]') : []; + currentReviews.push(reviewEntry); + updates.plan_review_comments = JSON.stringify(currentReviews); + + if (approved) { + updates.status = 'impl'; + updates.plan_review_count = (ctx as unknown as Record<string, number>).plan_review_count + ? (ctx as unknown as Record<string, number>).plan_review_count + 1 + : 1; + } else if (rejected) { + updates.status = 'plan'; // Send back to planning + updates.plan_review_count = (ctx as unknown as Record<string, number>).plan_review_count + ? (ctx as unknown as Record<string, number>).plan_review_count + 1 + : 1; + } + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates, + verdict, + message: `Plan review: ${verdict.toUpperCase()}`, + }; + } +} diff --git a/admin-panel/src/server/agents/inspector.ts b/admin-panel/src/server/agents/inspector.ts new file mode 100644 index 0000000..575c34e --- /dev/null +++ b/admin-panel/src/server/agents/inspector.ts @@ -0,0 +1,54 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class InspectorAgent extends BaseAgent { + name = 'inspector'; + templateFile = 'inspector.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a senior code reviewer. Be thorough but pragmatic. Focus on correctness and security. Approve code that is production-ready, reject code with significant issues.'; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + const firstLine = content.split('\n')[0].trim().toUpperCase(); + const approved = firstLine.includes('APPROVE'); + const verdict: 'approve' | 'reject' = approved ? 'approve' : 'reject'; + + const reviewEntry = { + agent: 'inspector', + verdict, + content, + timestamp: new Date().toISOString(), + }; + + const updates: Record<string, unknown> = {}; + + // Parse existing review comments + let currentReviews: unknown[]; + try { + currentReviews = JSON.parse(_ctx.reviewComments || '[]'); + } catch { + currentReviews = []; + } + currentReviews.push(reviewEntry); + updates.review_comments = JSON.stringify(currentReviews); + + if (approved) { + updates.status = 'test'; + } else { + updates.status = 'impl'; // Send back to builder + } + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates, + verdict, + message: `Code review: ${verdict.toUpperCase()}`, + }; + } +} diff --git a/admin-panel/src/server/agents/planner.ts b/admin-panel/src/server/agents/planner.ts new file mode 100644 index 0000000..eb004f9 --- /dev/null +++ b/admin-panel/src/server/agents/planner.ts @@ -0,0 +1,45 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class PlannerAgent extends BaseAgent { + name = 'planner'; + templateFile = 'planner.md'; + model = 'claude-opus-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a meticulous software architect who creates clear, actionable implementation plans. Be thorough but concise. Focus on practical steps, not theory.'; + } + + protected getMaxTokens(): number { + return 16384; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + + // Extract sections + const plan = content; + const doneWhen = extractSection(content, 'Done When') || ''; + const decisionLog = extractSection(content, 'Decisions') || ''; + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates: { + plan, + done_when: doneWhen, + decision_log: decisionLog, + status: 'plan_review', + }, + message: 'Generated implementation plan', + }; + } +} + +function extractSection(text: string, heading: string): string | null { + const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = text.match(regex); + return match ? match[1].trim() : null; +} diff --git a/admin-panel/src/server/agents/ranger.ts b/admin-panel/src/server/agents/ranger.ts new file mode 100644 index 0000000..f1f3918 --- /dev/null +++ b/admin-panel/src/server/agents/ranger.ts @@ -0,0 +1,54 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class RangerAgent extends BaseAgent { + name = 'ranger'; + templateFile = 'ranger.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a QA engineer making the final pass/fail decision. Be fair but thorough. Only pass tasks that genuinely meet their acceptance criteria.'; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + const firstLine = content.split('\n')[0].trim().toUpperCase(); + const passed = firstLine.includes('PASS'); + const verdict: 'approve' | 'reject' = passed ? 'approve' : 'reject'; + + const testEntry = { + agent: 'ranger', + verdict: passed ? 'pass' : 'fail', + content, + timestamp: new Date().toISOString(), + }; + + const updates: Record<string, unknown> = {}; + + let currentResults: unknown[]; + try { + currentResults = JSON.parse(_ctx.testResults || '[]'); + } catch { + currentResults = []; + } + currentResults.push(testEntry); + updates.test_results = JSON.stringify(currentResults); + + if (passed) { + updates.status = 'done'; + updates.completed_at = new Date().toISOString(); + } else { + updates.status = 'impl'; // Send back to builder + } + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates, + verdict, + message: `Final verdict: ${passed ? 'PASS' : 'FAIL'}`, + }; + } +} diff --git a/admin-panel/src/server/agents/shield.ts b/admin-panel/src/server/agents/shield.ts new file mode 100644 index 0000000..2ce0bc1 --- /dev/null +++ b/admin-panel/src/server/agents/shield.ts @@ -0,0 +1,62 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class ShieldAgent extends BaseAgent { + name = 'shield'; + templateFile = 'shield.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a test engineer who writes thorough, practical tests. Focus on verifying requirements and catching regressions. Use appropriate testing frameworks.'; + } + + protected getMaxTokens(): number { + return 12288; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + + // Extract test files + const files = parseTestFiles(content); + const strategy = extractSection(content, 'Test Strategy') || ''; + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates: { + // Test files are written by pipeline service + // Test results will be populated after running + }, + message: `Generated ${files.length} test file(s)`, + }; + } +} + +interface TestFile { + path: string; + content: string; +} + +function parseTestFiles(text: string): TestFile[] { + const files: TestFile[] = []; + const regex = /```FILE:\s*(.+?)\n([\s\S]*?)```/g; + let match; + + while ((match = regex.exec(text)) !== null) { + files.push({ + path: match[1].trim(), + content: match[2], + }); + } + + return files; +} + +function extractSection(text: string, heading: string): string | null { + const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = text.match(regex); + return match ? match[1].trim() : null; +} diff --git a/admin-panel/src/server/db.ts b/admin-panel/src/server/db.ts new file mode 100644 index 0000000..9c83031 --- /dev/null +++ b/admin-panel/src/server/db.ts @@ -0,0 +1,82 @@ +import Database from 'better-sqlite3'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { mkdirSync, existsSync } from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DATA_DIR = join(process.cwd(), 'data'); +const SCHEMA_PATH = join(__dirname, 'schema.sql'); + +const dbCache = new Map<string, Database.Database>(); + +function ensureDataDir(): void { + if (!existsSync(DATA_DIR)) { + mkdirSync(DATA_DIR, { recursive: true }); + } +} + +function loadSchema(): { globalSchema: string; projectSchema: string } { + const full = readFileSync(SCHEMA_PATH, 'utf-8'); + const marker = '-- === PROJECT_SCHEMA ==='; + const idx = full.indexOf(marker); + if (idx === -1) { + return { globalSchema: full, projectSchema: '' }; + } + return { + globalSchema: full.substring(0, idx).trim(), + projectSchema: full.substring(idx + marker.length).trim(), + }; +} + +export function getAdminDb(): Database.Database { + if (dbCache.has('admin')) { + return dbCache.get('admin')!; + } + ensureDataDir(); + const db = new Database(join(DATA_DIR, 'admin.db')); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + + const { globalSchema } = loadSchema(); + db.exec(globalSchema); + + dbCache.set('admin', db); + return db; +} + +export function getProjectDb(projectName: string): Database.Database { + if (dbCache.has(projectName)) { + return dbCache.get(projectName)!; + } + ensureDataDir(); + const db = new Database(join(DATA_DIR, `${projectName}.db`)); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + + const { projectSchema } = loadSchema(); + db.exec(projectSchema); + + dbCache.set(projectName, db); + return db; +} + +export function closeAll(): void { + for (const [name, db] of dbCache) { + db.close(); + dbCache.delete(name); + } +} + +export function getSetting(key: string): string | undefined { + const db = getAdminDb(); + const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as + | { value: string } + | undefined; + return row?.value; +} + +export function setSetting(key: string, value: string): void { + const db = getAdminDb(); + db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(key, value); +} diff --git a/admin-panel/src/server/index.ts b/admin-panel/src/server/index.ts new file mode 100644 index 0000000..f76602e --- /dev/null +++ b/admin-panel/src/server/index.ts @@ -0,0 +1,46 @@ +import express from 'express'; +import { createServer } from 'http'; +import { join, dirname } from 'path'; +import { existsSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { setupWebSocket } from './ws.js'; +import projectsRouter from './routes/projects.js'; +import tasksRouter from './routes/tasks.js'; +import pipelineRouter from './routes/pipeline.js'; +import gerritRouter from './routes/gerrit.js'; +import dashboardRouter from './routes/dashboard.js'; +import settingsRouter from './routes/settings.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const app = express(); +const server = createServer(app); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json()); + +// API routes +app.use('/api/projects', projectsRouter); +app.use('/api/projects/:project/tasks', tasksRouter); +app.use('/api/projects/:project/pipeline', pipelineRouter); +app.use('/api/projects/:project/gerrit', gerritRouter); +app.use('/api/dashboard', dashboardRouter); +app.use('/api/settings', settingsRouter); + +// Serve static client in production +const clientDir = join(__dirname, '..', 'client'); +if (existsSync(clientDir)) { + app.use(express.static(clientDir)); + app.get('*', (_req, res) => { + res.sendFile(join(clientDir, 'index.html')); + }); +} + +// WebSocket +setupWebSocket(server); + +server.listen(PORT, () => { + console.log(`Admin panel running on port ${PORT}`); +}); + +export { app, server }; diff --git a/admin-panel/src/server/routes/dashboard.ts b/admin-panel/src/server/routes/dashboard.ts new file mode 100644 index 0000000..3142e8e --- /dev/null +++ b/admin-panel/src/server/routes/dashboard.ts @@ -0,0 +1,138 @@ +import { Router, Request, Response } from 'express'; +import { getAdminDb, getProjectDb } from '../db.js'; + +const router = Router(); + +interface Project { + id: number; + name: string; + display_name: string; +} + +// GET /api/dashboard/stats +router.get('/stats', (_req: Request, res: Response) => { + const adminDb = getAdminDb(); + const projects = adminDb.prepare('SELECT * FROM projects').all() as Project[]; + + let totalTasks = 0; + let completedToday = 0; + const tasksByStatus: Record<string, number> = {}; + + const today = new Date().toISOString().split('T')[0]; + + for (const project of projects) { + try { + const db = getProjectDb(project.name); + const tasks = db.prepare('SELECT status, completed_at FROM tasks').all() as Array<{ + status: string; + completed_at: string | null; + }>; + + totalTasks += tasks.length; + + for (const task of tasks) { + tasksByStatus[task.status] = (tasksByStatus[task.status] || 0) + 1; + if (task.completed_at && task.completed_at.startsWith(today)) { + completedToday++; + } + } + } catch { + // Project DB might not exist yet + } + } + + res.json({ + totalProjects: projects.length, + totalTasks, + tasksByStatus, + completedToday, + }); +}); + +// GET /api/dashboard/activity +router.get('/activity', (_req: Request, res: Response) => { + const adminDb = getAdminDb(); + const projects = adminDb.prepare('SELECT * FROM projects').all() as Project[]; + + const activity: Array<{ + project: string; + taskId: number; + taskTitle: string; + agent: string; + action: string; + timestamp: string; + }> = []; + + for (const project of projects) { + try { + const db = getProjectDb(project.name); + const tasks = db.prepare('SELECT id, title, agent_log FROM tasks WHERE agent_log != \'[]\'').all() as Array<{ + id: number; + title: string; + agent_log: string; + }>; + + for (const task of tasks) { + try { + const logs = JSON.parse(task.agent_log) as Array<{ + agent: string; + message: string; + timestamp: string; + }>; + for (const log of logs) { + activity.push({ + project: project.name, + taskId: task.id, + taskTitle: task.title, + agent: log.agent, + action: log.message, + timestamp: log.timestamp, + }); + } + } catch { + // Skip malformed logs + } + } + } catch { + // Project DB might not exist + } + } + + // Sort by timestamp descending, take 20 most recent + activity.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); + res.json(activity.slice(0, 20)); +}); + +// GET /api/projects/:project/stats +router.get('/projects/:project', (req: Request, res: Response) => { + const { project } = req.params; + + const adminDb = getAdminDb(); + if (!adminDb.prepare('SELECT id FROM projects WHERE name = ?').get(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + try { + const db = getProjectDb(project as string); + const tasks = db.prepare('SELECT status FROM tasks').all() as Array<{ status: string }>; + const tasksByStatus: Record<string, number> = {}; + for (const task of tasks) { + tasksByStatus[task.status] = (tasksByStatus[task.status] || 0) + 1; + } + + const totalJobs = db.prepare('SELECT COUNT(*) as count FROM pipeline_jobs').get() as { count: number }; + const totalTokens = db.prepare('SELECT SUM(tokens_used) as total FROM pipeline_jobs').get() as { total: number | null }; + + res.json({ + totalTasks: tasks.length, + tasksByStatus, + totalPipelineJobs: totalJobs.count, + totalTokensUsed: totalTokens.total || 0, + }); + } catch { + res.json({ totalTasks: 0, tasksByStatus: {}, totalPipelineJobs: 0, totalTokensUsed: 0 }); + } +}); + +export default router; diff --git a/admin-panel/src/server/routes/gerrit.ts b/admin-panel/src/server/routes/gerrit.ts new file mode 100644 index 0000000..b48ea53 --- /dev/null +++ b/admin-panel/src/server/routes/gerrit.ts @@ -0,0 +1,68 @@ +import { Router, Request, Response } from 'express'; +import { getAdminDb, getProjectDb } from '../db.js'; +import { getChange, getChangeByNumber } from '../services/gerrit.service.js'; + +const router = Router({ mergeParams: true }); + +type Params = { project: string; taskId: string }; + +// GET /api/projects/:project/gerrit/:taskId +router.get('/:taskId', async (req: Request<Params>, res: Response) => { + const { project, taskId } = req.params; + + const adminDb = getAdminDb(); + if (!adminDb.prepare('SELECT id FROM projects WHERE name = ?').get(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const task = db.prepare('SELECT gerrit_change_id, gerrit_change_number FROM tasks WHERE id = ?').get(parseInt(taskId)) as { + gerrit_change_id: string | null; + gerrit_change_number: number | null; + } | undefined; + + if (!task) { + res.status(404).json({ error: 'Task not found' }); + return; + } + + if (!task.gerrit_change_id && !task.gerrit_change_number) { + res.json({ change: null }); + return; + } + + try { + const change = task.gerrit_change_id + ? await getChange(task.gerrit_change_id) + : task.gerrit_change_number + ? await getChangeByNumber(task.gerrit_change_number) + : null; + res.json({ change }); + } catch (err) { + res.status(502).json({ error: `Gerrit API error: ${(err as Error).message}` }); + } +}); + +// GET /api/projects/:project/diff/:taskId +router.get('/diff/:taskId', (req: Request<Params>, res: Response) => { + const { project, taskId } = req.params; + + const adminDb = getAdminDb(); + if (!adminDb.prepare('SELECT id FROM projects WHERE name = ?').get(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const task = db.prepare('SELECT diff FROM tasks WHERE id = ?').get(parseInt(taskId)) as { diff: string | null } | undefined; + + if (!task) { + res.status(404).json({ error: 'Task not found' }); + return; + } + + res.json({ diff: task.diff || '' }); +}); + +export default router; diff --git a/admin-panel/src/server/routes/pipeline.ts b/admin-panel/src/server/routes/pipeline.ts new file mode 100644 index 0000000..39df35c --- /dev/null +++ b/admin-panel/src/server/routes/pipeline.ts @@ -0,0 +1,82 @@ +import { Router, Request, Response } from 'express'; +import { getAdminDb } from '../db.js'; +import { + runFullPipeline, + runStep, + stopPipeline, + getPipelineStatus, + isPipelineRunning, +} from '../services/pipeline.service.js'; + +const router = Router({ mergeParams: true }); + +type Params = { project: string; taskId: string }; + +function projectExists(name: string): boolean { + const db = getAdminDb(); + return !!db.prepare('SELECT id FROM projects WHERE name = ?').get(name); +} + +// POST /api/projects/:project/pipeline/run/:taskId +router.post('/run/:taskId', async (req: Request<Params>, res: Response) => { + const { project, taskId } = req.params; + if (!projectExists(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const id = parseInt(taskId); + if (isPipelineRunning(project, id)) { + res.status(409).json({ error: 'Pipeline already running for this task' }); + return; + } + + // Run async — don't block the response + runFullPipeline(project, id).catch(err => { + console.error(`Pipeline error for ${project}/${taskId}:`, err); + }); + + res.json({ status: 'started' }); +}); + +// POST /api/projects/:project/pipeline/step/:taskId +router.post('/step/:taskId', async (req: Request<Params>, res: Response) => { + const { project, taskId } = req.params; + if (!projectExists(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + try { + await runStep(project, parseInt(taskId)); + res.json({ status: 'completed' }); + } catch (err) { + res.status(400).json({ error: (err as Error).message }); + } +}); + +// POST /api/projects/:project/pipeline/stop/:taskId +router.post('/stop/:taskId', (req: Request<Params>, res: Response) => { + const { project, taskId } = req.params; + if (!projectExists(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + stopPipeline(project, parseInt(taskId)); + res.json({ status: 'stopping' }); +}); + +// GET /api/projects/:project/pipeline/status/:taskId +router.get('/status/:taskId', (req: Request<Params>, res: Response) => { + const { project, taskId } = req.params; + if (!projectExists(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const status = getPipelineStatus(project, parseInt(taskId)); + res.json(status); +}); + +export default router; diff --git a/admin-panel/src/server/routes/projects.ts b/admin-panel/src/server/routes/projects.ts new file mode 100644 index 0000000..b2e63a8 --- /dev/null +++ b/admin-panel/src/server/routes/projects.ts @@ -0,0 +1,138 @@ +import { Router, Request, Response } from 'express'; +import { getAdminDb, getProjectDb } from '../db.js'; +import { createBareRepo, setRepoDescription, repoExists, deleteRepo } from '../services/git.service.js'; + +const router = Router(); + +interface Project { + id: number; + name: string; + display_name: string; + description: string | null; + repo_name: string; + repo_path: string; + default_branch: string; + created_at: string; + updated_at: string; +} + +// GET /api/projects +router.get('/', (_req: Request, res: Response) => { + const db = getAdminDb(); + const projects = db.prepare('SELECT * FROM projects ORDER BY created_at DESC').all(); + res.json(projects); +}); + +// POST /api/projects +router.post('/', (req: Request, res: Response) => { + const { name, display_name, description } = req.body; + + if (!name || !display_name) { + res.status(400).json({ error: 'name and display_name are required' }); + return; + } + + // Sanitize name: lowercase, alphanumeric + hyphens + const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + if (!safeName) { + res.status(400).json({ error: 'Invalid project name' }); + return; + } + + const repoName = `${safeName}.git`; + const db = getAdminDb(); + + // Check for duplicate + const existing = db.prepare('SELECT id FROM projects WHERE name = ?').get(safeName); + if (existing) { + res.status(409).json({ error: 'Project already exists' }); + return; + } + + // Create bare git repo + const repo = createBareRepo(repoName); + if (description) { + setRepoDescription(repoName, description); + } + + // Insert into admin DB + const result = db.prepare( + 'INSERT INTO projects (name, display_name, description, repo_name, repo_path) VALUES (?, ?, ?, ?, ?)' + ).run(safeName, display_name, description || null, repoName, repo.path); + + // Initialize project DB (creates tables) + getProjectDb(safeName); + + const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(result.lastInsertRowid); + res.status(201).json(project); +}); + +// GET /api/projects/:name +router.get('/:name', (req: Request, res: Response) => { + const db = getAdminDb(); + const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined; + if (!project) { + res.status(404).json({ error: 'Project not found' }); + return; + } + res.json(project); +}); + +// PATCH /api/projects/:name +router.patch('/:name', (req: Request, res: Response) => { + const db = getAdminDb(); + const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined; + if (!project) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const { display_name, description } = req.body; + const updates: string[] = []; + const params: unknown[] = []; + + if (display_name !== undefined) { + updates.push('display_name = ?'); + params.push(display_name); + } + if (description !== undefined) { + updates.push('description = ?'); + params.push(description); + if (description) { + setRepoDescription(project.repo_name, description); + } + } + + if (updates.length === 0) { + res.json(project); + return; + } + + updates.push("updated_at = datetime('now')"); + params.push(req.params.name); + + db.prepare(`UPDATE projects SET ${updates.join(', ')} WHERE name = ?`).run(...params); + const updated = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name); + res.json(updated); +}); + +// DELETE /api/projects/:name +router.delete('/:name', (req: Request, res: Response) => { + const db = getAdminDb(); + const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined; + if (!project) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + db.prepare('DELETE FROM projects WHERE name = ?').run(req.params.name); + + // Optionally delete repo (controlled by query param) + if (req.query.delete_repo === 'true') { + deleteRepo(project.repo_name); + } + + res.status(204).send(); +}); + +export default router; diff --git a/admin-panel/src/server/routes/settings.ts b/admin-panel/src/server/routes/settings.ts new file mode 100644 index 0000000..4eed6f1 --- /dev/null +++ b/admin-panel/src/server/routes/settings.ts @@ -0,0 +1,35 @@ +import { Router, Request, Response } from 'express'; +import { getAdminDb } from '../db.js'; + +const router = Router(); + +// GET /api/settings +router.get('/', (_req: Request, res: Response) => { + const db = getAdminDb(); + const rows = db.prepare('SELECT key, value FROM settings').all() as Array<{ + key: string; + value: string; + }>; + const settings: Record<string, string> = {}; + for (const row of rows) { + settings[row.key] = row.value; + } + res.json(settings); +}); + +// PATCH /api/settings +router.patch('/', (req: Request, res: Response) => { + const db = getAdminDb(); + const stmt = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)'); + + const updates = req.body as Record<string, string>; + for (const [key, value] of Object.entries(updates)) { + if (typeof key === 'string' && typeof value === 'string') { + stmt.run(key, value); + } + } + + res.status(204).send(); +}); + +export default router; diff --git a/admin-panel/src/server/routes/tasks.ts b/admin-panel/src/server/routes/tasks.ts new file mode 100644 index 0000000..1ab1e8d --- /dev/null +++ b/admin-panel/src/server/routes/tasks.ts @@ -0,0 +1,248 @@ +import { Router, Request, Response } from 'express'; +import { getAdminDb, getProjectDb } from '../db.js'; +import { broadcast } from '../ws.js'; + +const router = Router({ mergeParams: true }); + +type Params = { project: string; id?: string }; + +interface Task { + id: number; + title: string; + status: string; + priority: string; + level: number; + description: string | null; + plan: string | null; + decision_log: string | null; + done_when: string | null; + implementation_notes: string | null; + tags: string; + plan_review_comments: string; + review_comments: string; + test_results: string; + agent_log: string; + current_agent: string | null; + plan_review_count: number; + impl_review_count: number; + branch_name: string | null; + gerrit_change_id: string | null; + gerrit_change_number: number | null; + diff: string | null; + rank: number; + created_at: string; + started_at: string | null; + planned_at: string | null; + reviewed_at: string | null; + tested_at: string | null; + completed_at: string | null; +} + +const VALID_STATUSES = ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done']; +const VALID_PRIORITIES = ['high', 'medium', 'low']; + +function getProject(projectName: string): boolean { + const db = getAdminDb(); + return !!db.prepare('SELECT id FROM projects WHERE name = ?').get(projectName); +} + +// GET /api/projects/:project/tasks +router.get('/', (req: Request<Params>, res: Response) => { + const { project } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const tasks = db.prepare('SELECT * FROM tasks ORDER BY rank ASC, id ASC').all(); + res.json(tasks); +}); + +// POST /api/projects/:project/tasks +router.post('/', (req: Request<Params>, res: Response) => { + const { project } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const { title, description, priority, level, tags, status } = req.body; + if (!title) { + res.status(400).json({ error: 'title is required' }); + return; + } + + const db = getProjectDb(project); + + // Get max rank for ordering + const maxRank = (db.prepare('SELECT MAX(rank) as max FROM tasks').get() as { max: number | null })?.max || 0; + + const result = db.prepare( + `INSERT INTO tasks (title, description, priority, level, tags, status, rank) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ).run( + title, + description || null, + VALID_PRIORITIES.includes(priority) ? priority : 'medium', + [1, 2, 3].includes(level) ? level : 3, + JSON.stringify(tags || []), + VALID_STATUSES.includes(status) ? status : 'todo', + maxRank + 1 + ); + + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid); + broadcast(project, { type: 'task:created', project, task }); + res.status(201).json(task); +}); + +// GET /api/projects/:project/tasks/:id +router.get('/:id', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + if (!task) { + res.status(404).json({ error: 'Task not found' }); + return; + } + res.json(task); +}); + +// PATCH /api/projects/:project/tasks/:id +router.patch('/:id', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id) as Task | undefined; + if (!existing) { + res.status(404).json({ error: 'Task not found' }); + return; + } + + const allowedFields = [ + 'title', 'status', 'priority', 'level', 'description', 'plan', + 'decision_log', 'done_when', 'implementation_notes', 'tags', + 'plan_review_comments', 'review_comments', 'test_results', + 'agent_log', 'current_agent', 'plan_review_count', 'impl_review_count', + 'branch_name', 'gerrit_change_id', 'gerrit_change_number', 'diff', 'rank' + ]; + + const updates: string[] = []; + const params: unknown[] = []; + + for (const field of allowedFields) { + if (req.body[field] !== undefined) { + let value = req.body[field]; + + // Validate specific fields + if (field === 'status' && !VALID_STATUSES.includes(value)) continue; + if (field === 'priority' && !VALID_PRIORITIES.includes(value)) continue; + if (field === 'level' && ![1, 2, 3].includes(value)) continue; + + // Stringify JSON fields + if (['tags', 'plan_review_comments', 'review_comments', 'test_results', 'agent_log'].includes(field)) { + value = typeof value === 'string' ? value : JSON.stringify(value); + } + + updates.push(`${field} = ?`); + params.push(value); + } + } + + // Set timestamp fields based on status changes + if (req.body.status) { + const now = new Date().toISOString(); + switch (req.body.status) { + case 'plan': + if (!existing.started_at) { + updates.push('started_at = ?'); + params.push(now); + } + break; + case 'impl': + updates.push('planned_at = ?'); + params.push(now); + break; + case 'test': + updates.push('reviewed_at = ?'); + params.push(now); + break; + case 'done': + updates.push('completed_at = ?'); + params.push(now); + break; + } + } + + if (updates.length === 0) { + res.json(existing); + return; + } + + params.push(id); + db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...params); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + broadcast(project, { type: 'task:updated', project, task }); + res.json(task); +}); + +// DELETE /api/projects/:project/tasks/:id +router.delete('/:id', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + if (!task) { + res.status(404).json({ error: 'Task not found' }); + return; + } + + db.prepare('DELETE FROM tasks WHERE id = ?').run(id); + broadcast(project, { type: 'task:deleted', project, taskId: Number(id) }); + res.status(204).send(); +}); + +// PATCH /api/projects/:project/tasks/:id/reorder +router.patch('/:id/reorder', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const { rank, status } = req.body; + if (rank === undefined) { + res.status(400).json({ error: 'rank is required' }); + return; + } + + const db = getProjectDb(project); + const updates: string[] = ['rank = ?']; + const params: unknown[] = [rank]; + + if (status && VALID_STATUSES.includes(status)) { + updates.push('status = ?'); + params.push(status); + } + + params.push(id); + db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...params); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + broadcast(project, { type: 'task:updated', project, task }); + res.json(task); +}); + +export default router; diff --git a/admin-panel/src/server/schema.sql b/admin-panel/src/server/schema.sql new file mode 100644 index 0000000..36c0e40 --- /dev/null +++ b/admin-panel/src/server/schema.sql @@ -0,0 +1,73 @@ +-- Global database schema (admin.db) + +CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, + description TEXT, + repo_name TEXT NOT NULL, + repo_path TEXT NOT NULL, + default_branch TEXT DEFAULT 'main', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +-- Default settings +INSERT OR IGNORE INTO settings (key, value) VALUES + ('anthropic_model_planning', 'claude-opus-4-20250514'), + ('anthropic_model_review', 'claude-sonnet-4-20250514'), + ('anthropic_model_implementation', 'claude-opus-4-20250514'), + ('anthropic_model_testing', 'claude-sonnet-4-20250514'); + +-- Per-project database schema (applied to {project-name}.db) +-- This comment marks where project schema starts; code splits on this marker. +-- === PROJECT_SCHEMA === + +CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'todo', + priority TEXT NOT NULL DEFAULT 'medium', + level INTEGER NOT NULL DEFAULT 3, + description TEXT, + plan TEXT, + decision_log TEXT, + done_when TEXT, + implementation_notes TEXT, + tags TEXT DEFAULT '[]', + plan_review_comments TEXT DEFAULT '[]', + review_comments TEXT DEFAULT '[]', + test_results TEXT DEFAULT '[]', + agent_log TEXT DEFAULT '[]', + current_agent TEXT, + plan_review_count INTEGER DEFAULT 0, + impl_review_count INTEGER DEFAULT 0, + branch_name TEXT, + gerrit_change_id TEXT, + gerrit_change_number INTEGER, + diff TEXT, + rank REAL DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + started_at TEXT, + planned_at TEXT, + reviewed_at TEXT, + tested_at TEXT, + completed_at TEXT +); + +CREATE TABLE IF NOT EXISTS pipeline_jobs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id INTEGER NOT NULL REFERENCES tasks(id), + agent TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + model TEXT NOT NULL, + started_at TEXT, + completed_at TEXT, + error TEXT, + tokens_used INTEGER DEFAULT 0 +); diff --git a/admin-panel/src/server/services/claude.service.ts b/admin-panel/src/server/services/claude.service.ts new file mode 100644 index 0000000..b9aae37 --- /dev/null +++ b/admin-panel/src/server/services/claude.service.ts @@ -0,0 +1,47 @@ +import Anthropic from '@anthropic-ai/sdk'; + +let client: Anthropic | null = null; + +function getClient(): Anthropic { + if (!client) { + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) throw new Error('ANTHROPIC_API_KEY not configured'); + client = new Anthropic({ apiKey }); + } + return client; +} + +export interface ClaudeResponse { + content: string; + tokensUsed: number; + model: string; +} + +export async function callClaude(opts: { + model: string; + systemPrompt: string; + userMessage: string; + maxTokens?: number; +}): Promise<ClaudeResponse> { + const anthropic = getClient(); + + const response = await anthropic.messages.create({ + model: opts.model, + max_tokens: opts.maxTokens || 8192, + system: opts.systemPrompt, + messages: [{ role: 'user', content: opts.userMessage }], + }); + + const content = response.content + .filter(block => block.type === 'text') + .map(block => (block as { type: 'text'; text: string }).text) + .join('\n'); + + const tokensUsed = (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0); + + return { + content, + tokensUsed, + model: opts.model, + }; +} diff --git a/admin-panel/src/server/services/gerrit.service.ts b/admin-panel/src/server/services/gerrit.service.ts new file mode 100644 index 0000000..53c8f13 --- /dev/null +++ b/admin-panel/src/server/services/gerrit.service.ts @@ -0,0 +1,81 @@ +const GERRIT_URL = process.env.GERRIT_URL || 'http://gerrit:8080'; + +export interface GerritChange { + id: string; + change_id: string; + _number: number; + subject: string; + status: string; + created: string; + updated: string; + mergeable?: boolean; + labels?: Record<string, unknown>; +} + +export interface GerritReview { + message: string; + labels?: Record<string, number>; + comments?: Record<string, Array<{ line: number; message: string }>>; +} + +async function gerritFetch(path: string, options?: RequestInit): Promise<unknown> { + const url = `${GERRIT_URL}/a${path}`; + const res = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + ...options, + }); + + if (!res.ok) { + throw new Error(`Gerrit API error: ${res.status} ${res.statusText}`); + } + + const text = await res.text(); + // Gerrit prepends )]}' to JSON responses + const json = text.startsWith(")]}'") ? text.slice(4) : text; + return json ? JSON.parse(json) : null; +} + +export async function getChange(changeId: string): Promise<GerritChange | null> { + try { + return (await gerritFetch(`/changes/${encodeURIComponent(changeId)}`)) as GerritChange; + } catch { + return null; + } +} + +export async function getChangeByNumber(changeNumber: number): Promise<GerritChange | null> { + try { + return (await gerritFetch(`/changes/${changeNumber}`)) as GerritChange; + } catch { + return null; + } +} + +export async function postReview(changeId: string, review: GerritReview): Promise<void> { + await gerritFetch(`/changes/${encodeURIComponent(changeId)}/revisions/current/review`, { + method: 'POST', + body: JSON.stringify(review), + }); +} + +export async function getChangeDiff(changeId: string): Promise<string> { + const result = await gerritFetch( + `/changes/${encodeURIComponent(changeId)}/revisions/current/patch` + ); + // Patch is base64 encoded + if (typeof result === 'string') { + return Buffer.from(result, 'base64').toString('utf-8'); + } + return ''; +} + +export async function queryChanges(query: string): Promise<GerritChange[]> { + try { + return (await gerritFetch(`/changes/?q=${encodeURIComponent(query)}`)) as GerritChange[]; + } catch { + return []; + } +} diff --git a/admin-panel/src/server/services/git.service.ts b/admin-panel/src/server/services/git.service.ts new file mode 100644 index 0000000..990f5df --- /dev/null +++ b/admin-panel/src/server/services/git.service.ts @@ -0,0 +1,142 @@ +import { execSync } from 'child_process'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; + +const REPO_BASE = process.env.GIT_REPO_PATH || '/repos'; + +export interface RepoInfo { + name: string; + path: string; + exists: boolean; +} + +export function createBareRepo(repoName: string, defaultBranch = 'main'): RepoInfo { + const repoPath = join(REPO_BASE, repoName); + + if (existsSync(repoPath)) { + return { name: repoName, path: repoPath, exists: true }; + } + + execSync(`git init --bare "${repoPath}"`, { stdio: 'pipe' }); + execSync(`git -C "${repoPath}" symbolic-ref HEAD refs/heads/${defaultBranch}`, { + stdio: 'pipe', + }); + + // Set description for cgit + const descPath = join(repoPath, 'description'); + execSync(`echo "Repository managed by admin-panel" > "${descPath}"`, { stdio: 'pipe' }); + + // Enable post-update hook for git daemon + const hookPath = join(repoPath, 'hooks', 'post-update'); + if (!existsSync(hookPath)) { + execSync(`cp "${repoPath}/hooks/post-update.sample" "${hookPath}" 2>/dev/null || true`, { + stdio: 'pipe', + }); + } + + // Run git update-server-info for HTTP access + execSync(`git -C "${repoPath}" update-server-info`, { stdio: 'pipe' }); + + return { name: repoName, path: repoPath, exists: false }; +} + +export function setRepoDescription(repoName: string, description: string): void { + const descPath = join(REPO_BASE, repoName, 'description'); + execSync(`echo "${description.replace(/"/g, '\\"')}" > "${descPath}"`, { stdio: 'pipe' }); +} + +export function repoExists(repoName: string): boolean { + return existsSync(join(REPO_BASE, repoName)); +} + +export function deleteRepo(repoName: string): void { + const repoPath = join(REPO_BASE, repoName); + if (existsSync(repoPath)) { + execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' }); + } +} + +// --- Workspace operations for Builder agent --- + +export async function cloneRepo(bareRepoPath: string, workspacePath: string): Promise<void> { + if (existsSync(workspacePath)) { + // Pull latest instead of re-cloning + execSync(`git -C "${workspacePath}" fetch origin`, { stdio: 'pipe' }); + execSync(`git -C "${workspacePath}" reset --hard origin/main 2>/dev/null || true`, { stdio: 'pipe' }); + return; + } + mkdirSync(workspacePath, { recursive: true }); + execSync(`git clone "${bareRepoPath}" "${workspacePath}"`, { stdio: 'pipe' }); + + // Configure git user for commits + execSync(`git -C "${workspacePath}" config user.email "admin-panel@swave.lol"`, { stdio: 'pipe' }); + execSync(`git -C "${workspacePath}" config user.name "Admin Panel"`, { stdio: 'pipe' }); +} + +export async function createBranch(workspacePath: string, branchName: string): Promise<void> { + // Create or switch to branch + try { + execSync(`git -C "${workspacePath}" checkout -b "${branchName}"`, { stdio: 'pipe' }); + } catch { + // Branch might already exist + execSync(`git -C "${workspacePath}" checkout "${branchName}"`, { stdio: 'pipe' }); + } +} + +export async function writeFiles( + workspacePath: string, + files: Array<{ path: string; content: string }> +): Promise<void> { + for (const file of files) { + const fullPath = join(workspacePath, file.path); + mkdirSync(dirname(fullPath), { recursive: true }); + writeFileSync(fullPath, file.content, 'utf-8'); + } +} + +export async function commitAndDiff( + workspacePath: string, + message: string, + branchName: string +): Promise<string> { + execSync(`git -C "${workspacePath}" add -A`, { stdio: 'pipe' }); + + // Check if there are changes to commit + try { + execSync(`git -C "${workspacePath}" diff --cached --quiet`, { stdio: 'pipe' }); + // No changes + return ''; + } catch { + // There are changes — commit them + } + + execSync(`git -C "${workspacePath}" commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'pipe' }); + + // Get diff against main + const diff = execSync( + `git -C "${workspacePath}" diff main..${branchName} 2>/dev/null || git -C "${workspacePath}" diff HEAD~1..HEAD`, + { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 } + ); + + return diff; +} + +export async function pushToGerrit(workspacePath: string): Promise<string> { + const output = execSync( + `git -C "${workspacePath}" push origin HEAD:refs/for/main 2>&1`, + { encoding: 'utf-8' } + ); + return output; +} + +export function getFileList(workspacePath: string): string { + if (!existsSync(workspacePath)) return ''; + try { + return execSync(`find "${workspacePath}" -type f -not -path "*/.git/*" | sort`, { + encoding: 'utf-8', + maxBuffer: 1024 * 1024, + }); + } catch { + return ''; + } +} diff --git a/admin-panel/src/server/services/pipeline.service.ts b/admin-panel/src/server/services/pipeline.service.ts new file mode 100644 index 0000000..afdf422 --- /dev/null +++ b/admin-panel/src/server/services/pipeline.service.ts @@ -0,0 +1,304 @@ +import { getProjectDb } from '../db.js'; +import { broadcast } from '../ws.js'; +import { BaseAgent, type AgentContext } from '../agents/base-agent.js'; +import { PlannerAgent } from '../agents/planner.js'; +import { CriticAgent } from '../agents/critic.js'; +import { BuilderAgent } from '../agents/builder.js'; +import { ShieldAgent } from '../agents/shield.js'; +import { InspectorAgent } from '../agents/inspector.js'; +import { RangerAgent } from '../agents/ranger.js'; +import { parseFileBlocks } from '../agents/builder.js'; +import { cloneRepo, createBranch, writeFiles, commitAndDiff } from './git.service.js'; + +interface Task { + id: number; + title: string; + status: string; + priority: string; + level: number; + description: string | null; + plan: string | null; + decision_log: string | null; + done_when: string | null; + implementation_notes: string | null; + tags: string; + plan_review_comments: string; + review_comments: string; + test_results: string; + agent_log: string; + current_agent: string | null; + plan_review_count: number; + impl_review_count: number; + branch_name: string | null; + diff: string | null; + rank: number; +} + +// Maps status → which agent runs +const STATUS_AGENT_MAP: Record<string, () => BaseAgent> = { + todo: () => new PlannerAgent(), + plan: () => new PlannerAgent(), + plan_review: () => new CriticAgent(), + impl: () => new BuilderAgent(), + impl_review: () => new InspectorAgent(), + test: () => new RangerAgent(), +}; + +// Level determines which steps are skipped +// L1 (quick): plan → impl → done (skip reviews and tests) +// L2 (standard): plan → plan_review → impl → impl_review → done (skip tests) +// L3 (full): all steps +const LEVEL_FLOW: Record<number, string[]> = { + 1: ['todo', 'plan', 'impl', 'done'], + 2: ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'done'], + 3: ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done'], +}; + +const MAX_RETRIES = 3; + +// Track running pipelines so we can stop them +const runningPipelines = new Map<string, { stopped: boolean }>(); + +function pipelineKey(project: string, taskId: number): string { + return `${project}:${taskId}`; +} + +export function isPipelineRunning(project: string, taskId: number): boolean { + return runningPipelines.has(pipelineKey(project, taskId)); +} + +export function stopPipeline(project: string, taskId: number): void { + const key = pipelineKey(project, taskId); + const state = runningPipelines.get(key); + if (state) { + state.stopped = true; + } +} + +export async function runStep(project: string, taskId: number): Promise<void> { + const db = getProjectDb(project); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task | undefined; + if (!task) throw new Error('Task not found'); + + if (task.status === 'done') throw new Error('Task is already done'); + + const agentFactory = STATUS_AGENT_MAP[task.status]; + if (!agentFactory) throw new Error(`No agent for status: ${task.status}`); + + const agent = agentFactory(); + await executeAgent(project, task, agent, db); +} + +export async function runFullPipeline(project: string, taskId: number): Promise<void> { + const key = pipelineKey(project, taskId); + if (runningPipelines.has(key)) throw new Error('Pipeline already running'); + + const state = { stopped: false }; + runningPipelines.set(key, state); + + try { + const db = getProjectDb(project); + let retries = 0; + + while (!state.stopped) { + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task | undefined; + if (!task) break; + if (task.status === 'done') break; + + // Check if this status is in the level flow + const flow = LEVEL_FLOW[task.level] || LEVEL_FLOW[3]; + if (!flow.includes(task.status)) { + // Skip to next valid status in the flow + const currentIdx = flow.indexOf(task.status); + if (currentIdx === -1) break; + } + + const agentFactory = STATUS_AGENT_MAP[task.status]; + if (!agentFactory) break; + + const agent = agentFactory(); + + try { + const result = await executeAgent(project, task, agent, db); + + // Check for review rejections (circuit breaker) + if (result.verdict === 'reject') { + retries++; + if (retries >= MAX_RETRIES) { + broadcast(project, { + type: 'pipeline:error', + project, + taskId, + error: `Circuit breaker: ${MAX_RETRIES} rejections reached. Pipeline stopped.`, + }); + break; + } + } else { + retries = 0; + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + broadcast(project, { + type: 'pipeline:error', + project, + taskId, + error: errorMsg, + }); + break; + } + } + + broadcast(project, { type: 'pipeline:done', project, taskId }); + } finally { + runningPipelines.delete(key); + } +} + +async function executeAgent( + project: string, + task: Task, + agent: BaseAgent, + db: ReturnType<typeof getProjectDb> +): Promise<{ verdict?: 'approve' | 'reject' }> { + // Create pipeline job + const job = db.prepare( + `INSERT INTO pipeline_jobs (task_id, agent, status, model, started_at) + VALUES (?, ?, 'running', ?, datetime('now'))` + ).run(task.id, agent.name, agent.model); + const jobId = job.lastInsertRowid; + + // Update current agent on task + db.prepare('UPDATE tasks SET current_agent = ? WHERE id = ?').run(agent.name, task.id); + broadcast(project, { type: 'pipeline:agent_start', project, taskId: task.id, agent: agent.name }); + + try { + // Build context + const ctx: AgentContext = { + taskId: task.id, + title: task.title, + description: task.description || '', + plan: task.plan || undefined, + decisionLog: task.decision_log || undefined, + doneWhen: task.done_when || undefined, + implementationNotes: task.implementation_notes || undefined, + diff: task.diff || undefined, + testResults: task.test_results || undefined, + reviewComments: task.review_comments || undefined, + branchName: task.branch_name || undefined, + }; + + // Execute agent + const result = await agent.execute(ctx); + + // Handle Builder agent's file changes + if (agent.name === 'builder' && result.content) { + const files = parseFileBlocks(result.content); + if (files.length > 0) { + try { + const branchName = task.branch_name || `kanban/task-${task.id}`; + const adminDb = (await import('../db.js')).getAdminDb(); + const projectRow = adminDb.prepare('SELECT * FROM projects WHERE name = ?').get(project) as { repo_path: string } | undefined; + + if (projectRow) { + const workspacePath = `/app/workspaces/${project}-task-${task.id}`; + await cloneRepo(projectRow.repo_path, workspacePath); + await createBranch(workspacePath, branchName); + await writeFiles(workspacePath, files); + const diff = await commitAndDiff(workspacePath, `feat: ${task.title} [kanban #${task.id}]`, branchName); + result.updates.branch_name = branchName; + result.updates.diff = diff; + } + } catch (gitErr) { + console.error('Git operation failed:', gitErr); + // Don't fail the whole pipeline over git errors + result.updates.implementation_notes = + (result.updates.implementation_notes || '') + + `\n\nGit error: ${gitErr instanceof Error ? gitErr.message : String(gitErr)}`; + } + } + } + + // Apply updates to task + const updates = result.updates; + const setClauses: string[] = []; + const params: unknown[] = []; + + for (const [key, value] of Object.entries(updates)) { + setClauses.push(`${key} = ?`); + params.push(value); + } + + // Clear current_agent + setClauses.push('current_agent = NULL'); + + if (setClauses.length > 0) { + params.push(task.id); + db.prepare(`UPDATE tasks SET ${setClauses.join(', ')} WHERE id = ?`).run(...params); + } + + // Append to agent log + const logEntry = { + agent: agent.name, + message: result.message, + verdict: result.verdict, + tokensUsed: result.tokensUsed, + model: result.model, + timestamp: new Date().toISOString(), + }; + + let agentLog: unknown[]; + try { + agentLog = JSON.parse(task.agent_log || '[]'); + } catch { + agentLog = []; + } + agentLog.push(logEntry); + db.prepare('UPDATE tasks SET agent_log = ? WHERE id = ?').run(JSON.stringify(agentLog), task.id); + + // Update pipeline job + db.prepare( + `UPDATE pipeline_jobs SET status = 'completed', completed_at = datetime('now'), tokens_used = ? WHERE id = ?` + ).run(result.tokensUsed, jobId); + + // Broadcast completion + const updatedTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(task.id); + broadcast(project, { + type: 'pipeline:agent_complete', + project, + taskId: task.id, + agent: agent.name, + result: result.message, + }); + broadcast(project, { type: 'task:updated', project, task: updatedTask }); + + return { verdict: result.verdict }; + + } catch (err) { + // Mark job as failed + const errorMsg = err instanceof Error ? err.message : String(err); + db.prepare( + `UPDATE pipeline_jobs SET status = 'failed', completed_at = datetime('now'), error = ? WHERE id = ?` + ).run(errorMsg, jobId); + + // Clear current agent + db.prepare('UPDATE tasks SET current_agent = NULL WHERE id = ?').run(task.id); + + throw err; + } +} + +export function getPipelineStatus(project: string, taskId: number): { + running: boolean; + currentAgent: string | null; + jobs: unknown[]; +} { + const db = getProjectDb(project); + const task = db.prepare('SELECT current_agent FROM tasks WHERE id = ?').get(taskId) as { current_agent: string | null } | undefined; + const jobs = db.prepare('SELECT * FROM pipeline_jobs WHERE task_id = ? ORDER BY id DESC').all(taskId); + + return { + running: isPipelineRunning(project, taskId), + currentAgent: task?.current_agent || null, + jobs, + }; +} diff --git a/admin-panel/src/server/templates/builder.md b/admin-panel/src/server/templates/builder.md new file mode 100644 index 0000000..eee2cf6 --- /dev/null +++ b/admin-panel/src/server/templates/builder.md @@ -0,0 +1,47 @@ +You are a senior software engineer implementing a feature based on an approved plan. + +## Task #{{taskId}}: {{title}} + +### Requirements +{{description}} + +### Approved Plan +{{plan}} + +### Done When +{{done_when}} + +### Current Branch +{{branch_name}} + +### Repository File Listing +{{file_list}} + +### Relevant File Contents +{{file_contents}} + +## Instructions + +Implement the changes described in the plan. For each file that needs to be created or modified, output the COMPLETE file contents. + +## Output Format + +For each file, use this exact format: + +```FILE: path/to/file.ts +(complete file contents here) +``` + +After all files, provide a brief summary: + +### Summary +(what you implemented and any notes) + +### Implementation Notes +(any important details about the implementation) + +Important: +- Output the COMPLETE contents of each file (not just the changes) +- Include ALL imports, types, and code +- Follow existing code style and conventions in the repo +- Do not skip any file that needs changes diff --git a/admin-panel/src/server/templates/critic.md b/admin-panel/src/server/templates/critic.md new file mode 100644 index 0000000..dd68267 --- /dev/null +++ b/admin-panel/src/server/templates/critic.md @@ -0,0 +1,47 @@ +You are a critical plan reviewer. Your job is to evaluate an implementation plan for quality, correctness, and completeness. + +## Task #{{taskId}}: {{title}} + +### Requirements +{{description}} + +### Proposed Plan +{{plan}} + +### Decision Log +{{decision_log}} + +### Done When +{{done_when}} + +## Instructions + +Review this plan carefully. Consider: + +1. **Completeness**: Does the plan cover all requirements? Are any edge cases missed? +2. **Correctness**: Is the technical approach sound? Any potential bugs or issues? +3. **Simplicity**: Is the approach unnecessarily complex? Can it be simplified? +4. **Security**: Any security concerns (injection, XSS, auth bypass)? +5. **Testability**: Can the implementation be verified easily? + +## Output Format + +Start with your verdict on the FIRST line, exactly one of: +``` +VERDICT: APPROVE +``` +or +``` +VERDICT: REJECT +``` + +Then provide your review: + +### Strengths +- (what's good about this plan) + +### Issues +- (any problems found — required for REJECT) + +### Suggestions +- (improvements, even if approving) diff --git a/admin-panel/src/server/templates/inspector.md b/admin-panel/src/server/templates/inspector.md new file mode 100644 index 0000000..b392440 --- /dev/null +++ b/admin-panel/src/server/templates/inspector.md @@ -0,0 +1,61 @@ +You are a senior code reviewer. Your job is to review implemented code for quality, correctness, and adherence to the plan. + +## Task #{{taskId}}: {{title}} + +### Requirements +{{description}} + +### Approved Plan +{{plan}} + +### Done When +{{done_when}} + +### Implementation Diff +{{diff}} + +### Implementation Notes +{{implementation_notes}} + +### Test Results +{{test_results}} + +## Instructions + +Review the implementation diff carefully. Consider: + +1. **Correctness**: Does the code do what the plan describes? Any bugs? +2. **Plan adherence**: Does the implementation match the plan? +3. **Code quality**: Is the code clean, readable, and maintainable? +4. **Security**: Any vulnerabilities (injection, XSS, auth issues)? +5. **Performance**: Any obvious performance problems? +6. **Error handling**: Are errors handled appropriately? +7. **Test coverage**: Do the tests adequately verify the implementation? + +## Output Format + +Start with your verdict on the FIRST line, exactly one of: +``` +VERDICT: APPROVE +``` +or +``` +VERDICT: REJECT +``` + +Then provide: + +### Score +(1-10, where 10 is perfect) + +### Strengths +- (what's done well) + +### Issues +- (problems found — required for REJECT, include file:line references) + +### Suggestions +- (improvements, even if approving) + +### Comments +(line-specific comments for Gerrit, format: `file:line: comment`) diff --git a/admin-panel/src/server/templates/planner.md b/admin-panel/src/server/templates/planner.md new file mode 100644 index 0000000..5d1a2bb --- /dev/null +++ b/admin-panel/src/server/templates/planner.md @@ -0,0 +1,48 @@ +You are a senior software architect planning the implementation of a task. + +## Task #{{taskId}}: {{title}} + +### Requirements +{{description}} + +### Repository Files +{{file_list}} + +## Instructions + +Create a detailed implementation plan for this task. Your plan should include: + +1. **Analysis**: Briefly analyze the requirements and identify key challenges +2. **Approach**: Describe the technical approach you'll take +3. **Steps**: Numbered list of concrete implementation steps +4. **Files to modify**: List each file that needs to be created or modified, with a brief description of changes +5. **Done When**: Clear acceptance criteria — when is this task complete? +6. **Decision Log**: Any architectural decisions made and their rationale + +## Output Format + +Respond with the following sections using markdown headers: + +### Analysis +(your analysis) + +### Approach +(your approach) + +### Steps +1. (step 1) +2. (step 2) +... + +### Files +- `path/to/file.ts` — (what changes) +... + +### Done When +- (criterion 1) +- (criterion 2) +... + +### Decisions +- (decision 1): (rationale) +... diff --git a/admin-panel/src/server/templates/ranger.md b/admin-panel/src/server/templates/ranger.md new file mode 100644 index 0000000..74cc538 --- /dev/null +++ b/admin-panel/src/server/templates/ranger.md @@ -0,0 +1,50 @@ +You are a test runner and quality assurance agent. Your job is to evaluate test results and determine if the implementation passes. + +## Task #{{taskId}}: {{title}} + +### Done When +{{done_when}} + +### Test Results +{{test_results}} + +### Implementation Diff +{{diff}} + +### Review Comments +{{review_comments}} + +## Instructions + +Analyze the test results and review feedback. Determine if the task meets the "Done When" criteria. + +Consider: +1. **Test pass/fail**: Did all tests pass? +2. **Coverage**: Are the acceptance criteria covered by tests? +3. **Review status**: Was the code review approved? +4. **Quality**: Any remaining concerns? + +## Output Format + +Start with your verdict on the FIRST line, exactly one of: +``` +VERDICT: PASS +``` +or +``` +VERDICT: FAIL +``` + +Then provide: + +### Summary +(overall assessment) + +### Test Analysis +- (breakdown of test results) + +### Remaining Issues +- (any issues that prevent passing — required for FAIL) + +### Recommendation +(what should happen next) diff --git a/admin-panel/src/server/templates/shield.md b/admin-panel/src/server/templates/shield.md new file mode 100644 index 0000000..27563ca --- /dev/null +++ b/admin-panel/src/server/templates/shield.md @@ -0,0 +1,40 @@ +You are a TDD test engineer. Your job is to write comprehensive tests for an implementation. + +## Task #{{taskId}}: {{title}} + +### Requirements +{{description}} + +### Done When +{{done_when}} + +### Implementation Diff +{{diff}} + +### Implementation Notes +{{implementation_notes}} + +## Instructions + +Write tests that verify the implementation meets the requirements and "Done When" criteria. Consider: + +1. **Happy path**: Does the core functionality work? +2. **Edge cases**: Boundary conditions, empty inputs, null values +3. **Error cases**: Invalid inputs, missing data, failure modes +4. **Integration**: Do components work together correctly? + +## Output Format + +For each test file, use this exact format: + +```FILE: path/to/test-file.test.ts +(complete test file contents) +``` + +After the test files, provide: + +### Test Strategy +- (what's being tested and why) + +### Coverage Notes +- (what's covered, what's intentionally not covered) diff --git a/admin-panel/src/server/ws.ts b/admin-panel/src/server/ws.ts new file mode 100644 index 0000000..c0ffd8e --- /dev/null +++ b/admin-panel/src/server/ws.ts @@ -0,0 +1,56 @@ +import { WebSocketServer, WebSocket } from 'ws'; +import type { Server } from 'http'; + +let wss: WebSocketServer; + +interface Subscription { + ws: WebSocket; + projects: Set<string>; +} + +const subscriptions: Subscription[] = []; + +export function setupWebSocket(server: Server): void { + wss = new WebSocketServer({ server, path: '/ws' }); + + wss.on('connection', (ws) => { + const sub: Subscription = { ws, projects: new Set() }; + subscriptions.push(sub); + + ws.on('message', (data) => { + try { + const msg = JSON.parse(data.toString()); + if (msg.type === 'subscribe' && msg.project) { + sub.projects.add(msg.project); + } else if (msg.type === 'unsubscribe' && msg.project) { + sub.projects.delete(msg.project); + } + } catch { + // ignore malformed messages + } + }); + + ws.on('close', () => { + const idx = subscriptions.indexOf(sub); + if (idx !== -1) subscriptions.splice(idx, 1); + }); + }); +} + +export function broadcast(project: string, event: Record<string, unknown>): void { + const payload = JSON.stringify(event); + for (const sub of subscriptions) { + if (sub.projects.has(project) && sub.ws.readyState === WebSocket.OPEN) { + sub.ws.send(payload); + } + } +} + +export function broadcastAll(event: Record<string, unknown>): void { + const payload = JSON.stringify(event); + for (const sub of subscriptions) { + if (sub.ws.readyState === WebSocket.OPEN) { + sub.ws.send(payload); + } + } +} diff --git a/admin-panel/tsconfig.json b/admin-panel/tsconfig.json new file mode 100644 index 0000000..b73e854 --- /dev/null +++ b/admin-panel/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/admin-panel/tsconfig.server.json b/admin-panel/tsconfig.server.json new file mode 100644 index 0000000..d616782 --- /dev/null +++ b/admin-panel/tsconfig.server.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "dist", + "rootDir": "src", + "declaration": false, + "sourceMap": true + }, + "include": ["src/server/**/*"], + "exclude": ["node_modules", "dist", "src/client"] +} diff --git a/admin-panel/vite.config.ts b/admin-panel/vite.config.ts new file mode 100644 index 0000000..0cbe3b2 --- /dev/null +++ b/admin-panel/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + root: 'src/client', + build: { + outDir: '../../dist/client', + emptyOutDir: true, + }, + server: { + proxy: { + '/api': 'http://localhost:3000', + '/ws': { + target: 'ws://localhost:3000', + ws: true, + }, + }, + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src/client'), + }, + }, +}); diff --git a/nginx/docker-compose.yaml b/nginx/docker-compose.yaml index 86dfc74..922df75 100644 --- a/nginx/docker-compose.yaml +++ b/nginx/docker-compose.yaml @@ -24,6 +24,7 @@ services: monitoring_network: nexus_network: authelia_network: + admin_network: networks: #write each stack network (or connect it manually later) @@ -45,4 +46,7 @@ networks: authelia_network: name: authelia_network external: true + admin_network: + name: admin_network + external: true diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 2c9f4cf..52bb590 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -45,7 +45,7 @@ server { listen 80; listen [::]:80; - server_name swave.lol blog.swave.lol ghost.swave.lol jenkins.swave.lol cgit.swave.lol gerrit.swave.lol nexus.swave.lol registry.swave.lol auth.swave.lol; + server_name swave.lol blog.swave.lol ghost.swave.lol jenkins.swave.lol cgit.swave.lol gerrit.swave.lol nexus.swave.lol registry.swave.lol auth.swave.lol admin.swave.lol; location / { rewrite ^ https://$host$request_uri? permanent; @@ -524,3 +524,82 @@ server { proxy_set_header X-Forwarded-Host $http_host; } } + +# Admin Panel — admin.swave.lol +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name admin.swave.lol; + + server_tokens off; + + ssl_certificate /var/letsencrypt/etc/live/swave.lol/fullchain.pem; + ssl_certificate_key /var/letsencrypt/etc/live/swave.lol/privkey.pem; + + ssl_buffer_size 8k; + + ssl_dhparam /etc/ssl/certs/dhparam-2048.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + + ssl_ecdh_curve secp384r1; + ssl_session_tickets off; + + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + resolver 8.8.8.8; + + charset utf-8; + + # WebSocket support + location /ws { + auth_request /_authelia-auth; + auth_request_set $authelia_user $upstream_http_remote_user; + proxy_pass http://admin-panel:3000/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + } + + location / { + auth_request /_authelia-auth; + auth_request_set $authelia_user $upstream_http_remote_user; + proxy_pass http://admin-panel:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Authelia forward-auth subrequest endpoint + location = /_authelia-auth { + internal; + resolver 127.0.0.11 valid=30s; + set $authelia_upstream http://authelia:9091/api/verify; + proxy_pass $authelia_upstream; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-Method $request_method; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Uri $request_uri; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + error_page 401 = @authelia_login_redirect; + location @authelia_login_redirect { + return 302 https://auth.swave.lol/?rd=$scheme://$http_host$request_uri; + } +} |
