commit 5d40023de0feca91be0738435d82e9095226a9c8 Author: Arunavo Ray Date: Sun May 18 09:31:23 2025 +0530 🎉 Gitea Mirror: Added diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..650dea2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,65 @@ +# Version control +.git +.gitignore +.github + +# Node.js +node_modules +npm-debug.log +yarn-debug.log +yarn-error.log +pnpm-debug.log + +# Build outputs +dist +build +.next +out + +# Environment variables +.env +.env.local +.env.development +.env.test +.env.production + +# IDE and editor files +.idea +.vscode +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Test coverage +coverage +.nyc_output + +# Docker +Dockerfile +.dockerignore +docker-compose.yml +docker-compose.*.yml + +# Documentation +README.md +LICENSE +docs + +# Temporary files +tmp +temp +*.tmp +*.temp + +# Logs +logs +*.log + +# Cache +.cache +.npm +.pnpm-store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4d6d768 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Docker Registry Configuration +DOCKER_REGISTRY=ghcr.io +DOCKER_IMAGE=arunavo4/gitea-mirror +DOCKER_TAG=latest + +# Application Configuration +NODE_ENV=production +HOST=0.0.0.0 +PORT=4321 +DATABASE_URL=sqlite://data/gitea-mirror.db + +# Security +JWT_SECRET=change-this-to-a-secure-random-string-in-production + +# Optional GitHub/Gitea Mirror Configuration (for docker-compose, can also be set via web UI) +# Uncomment and set as needed. These are passed as environment variables to the container. +# GITHUB_USERNAME=your-github-username +# GITHUB_TOKEN=your-github-personal-access-token +# SKIP_FORKS=false +# PRIVATE_REPOSITORIES=false +# MIRROR_ISSUES=false +# MIRROR_STARRED=false +# MIRROR_ORGANIZATIONS=false +# PRESERVE_ORG_STRUCTURE=false +# ONLY_MIRROR_ORGS=false +# SKIP_STARRED_ISSUES=false +# GITEA_URL=http://gitea:3000 +# GITEA_TOKEN=your-local-gitea-token +# GITEA_USERNAME=your-local-gitea-username +# GITEA_ORGANIZATION=github-mirrors +# GITEA_ORG_VISIBILITY=public +# DELAY=3600 diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..d78b898 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,88 @@ +# GitHub Workflows for Gitea Mirror + +This directory contains GitHub Actions workflows that automate the build, test, and deployment processes for the Gitea Mirror application. + +## Workflow Overview + +| Workflow | File | Purpose | +|----------|------|---------| +| Astro Build and Test | `astro-build-test.yml` | Builds and tests the Astro application for all branches and PRs | +| Docker Build and Push | `docker-build.yml` | Builds and pushes Docker images only for the main branch | +| Docker Security Scan | `docker-scan.yml` | Scans Docker images for security vulnerabilities | + +## Workflow Details + +### Astro Build and Test (`astro-build-test.yml`) + +This workflow runs on all branches and pull requests. It: + +- Builds the Astro project +- Runs all tests +- Uploads build artifacts for potential use in other workflows + +**When it runs:** +- On push to any branch (except changes to README.md and docs) +- On pull requests to any branch (except changes to README.md and docs) + +**Key features:** +- Uses pnpm for faster dependency installation +- Caches dependencies to speed up builds +- Uploads build artifacts for 7 days + +### Docker Build and Push (`docker-build.yml`) + +This workflow builds and pushes Docker images to GitHub Container Registry (ghcr.io), but only when changes are merged to the main branch. + +**When it runs:** +- On push to the main branch +- On tag creation (v*) + +**Key features:** +- Builds multi-architecture images (amd64 and arm64) +- Pushes images only on main branch, not for PRs +- Uses build caching to speed up builds +- Creates multiple tags for each image (latest, semver, sha) + +### Docker Security Scan (`docker-scan.yml`) + +This workflow scans Docker images for security vulnerabilities using Trivy. + +**When it runs:** +- On push to the main branch that affects Docker-related files +- Weekly on Sunday at midnight (scheduled) + +**Key features:** +- Scans for critical and high severity vulnerabilities +- Fails the build if vulnerabilities are found +- Ignores unfixed vulnerabilities + +## CI/CD Pipeline Philosophy + +Our CI/CD pipeline follows these principles: + +1. **Fast feedback for developers**: The Astro build and test workflow runs on all branches and PRs to provide quick feedback. +2. **Efficient resource usage**: Docker images are only built when changes are merged to main, not for every PR. +3. **Security first**: Regular security scanning ensures our Docker images are free from known vulnerabilities. +4. **Multi-architecture support**: All Docker images are built for both amd64 and arm64 architectures. + +## Adding or Modifying Workflows + +When adding or modifying workflows: + +1. Ensure the workflow follows the existing patterns +2. Test the workflow on a branch before merging to main +3. Update this README if you add a new workflow or significantly change an existing one +4. Consider the impact on CI resources and build times + +## Troubleshooting + +If a workflow fails: + +1. Check the workflow logs in the GitHub Actions tab +2. Common issues include: + - Test failures + - Build errors + - Docker build issues + - Security vulnerabilities + +For persistent issues, consider opening an issue in the repository. diff --git a/.github/workflows/astro-build-test.yml b/.github/workflows/astro-build-test.yml new file mode 100644 index 0000000..448190f --- /dev/null +++ b/.github/workflows/astro-build-test.yml @@ -0,0 +1,50 @@ +name: Astro Build and Test + +on: + push: + branches: [ '*' ] + paths-ignore: + - 'README.md' + - 'docs/**' + pull_request: + branches: [ '*' ] + paths-ignore: + - 'README.md' + - 'docs/**' + +jobs: + build-and-test: + name: Build and Test Astro Project + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 10 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Run tests + run: pnpm test + + - name: Build Astro project + run: pnpm build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: astro-build + path: dist/ + retention-days: 7 diff --git a/.github/workflows/docker-build-stable.yml b/.github/workflows/docker-build-stable.yml new file mode 100644 index 0000000..8637c4d --- /dev/null +++ b/.github/workflows/docker-build-stable.yml @@ -0,0 +1,45 @@ +name: Build and Push Docker Images (Stable) + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + +env: + REGISTRY: ghcr.io + IMAGE: ${{ github.repository }} + +permissions: + contents: write + packages: write + +jobs: + docker: + runs-on: ubuntu-latest + + services: + redis: + image: redis:7-alpine + ports: ['6379:6379'] + + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} # Replace with secrets.GHCR_PAT if using PAT + + - uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} diff --git a/.github/workflows/docker-scan.yml b/.github/workflows/docker-scan.yml new file mode 100644 index 0000000..005e612 --- /dev/null +++ b/.github/workflows/docker-scan.yml @@ -0,0 +1,53 @@ +name: Docker Security Scan + +on: + push: + branches: [ main ] + paths: + - 'Dockerfile' + - '.dockerignore' + - 'package.json' + - 'pnpm-lock.yaml' + pull_request: + branches: [ main ] + paths: + - 'Dockerfile' + - '.dockerignore' + - 'package.json' + - 'pnpm-lock.yaml' + schedule: + - cron: '0 0 * * 0' # Run weekly on Sunday at midnight + +jobs: + scan: + name: Scan Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + load: true + tags: gitea-mirror:scan + # Disable GitHub Actions cache for this workflow + no-cache: true + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: gitea-mirror:scan + format: 'table' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c562295 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# database files +data/gitea-mirror.db + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3e7c47 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,88 @@ +# syntax=docker/dockerfile:1.4 + +FROM node:lts-alpine AS base +ENV PNPM_HOME=/usr/local/bin +ENV PATH=$PNPM_HOME:$PATH +RUN apk add --no-cache libc6-compat + +# ----------------------------------- +FROM base AS deps +WORKDIR /app +RUN apk add --no-cache python3 make g++ gcc + +RUN --mount=type=cache,target=/root/.npm \ + corepack enable && corepack prepare pnpm@latest --activate + +COPY package.json pnpm-lock.yaml* ./ + +# Full dev install +RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +# ----------------------------------- +FROM base AS builder +WORKDIR /app +RUN apk add --no-cache python3 make g++ gcc + +RUN --mount=type=cache,target=/root/.npm \ + corepack enable && corepack prepare pnpm@latest --activate + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +RUN pnpm build + +# ----------------------------------- +FROM deps AS pruner +WORKDIR /app + +# Prune dev dependencies and just keep the production bits +RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ + pnpm prune --prod + +# ----------------------------------- +FROM base AS runner +WORKDIR /app + +# Only copy production node_modules and built output +COPY --from=pruner /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/docker-entrypoint.sh ./docker-entrypoint.sh +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/data ./data + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=4321 +ENV DATABASE_URL=sqlite://data/gitea-mirror.db + +# Make entrypoint executable +RUN chmod +x /app/docker-entrypoint.sh + +ENTRYPOINT ["/app/docker-entrypoint.sh"] + +RUN apk add --no-cache wget && \ + mkdir -p /app/data && \ + addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 gitea-mirror && \ + chown -R gitea-mirror:nodejs /app/data + +COPY --from=builder --chown=gitea-mirror:nodejs /app/dist ./dist +COPY --from=pruner --chown=gitea-mirror:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=gitea-mirror:nodejs /app/package.json ./package.json +COPY --from=builder --chown=gitea-mirror:nodejs /app/scripts ./scripts + +USER gitea-mirror + +VOLUME /app/data +EXPOSE 4321 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:4321/ || exit 1 + +# Create a startup script that initializes the database before starting the application +COPY --from=builder --chown=gitea-mirror:nodejs /app/docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x ./docker-entrypoint.sh + +CMD ["./docker-entrypoint.sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..86d4d6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ARUNAVO RAY + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b46c24 --- /dev/null +++ b/README.md @@ -0,0 +1,378 @@ +# Gitea Mirror + +**Gitea Mirror** is a modern web application for automatically mirroring repositories from GitHub to your self-hosted Gitea instance. Designed for developers, teams, and organizations who want to retain full control of their code while still collaborating on GitHub. + +## Features + +- 🔁 Sync public, private, or starred GitHub repos to Gitea +- đŸĸ Mirror entire organizations with structure preservation +- 🐞 Optional mirroring of issues and labels +- 🌟 Mirror your starred repositories +- đŸ•šī¸ Modern user interface with toast notifications and smooth experience +- 🧠 Smart filtering and job queue with detailed logs +- đŸ› ī¸ Works with personal access tokens (GitHub + Gitea) +- 🔒 First-time user signup experience with secure authentication +- đŸŗ Fully Dockerized + can be self-hosted in minutes +- 📊 Dashboard with real-time status updates +- âąī¸ Scheduled automatic mirroring + +## Screenshots + +
+ +### Dashboard +The dashboard provides an overview of your mirroring status, including total repositories, successfully mirrored repositories, and recent activity. + +### Repository Management +Manage all your repositories in one place. Filter by status, search by name, and trigger manual mirroring operations. + +### Configuration +Easily configure your GitHub and Gitea connections, set up automatic mirroring schedules, and manage organization mirroring. + +## Getting Started + +See the [Quick Start Guide](docs/quickstart.md) for detailed instructions on getting up and running quickly. + +### Prerequisites + +- Node.js 18 or later +- A GitHub account with a personal access token +- A Gitea instance with an access token + + +#### Database + +The database (`data/gitea-mirror.db`) is created when the application first runs. It starts empty and is populated as you configure and use the application. + +**Note**: On first launch, you'll be guided through creating an admin account with your chosen credentials. + +#### Production Database + +The production database (`data/gitea-mirror.db`) is created when the application runs in production mode. It starts empty and is populated as you configure and use the application. + +**Important**: The production database file is excluded from the Git repository as it may contain sensitive information like GitHub and Gitea tokens. Never commit this file to the repository. + +##### Database Initialization + +Before running the application in production mode for the first time, you need to initialize the database: + +```bash +# Initialize the database for production mode +pnpm setup +``` + +This will create the necessary tables. On first launch, you'll be guided through creating your admin account with a secure password. + +### Installation + +#### Using Docker (Recommended) + +Gitea Mirror provides multi-architecture Docker images that work on both ARM64 (e.g., Apple Silicon, Raspberry Pi) and x86_64 (Intel/AMD) platforms. + +##### Using Pre-built Images from GitHub Container Registry + +```bash +# Pull the latest multi-architecture image +docker pull ghcr.io/arunavo4/gitea-mirror:latest + +# Run the application +docker run -d \\ + -p 4321:4321 \\ + ghcr.io/arunavo4/gitea-mirror:latest +``` + +##### Using Docker Compose (Recommended) + +```bash +# Start the application using Docker Compose +docker-compose --profile production up -d + +# For development mode (requires configuration) +# Ensure you have run pnpm setup first +docker-compose -f docker-compose.dev.yml up -d +``` + +##### Building Docker Images Manually + +The project includes a build script to create and manage multi-architecture Docker images: + +```bash +# Copy example environment file if you don't have one +cp .env.example .env + +# Edit .env file with your preferred settings +# DOCKER_REGISTRY, DOCKER_IMAGE, DOCKER_TAG, etc. + +# Build and load into local Docker +./scripts/build-docker.sh --load + +# OR: Build and push to a registry (requires authentication) +./scripts/build-docker.sh --push + +# Then run with Docker Compose +docker-compose --profile production up -d +``` + +See [Docker build documentation](./scripts/README-docker.md) for more details. + +##### Building Your Own Image + +For manual Docker builds (without the helper script): + +```bash +# Build the Docker image for your current architecture +docker build -t gitea-mirror:latest . + +# Build multi-architecture images (requires Docker Buildx) +docker buildx create --name multiarch --driver docker-container --use +docker buildx build --platform linux/amd64,linux/arm64 -t gitea-mirror:latest --load . + +# If you encounter issues with Buildx, you can try these workarounds: +# 1. Retry with network settings +docker buildx build --platform linux/amd64,linux/arm64 -t gitea-mirror:latest --network=host --load . + +# 2. Build one platform at a time if you're having resource issues +docker buildx build --platform linux/amd64 -t gitea-mirror:amd64 --load . +docker buildx build --platform linux/arm64 -t gitea-mirror:arm64 --load . + +# Create a named volume for database persistence +docker volume create gitea-mirror-data +``` + +##### Environment Variables + +The Docker container can be configured with the following environment variables: + +- `DATABASE_URL`: SQLite database URL (default: `sqlite://data/gitea-mirror.db`) +- `HOST`: Host to bind to (default: `0.0.0.0`) +- `PORT`: Port to listen on (default: `3000`) +- `JWT_SECRET`: Secret key for JWT token generation (important for security) + + +#### Manual Installation + +```bash +# Clone the repository +git clone https://github.com/arunavo4/gitea-mirror.git +cd gitea-mirror + +# Quick setup (installs dependencies and initializes the database) +pnpm setup + +# Development Mode Options + +# Run in development mode +pnpm dev + +# Run in development mode with clean database (removes existing DB first) +pnpm dev:clean + +# Production Mode Options + +# Build the application +pnpm build + +# Preview the production build +pnpm preview + +# Start the production server (default) +pnpm start + +# Start the production server with a clean setup +pnpm start:fresh + +# Database Management + +# Initialize the database +pnpm init-db + +# Reset users for testing first-time signup +pnpm reset-users + +# Check database status +pnpm check-db +``` + +### Configuration + +Gitea Mirror can be configured through environment variables or through the web UI. See the [Configuration Guide](docs/configuration.md) for more details. + +Key configuration options include: + +- GitHub connection settings (username, token, repository filters) +- Gitea connection settings (URL, token, organization) +- Mirroring options (issues, starred repositories, organizations) +- Scheduling options for automatic mirroring + +## Architecture + +Gitea Mirror follows a modular architecture with clear separation of concerns. See the [Architecture Document](docs/architecture.md) for a comprehensive overview of the system design, including: + +- Component diagrams +- Data flow +- Project structure +- Key components +- Database schema +- API endpoints + +### High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Gitea Mirror │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ +│ │ │ │ │ │ │ │ +│ │ Frontend │◄───â–ē│ Backend │◄───â–ē│ Database │ │ +│ │ (Astro) │ │ (Node.js) │ │ (SQLite) │ │ +│ │ │ │ │ │ │ │ +│ └─────────────┘ └──────â”Ŧ──────┘ └─────────────────┘ │ +│ │ │ +└─────────────────────────────â”ŧ───────────────────────────────────┘ + │ + â–ŧ + ┌─────────────────────────────────────────┐ + │ │ + │ External APIs │ + │ │ + │ ┌─────────────┐ ┌─────────────┐ │ + │ │ │ │ │ │ + │ │ GitHub API │ │ Gitea API │ │ + │ │ │ │ │ │ + │ └─────────────┘ └─────────────┘ │ + │ │ + └─────────────────────────────────────────┘ +``` + +## Development + +### Local Development Setup + +```bash +# Install dependencies +pnpm setup + +# Start the development server +pnpm dev +``` + +### Setting Up a Local Gitea Instance for Testing + +For full end-to-end testing, you can set up a local Gitea instance using Docker: + +```bash +# Create a Docker network for Gitea and Gitea Mirror to communicate +docker network create gitea-network + +# Create volumes for Gitea data persistence +docker volume create gitea-data +docker volume create gitea-config + +# Run Gitea container +docker run -d \ + --name gitea \ + --network gitea-network \ + -p 3001:3000 \ + -p 2222:22 \ + -v gitea-data:/data \ + -v gitea-config:/etc/gitea \ + -e USER_UID=1000 \ + -e USER_GID=1000 \ + -e GITEA__database__DB_TYPE=sqlite3 \ + -e GITEA__database__PATH=/data/gitea.db \ + -e GITEA__server__DOMAIN=localhost \ + -e GITEA__server__ROOT_URL=http://localhost:3001/ \ + -e GITEA__server__SSH_DOMAIN=localhost \ + -e GITEA__server__SSH_PORT=2222 \ + -e GITEA__server__START_SSH_SERVER=true \ + -e GITEA__security__INSTALL_LOCK=true \ + -e GITEA__service__DISABLE_REGISTRATION=false \ + gitea/gitea:latest +``` + +After Gitea is running: + +1. Access Gitea at http://localhost:3001/ +2. Register a new user +3. Create a personal access token in Gitea (Settings > Applications > Generate New Token) +4. Run Gitea Mirror with the local Gitea configuration: + +```bash +# Run Gitea Mirror connected to the local Gitea instance +docker run -d \ + --name gitea-mirror-dev \ + --network gitea-network \ + -p 3000:3000 \ + -v gitea-mirror-data:/app/data \ + -e NODE_ENV=development \ + -e JWT_SECRET=dev-secret-key \ + -e GITHUB_TOKEN=your-github-token \ + -e GITHUB_USERNAME=your-github-username \ + -e GITEA_URL=http://gitea:3000 \ + -e GITEA_TOKEN=your-local-gitea-token \ + -e GITEA_USERNAME=your-local-gitea-username \ + arunavo4/gitea-mirror:latest +``` + +This setup allows you to test the full mirroring functionality with a local Gitea instance. + +### Using Docker Compose for Development + +For convenience, a dedicated development docker-compose file is provided that sets up both Gitea Mirror and a local Gitea instance: + +```bash +# Start with development environment and local Gitea instance +docker-compose -f docker-compose.dev.yml up -d +``` + +You can also create a `.env` file with your GitHub and Gitea credentials: + +``` +# GitHub credentials +GITHUB_TOKEN=your-github-token +GITHUB_USERNAME=your-github-username + +# Gitea credentials (will be set up after you create a user in the local Gitea instance) +GITEA_TOKEN=your-local-gitea-token +GITEA_USERNAME=your-local-gitea-username +``` + +## Technologies Used + +- **Frontend**: Astro, React, Shadcn UI, Tailwind CSS v4 +- **Backend**: Node.js +- **Database**: SQLite (default) or PostgreSQL +- **API Integration**: GitHub API (Octokit), Gitea API + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Project Status + +This project is now complete with the following features implemented: + +- ✅ User-friendly dashboard with status overview +- ✅ Repository management interface +- ✅ Organization management interface +- ✅ Configuration management for GitHub and Gitea +- ✅ Scheduling and automation +- ✅ Activity logging and monitoring +- ✅ Responsive design for all screen sizes +- ✅ Modern toast notifications for better user feedback +- ✅ First-time user signup experience +- ✅ Better error handling and user guidance +- ✅ Comprehensive error handling +- ✅ Unit tests for components and API +- ✅ Direct GitHub to Gitea mirroring (no external dependencies) +- ✅ Docker and docker-compose support for easy deployment + +## Acknowledgements + +- [Octokit](https://github.com/octokit/rest.js/) - GitHub REST API client for JavaScript +- [Shadcn UI](https://ui.shadcn.com/) - For the beautiful UI components +- [Astro](https://astro.build/) - For the excellent web framework diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..7e07ada --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,17 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import tailwindcss from '@tailwindcss/vite'; +import react from '@astrojs/react'; +import node from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + output: 'server', + adapter: node({ + mode: 'standalone', + }), + vite: { + plugins: [tailwindcss()] + }, + integrations: [react()] +}); \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..750bc1b --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..2a46038 --- /dev/null +++ b/data/README.md @@ -0,0 +1,32 @@ +# Data Directory + +This directory contains the SQLite database file for the Gitea Mirror application. + +## Files + +- `gitea-mirror.db`: The main database file. This file is **not** committed to the repository as it may contain sensitive information like tokens. + +## Important Notes + +- **Never commit `gitea-mirror.db` to the repository** as it may contain sensitive information like GitHub and Gitea tokens. +- The application will create this database file automatically on first run. + +## Database Initialization + +To initialize the database for real data mode, run: + +```bash +pnpm init-db +``` + +This will create the necessary tables. On first launch, you'll be guided through creating an admin account with your chosen credentials. + +## User Management + +To reset users (for testing the first-time setup flow), run: + +```bash +pnpm reset-users +``` + +This will remove all users and their associated data from the database, allowing you to test the signup flow. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..6101b7a --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,111 @@ +version: '3.8' + +# Development environment with local Gitea instance for testing +# Run with: docker-compose -f docker-compose.dev.yml up -d + +services: + # Local Gitea instance for testing + gitea: + image: gitea/gitea:latest + container_name: gitea + restart: unless-stopped + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=sqlite3 + - GITEA__database__PATH=/data/gitea.db + - GITEA__server__DOMAIN=localhost + - GITEA__server__ROOT_URL=http://localhost:3001/ + - GITEA__server__SSH_DOMAIN=localhost + - GITEA__server__SSH_PORT=2222 + - GITEA__server__START_SSH_SERVER=true + - GITEA__security__INSTALL_LOCK=true + - GITEA__service__DISABLE_REGISTRATION=false + ports: + - "3001:3000" + - "2222:22" + volumes: + - gitea-data:/data + - gitea-config:/etc/gitea + networks: + - gitea-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + # Development service connected to local Gitea + gitea-mirror-dev: + image: ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-arunavo4/gitea-mirror}:${DOCKER_TAG:-latest} + build: + context: . + dockerfile: Dockerfile + platforms: + - linux/amd64 + - linux/arm64 + container_name: gitea-mirror-dev + restart: unless-stopped + ports: + - "4321:4321" + volumes: + - gitea-mirror-data:/app/data + depends_on: + - gitea + - redis + environment: + - NODE_ENV=development + - DATABASE_URL=sqlite://data/gitea-mirror.db + - HOST=0.0.0.0 + - PORT=4321 + - JWT_SECRET=dev-secret-key + # GitHub/Gitea Mirror Config + - GITHUB_USERNAME=${GITHUB_USERNAME:-your-github-username} + - GITHUB_TOKEN=${GITHUB_TOKEN:-your-github-token} + - SKIP_FORKS=${SKIP_FORKS:-false} + - PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false} + - MIRROR_ISSUES=${MIRROR_ISSUES:-false} + - MIRROR_STARRED=${MIRROR_STARRED:-false} + - MIRROR_ORGANIZATIONS=${MIRROR_ORGANIZATIONS:-false} + - PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false} + - ONLY_MIRROR_ORGS=${ONLY_MIRROR_ORGS:-false} + - SKIP_STARRED_ISSUES=${SKIP_STARRED_ISSUES:-false} + - GITEA_URL=http://gitea:3000 + - GITEA_TOKEN=${GITEA_TOKEN:-your-local-gitea-token} + - GITEA_USERNAME=${GITEA_USERNAME:-your-local-gitea-username} + - GITEA_ORGANIZATION=${GITEA_ORGANIZATION:-github-mirrors} + - GITEA_ORG_VISIBILITY=${GITEA_ORG_VISIBILITY:-public} + - DELAY=${DELAY:-3600} + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4321/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 5s + networks: + - gitea-network + + redis: + image: redis:7-alpine + container_name: redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis-data:/data + networks: + - gitea-network + +# Define named volumes for data persistence +volumes: + gitea-data: # Gitea data volume + gitea-config: # Gitea config volume + gitea-mirror-data: # Gitea Mirror database volume + + redis-data: + +# Define networks +networks: + gitea-network: + name: gitea-network diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..467c5bc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +# Gitea Mirror deployment configuration +# - production: Standard deployment with real data + +services: + # Production service with real data + gitea-mirror: + image: ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-gitea-mirror}:${DOCKER_TAG:-latest} + build: + context: . + dockerfile: Dockerfile + platforms: + - linux/amd64 + - linux/arm64 + cache_from: + - ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-gitea-mirror}:${DOCKER_TAG:-latest} + container_name: gitea-mirror + restart: unless-stopped + ports: + - "4321:4321" + volumes: + - gitea-mirror-data:/app/data + depends_on: + - redis + environment: + - NODE_ENV=production + - DATABASE_URL=sqlite://data/gitea-mirror.db + - HOST=0.0.0.0 + - PORT=4321 + - JWT_SECRET=${JWT_SECRET:-your-secret-key-change-this-in-production} + # GitHub/Gitea Mirror Config + - GITHUB_USERNAME=${GITHUB_USERNAME:-} + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - SKIP_FORKS=${SKIP_FORKS:-false} + - PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false} + - MIRROR_ISSUES=${MIRROR_ISSUES:-false} + - MIRROR_STARRED=${MIRROR_STARRED:-false} + - MIRROR_ORGANIZATIONS=${MIRROR_ORGANIZATIONS:-false} + - PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false} + - ONLY_MIRROR_ORGS=${ONLY_MIRROR_ORGS:-false} + - SKIP_STARRED_ISSUES=${SKIP_STARRED_ISSUES:-false} + - GITEA_URL=${GITEA_URL:-} + - GITEA_TOKEN=${GITEA_TOKEN:-} + - GITEA_USERNAME=${GITEA_USERNAME:-} + - GITEA_ORGANIZATION=${GITEA_ORGANIZATION:-github-mirrors} + - GITEA_ORG_VISIBILITY=${GITEA_ORG_VISIBILITY:-public} + - DELAY=${DELAY:-3600} + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 15s + profiles: ["production"] + + redis: + image: redis:7-alpine + container_name: redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis-data:/data + +# Define named volumes for database persistence +volumes: + gitea-mirror-data: # Database volume + redis-data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..c0a52f6 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,45 @@ +#!/bin/sh +set -e + + +# Ensure data directory exists +mkdir -p /app/data + +# If pnpm is available, run setup (for dev images), else run node init directly +if command -v pnpm >/dev/null 2>&1; then + echo "Running pnpm setup (if needed)..." + pnpm setup || true +fi + +# Initialize the database if it doesn't exist +if [ ! -f "/app/data/gitea-mirror.db" ]; then + echo "Initializing database..." + if [ -f "scripts/init-db.ts" ]; then + node -r tsx/cjs scripts/init-db.ts + elif [ -f "scripts/manage-db.ts" ]; then + node -r tsx/cjs scripts/manage-db.ts init + fi +else + echo "Database already exists, checking for issues..." + if [ -f "scripts/fix-db-issues.ts" ]; then + node -r tsx/cjs scripts/fix-db-issues.ts + elif [ -f "scripts/manage-db.ts" ]; then + node -r tsx/cjs scripts/manage-db.ts fix + fi + + # Update the database schema + echo "Updating database schema..." + if [ -f "scripts/manage-db.ts" ]; then + node -r tsx/cjs scripts/manage-db.ts update-schema + fi + + # Run migrations + echo "Running database migrations..." + if [ -f "scripts/run-migrations.ts" ]; then + node -r tsx/cjs scripts/run-migrations.ts + fi +fi + +# Start the application +echo "Starting Gitea Mirror..." +exec node ./dist/server/entry.mjs diff --git a/package.json b/package.json new file mode 100644 index 0000000..48baf3c --- /dev/null +++ b/package.json @@ -0,0 +1,87 @@ +{ + "name": "gitea-mirror", + "type": "module", + "version": "0.0.1", + "scripts": { + "setup": "pnpm install && pnpm manage-db init", + "dev": "astro dev", + "dev:clean": "pnpm cleanup-db && pnpm manage-db init && astro dev", + "build": "astro build", + "cleanup-db": "rm -f gitea-mirror.db data/gitea-mirror.db", + "manage-db": "tsx scripts/manage-db.ts", + "init-db": "tsx scripts/manage-db.ts init", + "check-db": "tsx scripts/manage-db.ts check", + "fix-db": "tsx scripts/manage-db.ts fix", + "reset-users": "tsx scripts/manage-db.ts reset-users", + "update-schema": "tsx scripts/manage-db.ts update-schema", + "db-auto": "tsx scripts/manage-db.ts auto", + "run-migrations": "tsx scripts/run-migrations.ts", + "preview": "astro preview", + "start": "node dist/server/entry.mjs", + "start:fresh": "pnpm cleanup-db && pnpm manage-db init && node dist/server/entry.mjs", + "test": "vitest run", + "test:watch": "vitest", + "astro": "astro" + }, + "dependencies": { + "@astrojs/mdx": "^4.2.6", + "@astrojs/node": "^9.2.1", + "@astrojs/react": "^4.2.7", + "@libsql/client": "^0.15.4", + "@octokit/rest": "^21.1.1", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-popover": "^1.1.13", + "@radix-ui/react-radio-group": "^1.3.6", + "@radix-ui/react-select": "^2.1.7", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-tooltip": "^1.2.6", + "@tailwindcss/vite": "^4.1.3", + "@tanstack/react-virtual": "^3.13.8", + "@types/canvas-confetti": "^1.9.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "astro": "^5.7.10", + "axios": "^1.8.4", + "bcryptjs": "^3.0.2", + "canvas-confetti": "^1.9.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "drizzle-orm": "^0.41.0", + "fuse.js": "^7.1.0", + "ioredis": "^5.6.1", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.488.0", + "next-themes": "^0.4.6", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-icons": "^5.5.0", + "sonner": "^2.0.3", + "superagent": "^10.2.0", + "tailwind-merge": "^3.2.0", + "tailwindcss": "^4.1.3", + "tw-animate-css": "^1.2.5", + "uuid": "^11.1.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/bcryptjs": "^3.0.0", + "@types/better-sqlite3": "^7.6.13", + "@types/jsonwebtoken": "^9.0.9", + "@types/superagent": "^8.1.9", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^4.4.0", + "better-sqlite3": "^9.6.0", + "jsdom": "^26.1.0", + "tsx": "^4.19.3", + "vitest": "^3.1.1" + }, + "packageManager": "pnpm@10.10.0" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..fcbe522 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7713 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/mdx': + specifier: ^4.2.6 + version: 4.2.6(astro@5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3)) + '@astrojs/node': + specifier: ^9.2.1 + version: 9.2.1(astro@5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3)) + '@astrojs/react': + specifier: ^4.2.7 + version: 4.2.7(@types/node@22.15.12)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(jiti@2.4.2)(lightningcss@1.29.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tsx@4.19.4) + '@libsql/client': + specifier: ^0.15.4 + version: 0.15.4 + '@octokit/rest': + specifier: ^21.1.1 + version: 21.1.1 + '@radix-ui/react-avatar': + specifier: ^1.1.4 + version: 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-checkbox': + specifier: ^1.1.5 + version: 1.3.1(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': + specifier: ^1.1.7 + version: 1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.7 + version: 2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-label': + specifier: ^2.1.6 + version: 2.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-popover': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-radio-group': + specifier: ^1.3.6 + version: 1.3.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-select': + specifier: ^2.1.7 + version: 2.2.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': + specifier: ^1.2.0 + version: 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-tabs': + specifier: ^1.1.4 + version: 1.1.11(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tailwindcss/vite': + specifier: ^4.1.3 + version: 4.1.5(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + '@tanstack/react-virtual': + specifier: ^3.13.8 + version: 3.13.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/canvas-confetti': + specifier: ^1.9.0 + version: 1.9.0 + '@types/react': + specifier: ^19.1.2 + version: 19.1.3 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.3(@types/react@19.1.3) + astro: + specifier: ^5.7.10 + version: 5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3) + axios: + specifier: ^1.8.4 + version: 1.9.0 + bcryptjs: + specifier: ^3.0.2 + version: 3.0.2 + canvas-confetti: + specifier: ^1.9.3 + version: 1.9.3 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + drizzle-orm: + specifier: ^0.41.0 + version: 0.41.0(@libsql/client@0.15.4)(@types/better-sqlite3@7.6.13)(better-sqlite3@9.6.0) + fuse.js: + specifier: ^7.1.0 + version: 7.1.0 + ioredis: + specifier: ^5.6.1 + version: 5.6.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + lucide-react: + specifier: ^0.488.0 + version: 0.488.0(react@19.1.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.1.0) + sonner: + specifier: ^2.0.3 + version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + superagent: + specifier: ^10.2.0 + version: 10.2.1 + tailwind-merge: + specifier: ^3.2.0 + version: 3.2.0 + tailwindcss: + specifier: ^4.1.3 + version: 4.1.5 + tw-animate-css: + specifier: ^1.2.5 + version: 1.2.9 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + zod: + specifier: ^3.24.2 + version: 3.24.4 + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/bcryptjs': + specifier: ^3.0.0 + version: 3.0.0 + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.9 + '@types/superagent': + specifier: ^8.1.9 + version: 8.1.9 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.4.1(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + better-sqlite3: + specifier: ^9.6.0 + version: 9.6.0 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + tsx: + specifier: ^4.19.3 + version: 4.19.4 + vitest: + specifier: ^3.1.1 + version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.12)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) + +packages: + + '@adobe/css-tools@4.4.2': + resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@asamuzakjp/css-color@3.1.7': + resolution: {integrity: sha512-Ok5fYhtwdyJQmU1PpEv6Si7Y+A4cYb8yNM9oiIJC9TzXPMuN9fvdonKJqcnz9TbFqV6bQ8z0giRq0iaOpGZV2g==} + + '@astrojs/compiler@2.12.0': + resolution: {integrity: sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA==} + + '@astrojs/internal-helpers@0.6.1': + resolution: {integrity: sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A==} + + '@astrojs/markdown-remark@6.3.1': + resolution: {integrity: sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg==} + + '@astrojs/mdx@4.2.6': + resolution: {integrity: sha512-0i/GmOm6d0qq1/SCfcUgY/IjDc/bS0i42u7h85TkPFBmlFOcBZfkYhR5iyz6hZLwidvJOEq5yGfzt9B1Azku4w==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} + peerDependencies: + astro: ^5.0.0 + + '@astrojs/node@9.2.1': + resolution: {integrity: sha512-kEHLB37ooW91p7FLGalqa3jVQRIafntfKiZgCnjN1lEYw+j8NP6VJHQbLHmzzbtKUI0J+srGiTnGZmaHErHE5w==} + peerDependencies: + astro: ^5.3.0 + + '@astrojs/prism@3.2.0': + resolution: {integrity: sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} + + '@astrojs/react@4.2.7': + resolution: {integrity: sha512-/wM90noT/6QyJEOGdDmDbq2D9qZooKTJNG1M8olmsW5ns6bJ7uxG5fzkYxcpA3WUTD6Dj6NtpEqchvb5h8Fa+g==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} + peerDependencies: + '@types/react': ^17.0.50 || ^18.0.21 || ^19.0.0 + '@types/react-dom': ^17.0.17 || ^18.0.6 || ^19.0.0 + react: ^17.0.2 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 + + '@astrojs/telemetry@3.2.1': + resolution: {integrity: sha512-SSVM820Jqc6wjsn7qYfV9qfeQvePtVc1nSofhyap7l0/iakUKywj3hfy3UJAOV4sGV4Q/u450RD4AaCaFvNPlg==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.1': + resolution: {integrity: sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.1': + resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.1': + resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.1': + resolution: {integrity: sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.1': + resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.1': + resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.1': + resolution: {integrity: sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.1': + resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.1': + resolution: {integrity: sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.1': + resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + engines: {node: '>=6.9.0'} + + '@capsizecss/unpack@2.4.0': + resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} + + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.3': + resolution: {integrity: sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.9': + resolution: {integrity: sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.0': + resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} + + '@floating-ui/dom@1.7.0': + resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@libsql/client@0.15.4': + resolution: {integrity: sha512-m8a7giWlhLdfKVIZFd3UlBptWTS+H0toSOL09BxbqzBeFHwuVC+5ewyi4LMBxoy2TLNQGE4lO8cwpsTWmu695w==} + + '@libsql/core@0.15.4': + resolution: {integrity: sha512-NMvh6xnn3vrcd7DNehj0HiJcRWB2a8hHhJUTkOBej3Pf3KB21HOmdOUjXxJ5pGbjWXh4ezQBmHtF5ozFhocXaA==} + + '@libsql/darwin-arm64@0.5.8': + resolution: {integrity: sha512-dJRfwCHAKOIgysMbB+PBo3ZmCVuGC02fH57kFEFlqbbUv6wnAZV5g7GErQIv4IlC4VPKAS4RL20fjLUgXE+0Xg==} + cpu: [arm64] + os: [darwin] + + '@libsql/darwin-x64@0.5.8': + resolution: {integrity: sha512-ua5ngqJy9o4lyjYDzF8c69YbOAwP2TQXPhDURs8l97b09HHFh5/8gWRNor7vYRpsziwp8TC77DdQ0C84+gP5tg==} + cpu: [x64] + os: [darwin] + + '@libsql/hrana-client@0.7.0': + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} + + '@libsql/isomorphic-fetch@0.3.1': + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} + engines: {node: '>=18.0.0'} + + '@libsql/isomorphic-ws@0.1.5': + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + + '@libsql/linux-arm64-gnu@0.5.8': + resolution: {integrity: sha512-6HHZlPbMu+cmCJafg/dwOcWFMu07hTB5teMKU5ke66kqeWLRBnOs5/DnZGVz6q0k+Z4L4UTRbdrnCklR3GmvFg==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-arm64-musl@0.5.8': + resolution: {integrity: sha512-QGhZadKk3gvrDHa63U7xQrsqET/43E6L7/15oh7I+SINl8meoZAJNTJNYfOUmPM2lPPfNDgr46v4p5ggo6su0A==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-x64-gnu@0.5.8': + resolution: {integrity: sha512-HUWxOvLE5W287O/vaHWFpZMqaaebEBZvcUqJJ/E+IcC9kmKc6GqDW+fJkfPfosrpGyPNbYDj0w9pIcck0l/oeA==} + cpu: [x64] + os: [linux] + + '@libsql/linux-x64-musl@0.5.8': + resolution: {integrity: sha512-hfhkPwqzFroU01xUB7ZXFw3bP+jqcGolGLyhEkeh/Rsoune0ucm1KPrU2tqTBqQP4a7lL0nSL1A37nfjIO61Hw==} + cpu: [x64] + os: [linux] + + '@libsql/win32-x64-msvc@0.5.8': + resolution: {integrity: sha512-8hKczus0swLEvXu8N0znWdyFo5QzFnE9mnz7G/sb+eVn+zpPlT6ZdFHZdhQzev9C0to7kvYDj03qESTUIwDiqg==} + cpu: [x64] + os: [win32] + + '@mdx-js/mdx@3.1.0': + resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + + '@neon-rs/load@0.0.4': + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.5': + resolution: {integrity: sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.2.2': + resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/openapi-types@25.0.0': + resolution: {integrity: sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==} + + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.2.3': + resolution: {integrity: sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==} + engines: {node: '>= 18'} + + '@octokit/rest@21.1.1': + resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@14.0.0': + resolution: {integrity: sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-arrow@1.1.6': + resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.9': + resolution: {integrity: sha512-10tQokfvZdFvnvDkcOJPjm2pWiP8A0R4T83MoD7tb15bC/k2GU7B1YBuzJi8lNQ8V1QqhP8ocNqp27ByZaNagQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.1': + resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.6': + resolution: {integrity: sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.13': + resolution: {integrity: sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.9': + resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.14': + resolution: {integrity: sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.6': + resolution: {integrity: sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.6': + resolution: {integrity: sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.14': + resolution: {integrity: sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.13': + resolution: {integrity: sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.6': + resolution: {integrity: sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.8': + resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.2': + resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.6': + resolution: {integrity: sha512-1tfTAqnYZNVwSpFhCT273nzK8qGBReeYnNTPspCggqk1fvIrfVxJekIuBFidNivzpdiMqDwVGnQvHqXrRPM4Og==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.9': + resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.4': + resolution: {integrity: sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.2': + resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.11': + resolution: {integrity: sha512-4FiKSVoXqPP/KfzlB7lwwqoFV6EPwkrrqGp9cUYXjwDYHhvpnqq79P+EPHKcdoTE7Rl8w/+6s9rTlsfXHES9GA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.6': + resolution: {integrity: sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.2': + resolution: {integrity: sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.40.2': + resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.40.2': + resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.40.2': + resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.40.2': + resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.40.2': + resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.40.2': + resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.40.2': + resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.40.2': + resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.40.2': + resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.40.2': + resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.40.2': + resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.40.2': + resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.40.2': + resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.40.2': + resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.40.2': + resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.40.2': + resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.40.2': + resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.4.0': + resolution: {integrity: sha512-0YOzTSRDn/IAfQWtK791gs1u8v87HNGToU6IwcA3K7nPoVOrS2Dh6X6A6YfXgPTSkTwR5y6myk0MnI0htjnwrA==} + + '@shikijs/engine-javascript@3.4.0': + resolution: {integrity: sha512-1ywDoe+z/TPQKj9Jw0eU61B003J9DqUFRfH+DVSzdwPUFhR7yOmfyLzUrFz0yw8JxFg/NgzXoQyyykXgO21n5Q==} + + '@shikijs/engine-oniguruma@3.4.0': + resolution: {integrity: sha512-zwcWlZ4OQuJ/+1t32ClTtyTU1AiDkK1lhtviRWoq/hFqPjCNyLj22bIg9rB7BfoZKOEOfrsGz7No33BPCf+WlQ==} + + '@shikijs/langs@3.4.0': + resolution: {integrity: sha512-bQkR+8LllaM2duU9BBRQU0GqFTx7TuF5kKlw/7uiGKoK140n1xlLAwCgXwSxAjJ7Htk9tXTFwnnsJTCU5nDPXQ==} + + '@shikijs/themes@3.4.0': + resolution: {integrity: sha512-YPP4PKNFcFGLxItpbU0ZW1Osyuk8AyZ24YEFaq04CFsuCbcqydMvMUTi40V2dkc0qs1U2uZFrnU6s5zI6IH+uA==} + + '@shikijs/types@3.4.0': + resolution: {integrity: sha512-EUT/0lGiE//7j5N/yTMNMT3eCWNcHJLrRKxT0NDXWIfdfSmFJKfPX7nMmRBrQnWboAzIsUziCThrYMMhjbMS1A==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@tailwindcss/node@4.1.5': + resolution: {integrity: sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==} + + '@tailwindcss/oxide-android-arm64@4.1.5': + resolution: {integrity: sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + resolution: {integrity: sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.5': + resolution: {integrity: sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + resolution: {integrity: sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + resolution: {integrity: sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + resolution: {integrity: sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + resolution: {integrity: sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + resolution: {integrity: sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + resolution: {integrity: sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + resolution: {integrity: sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + resolution: {integrity: sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + resolution: {integrity: sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.5': + resolution: {integrity: sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.5': + resolution: {integrity: sha512-FE1stRoqdHSb7RxesMfCXE8icwI1W6zGE/512ae3ZDrpkQYTTYeSyUJPRCjZd8CwVAhpDUbi1YR8pcZioFJQ/w==} + peerDependencies: + vite: ^5.2.0 || ^6 + + '@tanstack/react-virtual@3.13.8': + resolution: {integrity: sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.8': + resolution: {integrity: sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==} + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.0': + resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/bcryptjs@3.0.0': + resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} + deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/canvas-confetti@1.9.0': + resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/jsonwebtoken@9.0.9': + resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/node@22.15.12': + resolution: {integrity: sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==} + + '@types/react-dom@19.1.3': + resolution: {integrity: sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.3': + resolution: {integrity: sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react@4.4.1': + resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + '@vitest/expect@3.1.3': + resolution: {integrity: sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==} + + '@vitest/mocker@3.1.3': + resolution: {integrity: sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.1.3': + resolution: {integrity: sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==} + + '@vitest/runner@3.1.3': + resolution: {integrity: sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==} + + '@vitest/snapshot@3.1.3': + resolution: {integrity: sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==} + + '@vitest/spy@3.1.3': + resolution: {integrity: sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==} + + '@vitest/utils@3.1.3': + resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + astro@5.7.10: + resolution: {integrity: sha512-9TQcFZqP2w6//JXXUHfw8/5PX7KUx9EkG5O3m+hISuyeUztvjY1q5+p7+C5HiXyg24Zs3KkpieoL5BGRXGCAGA==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcryptjs@3.0.2: + resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==} + hasBin: true + + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + + better-sqlite3@9.6.0: + resolution: {integrity: sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + blob-to-buffer@1.2.9: + resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserslist@4.24.5: + resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + caniuse-lite@1.0.30001717: + resolution: {integrity: sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==} + + canvas-confetti@1.9.3: + resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + ci-info@4.2.0: + resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} + engines: {node: '>=8'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + crossws@0.3.4: + resolution: {integrity: sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.3.1: + resolution: {integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==} + engines: {node: '>=18'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + + decode-named-character-reference@1.1.0: + resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + deterministic-object-hash@2.0.2: + resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} + engines: {node: '>=18'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + drizzle-orm@0.41.0: + resolution: {integrity: sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.150: + resolution: {integrity: sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + engines: {node: '>=12.0.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + h3@1.15.3: + resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + ioredis@5.6.1: + resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} + engines: {node: '>=12.22.0'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + libsql@0.5.8: + resolution: {integrity: sha512-+OopMI1wM/NvAJTHf3O3+beHd1YfKLnSVsOGBl3/7UBDZ4ydVadkbBk5Hjjs9d3ALC5rBaftMY59AvwyC8MzPw==} + os: [darwin, linux, win32] + + lightningcss-darwin-arm64@1.29.2: + resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.2: + resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.2: + resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.2: + resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.2: + resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.2: + resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.2: + resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.2: + resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.2: + resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.2: + resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.2: + resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + engines: {node: '>= 12.0.0'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.488.0: + resolution: {integrity: sha512-ronlL0MyKut4CEzBY/ai2ZpKPxyWO4jUqdAkm2GNK5Zn3Rj+swDz+3lvyAUXN0PNqPKIX6XM9Xadwz/skLs/pQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-abi@3.75.0: + resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + engines: {node: '>=10'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-mock-http@1.0.0: + resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nwsapi@2.2.20: + resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} + engines: {node: '>=18'} + + p-queue@8.1.0: + resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==} + engines: {node: '>=18'} + + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.6.3: + resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.0: + resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.0: + resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + rollup@4.40.2: + resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + server-destroy@1.0.1: + resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shiki@3.4.0: + resolution: {integrity: sha512-Ni80XHcqhOEXv5mmDAvf5p6PAJqbUc/RzFeaOqk+zP5DLvTPS3j0ckvA+MI87qoxTQ5RGJDVTbdl/ENLSyyAnQ==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.3.4: + resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==} + engines: {node: '>= 18'} + + sonner@2.0.3: + resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + style-to-js@1.1.16: + resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + + superagent@10.2.1: + resolution: {integrity: sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==} + engines: {node: '>=14.18.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwind-merge@3.2.0: + resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==} + + tailwindcss@4.1.5: + resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-fs@2.1.2: + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tsconfck@3.1.5: + resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.19.4: + resolution: {integrity: sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==} + engines: {node: '>=18.0.0'} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tw-animate-css@1.2.9: + resolution: {integrity: sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.4.1: + resolution: {integrity: sha512-zKSY9qO8svWYns+FGKjyVdLvpGPwqmsCjeJLN1xndMiqxHWBAhoWDMYMG960MxeV48clBmG+fDP59dHY1VoZvg==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + + unstorage@1.16.0: + resolution: {integrity: sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@3.1.3: + resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + 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 + + vitefu@1.0.6: + resolution: {integrity: sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@3.1.3: + resolution: {integrity: sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.1.3 + '@vitest/ui': 3.1.3 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + 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 + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + yocto-spinner@0.2.2: + resolution: {integrity: sha512-21rPcM3e4vCpOXThiFRByX8amU5By1R0wNS8Oex+DP3YgC8xdU0vEJ/K8cbPLiIJVosSSysgcFof6s6MSD5/Vw==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + + zod-to-ts@1.2.0: + resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} + peerDependencies: + typescript: ^4.9.4 || ^5.0.2 + zod: ^3 + + zod@3.24.4: + resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@adobe/css-tools@4.4.2': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@asamuzakjp/css-color@3.1.7': + dependencies: + '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + + '@astrojs/compiler@2.12.0': {} + + '@astrojs/internal-helpers@0.6.1': {} + + '@astrojs/markdown-remark@6.3.1': + dependencies: + '@astrojs/internal-helpers': 0.6.1 + '@astrojs/prism': 3.2.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + import-meta-resolve: 4.1.0 + js-yaml: 4.1.0 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + shiki: 3.4.0 + smol-toml: 1.3.4 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/mdx@4.2.6(astro@5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3))': + dependencies: + '@astrojs/markdown-remark': 6.3.1 + '@mdx-js/mdx': 3.1.0(acorn@8.14.1) + acorn: 8.14.1 + astro: 5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3) + es-module-lexer: 1.7.0 + estree-util-visit: 2.0.0 + hast-util-to-html: 9.0.5 + kleur: 4.1.5 + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + remark-smartypants: 3.0.2 + source-map: 0.7.4 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/node@9.2.1(astro@5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3))': + dependencies: + '@astrojs/internal-helpers': 0.6.1 + astro: 5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3) + send: 1.2.0 + server-destroy: 1.0.1 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.2.0': + dependencies: + prismjs: 1.30.0 + + '@astrojs/react@4.2.7(@types/node@22.15.12)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(jiti@2.4.2)(lightningcss@1.29.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tsx@4.19.4)': + dependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + '@vitejs/plugin-react': 4.4.1(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + ultrahtml: 1.6.0 + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@astrojs/telemetry@3.2.1': + dependencies: + ci-info: 4.2.0 + debug: 4.4.0 + dlv: 1.1.3 + dset: 3.1.4 + is-docker: 3.0.0 + is-wsl: 3.1.0 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.1': {} + + '@babel/core@7.27.1': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/helper-compilation-targets': 7.27.1 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helpers': 7.27.1 + '@babel/parser': 7.27.1 + '@babel/template': 7.27.1 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.1': + dependencies: + '@babel/parser': 7.27.1 + '@babel/types': 7.27.1 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.1': + dependencies: + '@babel/compat-data': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.1': + dependencies: + '@babel/template': 7.27.1 + '@babel/types': 7.27.1 + + '@babel/parser@7.27.1': + dependencies: + '@babel/types': 7.27.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.27.1': {} + + '@babel/template@7.27.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.1 + '@babel/types': 7.27.1 + + '@babel/traverse@7.27.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/parser': 7.27.1 + '@babel/template': 7.27.1 + '@babel/types': 7.27.1 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@capsizecss/unpack@2.4.0': + dependencies: + blob-to-buffer: 1.2.9 + cross-fetch: 3.2.0 + fontkit: 2.0.4 + transitivePeerDependencies: + - encoding + + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.4': + optional: true + + '@esbuild/android-arm64@0.25.4': + optional: true + + '@esbuild/android-arm@0.25.4': + optional: true + + '@esbuild/android-x64@0.25.4': + optional: true + + '@esbuild/darwin-arm64@0.25.4': + optional: true + + '@esbuild/darwin-x64@0.25.4': + optional: true + + '@esbuild/freebsd-arm64@0.25.4': + optional: true + + '@esbuild/freebsd-x64@0.25.4': + optional: true + + '@esbuild/linux-arm64@0.25.4': + optional: true + + '@esbuild/linux-arm@0.25.4': + optional: true + + '@esbuild/linux-ia32@0.25.4': + optional: true + + '@esbuild/linux-loong64@0.25.4': + optional: true + + '@esbuild/linux-mips64el@0.25.4': + optional: true + + '@esbuild/linux-ppc64@0.25.4': + optional: true + + '@esbuild/linux-riscv64@0.25.4': + optional: true + + '@esbuild/linux-s390x@0.25.4': + optional: true + + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + + '@esbuild/openbsd-x64@0.25.4': + optional: true + + '@esbuild/sunos-x64@0.25.4': + optional: true + + '@esbuild/win32-arm64@0.25.4': + optional: true + + '@esbuild/win32-ia32@0.25.4': + optional: true + + '@esbuild/win32-x64@0.25.4': + optional: true + + '@floating-ui/core@1.7.0': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.7.0': + dependencies: + '@floating-ui/core': 1.7.0 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/utils@0.2.9': {} + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@ioredis/commands@1.2.0': {} + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@libsql/client@0.15.4': + dependencies: + '@libsql/core': 0.15.4 + '@libsql/hrana-client': 0.7.0 + js-base64: 3.7.7 + libsql: 0.5.8 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/core@0.15.4': + dependencies: + js-base64: 3.7.7 + + '@libsql/darwin-arm64@0.5.8': + optional: true + + '@libsql/darwin-x64@0.5.8': + optional: true + + '@libsql/hrana-client@0.7.0': + dependencies: + '@libsql/isomorphic-fetch': 0.3.1 + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.7.7 + node-fetch: 3.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/isomorphic-fetch@0.3.1': {} + + '@libsql/isomorphic-ws@0.1.5': + dependencies: + '@types/ws': 8.18.1 + ws: 8.18.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/linux-arm64-gnu@0.5.8': + optional: true + + '@libsql/linux-arm64-musl@0.5.8': + optional: true + + '@libsql/linux-x64-gnu@0.5.8': + optional: true + + '@libsql/linux-x64-musl@0.5.8': + optional: true + + '@libsql/win32-x64-msvc@0.5.8': + optional: true + + '@mdx-js/mdx@3.1.0(acorn@8.14.1)': + dependencies: + '@types/estree': 1.0.7 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.14.1) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.4 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - acorn + - supports-color + + '@neon-rs/load@0.0.4': {} + + '@noble/hashes@1.8.0': {} + + '@octokit/auth-token@5.1.2': {} + + '@octokit/core@6.1.5': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.2 + '@octokit/request': 9.2.3 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.0.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.4': + dependencies: + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.2.2': + dependencies: + '@octokit/request': 9.2.3 + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@24.2.0': {} + + '@octokit/openapi-types@25.0.0': {} + + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.5)': + dependencies: + '@octokit/core': 6.1.5 + '@octokit/types': 13.10.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.5)': + dependencies: + '@octokit/core': 6.1.5 + + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.5)': + dependencies: + '@octokit/core': 6.1.5 + '@octokit/types': 13.10.0 + + '@octokit/request-error@6.1.8': + dependencies: + '@octokit/types': 14.0.0 + + '@octokit/request@9.2.3': + dependencies: + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.0.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.2 + + '@octokit/rest@21.1.1': + dependencies: + '@octokit/core': 6.1.5 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.5) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.5) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.5) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + + '@octokit/types@14.0.0': + dependencies: + '@octokit/openapi-types': 25.0.0 + + '@oslojs/encoding@1.1.0': {} + + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.8.0 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-arrow@1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-avatar@1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-checkbox@1.3.1(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-collection@1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-context@1.1.2(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-dialog@1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-direction@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-dropdown-menu@2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-menu': 2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-id@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-label@2.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-menu@2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-popover@1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-popper@1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-portal@1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-primitive@2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-radio-group@1.3.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-roving-focus@1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-select@2.2.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-slot@1.2.2(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-tabs@1.1.11(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-tooltip@1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.3)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.3 + + '@radix-ui/react-visually-hidden@1.2.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@radix-ui/rect@1.1.1': {} + + '@rollup/pluginutils@5.1.4(rollup@4.40.2)': + dependencies: + '@types/estree': 1.0.7 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.40.2 + + '@rollup/rollup-android-arm-eabi@4.40.2': + optional: true + + '@rollup/rollup-android-arm64@4.40.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.40.2': + optional: true + + '@rollup/rollup-darwin-x64@4.40.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.40.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.40.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.40.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.40.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.40.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.40.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.40.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.40.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.40.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.40.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.40.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.40.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.40.2': + optional: true + + '@shikijs/core@3.4.0': + dependencies: + '@shikijs/types': 3.4.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.4.0': + dependencies: + '@shikijs/types': 3.4.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + + '@shikijs/engine-oniguruma@3.4.0': + dependencies: + '@shikijs/types': 3.4.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.4.0': + dependencies: + '@shikijs/types': 3.4.0 + + '@shikijs/themes@3.4.0': + dependencies: + '@shikijs/types': 3.4.0 + + '@shikijs/types@3.4.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.5': + dependencies: + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.29.2 + tailwindcss: 4.1.5 + + '@tailwindcss/oxide-android-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide@4.1.5': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-x64': 4.1.5 + '@tailwindcss/oxide-freebsd-x64': 4.1.5 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.5 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.5 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-x64-musl': 4.1.5 + '@tailwindcss/oxide-wasm32-wasi': 4.1.5 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.5 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.5 + + '@tailwindcss/vite@4.1.5(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4))': + dependencies: + '@tailwindcss/node': 4.1.5 + '@tailwindcss/oxide': 4.1.5 + tailwindcss: 4.1.5 + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + + '@tanstack/react-virtual@3.13.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/virtual-core': 3.13.8 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@tanstack/virtual-core@3.13.8': {} + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.27.1 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.2 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@testing-library/dom': 10.4.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@types/react-dom': 19.1.3(@types/react@19.1.3) + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.27.1 + '@babel/types': 7.27.1 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.1 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.27.1 + '@babel/types': 7.27.1 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.1 + + '@types/bcryptjs@3.0.0': + dependencies: + bcryptjs: 3.0.2 + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 22.15.12 + + '@types/canvas-confetti@1.9.0': {} + + '@types/cookiejar@2.1.5': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.7 + + '@types/estree@1.0.7': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/jsonwebtoken@9.0.9': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.15.12 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/methods@1.1.4': {} + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@22.15.12': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.1.3(@types/react@19.1.3)': + dependencies: + '@types/react': 19.1.3 + + '@types/react@19.1.3': + dependencies: + csstype: 3.1.3 + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.15.12 + form-data: 4.0.2 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/uuid@10.0.0': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.15.12 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4))': + dependencies: + '@babel/core': 7.27.1 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.1.3': + dependencies: + '@vitest/spy': 3.1.3 + '@vitest/utils': 3.1.3 + chai: 5.2.0 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4))': + dependencies: + '@vitest/spy': 3.1.3 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + + '@vitest/pretty-format@3.1.3': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.1.3': + dependencies: + '@vitest/utils': 3.1.3 + pathe: 2.0.3 + + '@vitest/snapshot@3.1.3': + dependencies: + '@vitest/pretty-format': 3.1.3 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.1.3': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@3.1.3': + dependencies: + '@vitest/pretty-format': 3.1.3 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + agent-base@7.1.3: {} + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.4: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + asap@2.0.6: {} + + assertion-error@2.0.1: {} + + astring@1.9.0: {} + + astro@5.7.10(@types/node@22.15.12)(ioredis@5.6.1)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.2)(tsx@4.19.4)(typescript@5.8.3): + dependencies: + '@astrojs/compiler': 2.12.0 + '@astrojs/internal-helpers': 0.6.1 + '@astrojs/markdown-remark': 6.3.1 + '@astrojs/telemetry': 3.2.1 + '@capsizecss/unpack': 2.4.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.1.4(rollup@4.40.2) + acorn: 8.14.1 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.2.0 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.0.2 + cssesc: 3.0.0 + debug: 4.4.0 + deterministic-object-hash: 2.0.2 + devalue: 5.1.1 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.25.4 + estree-walker: 3.0.3 + flattie: 1.1.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.1.1 + js-yaml: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.17 + magicast: 0.3.5 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.0 + package-manager-detector: 1.3.0 + picomatch: 4.0.2 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.7.1 + shiki: 3.4.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.13 + tsconfck: 3.1.5(typescript@5.8.3) + ultrahtml: 1.6.0 + unifont: 0.4.1 + unist-util-visit: 5.0.0 + unstorage: 1.16.0(ioredis@5.6.1) + vfile: 6.0.3 + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + vitefu: 1.0.6(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.2 + zod: 3.24.4 + zod-to-json-schema: 3.24.5(zod@3.24.4) + zod-to-ts: 1.2.0(typescript@5.8.3)(zod@3.24.4) + optionalDependencies: + sharp: 0.33.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - db0 + - encoding + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + + asynckit@0.4.0: {} + + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + base-64@1.0.0: {} + + base64-js@1.5.1: {} + + bcryptjs@3.0.2: {} + + before-after-hook@3.0.2: {} + + better-sqlite3@9.6.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blob-to-buffer@1.2.9: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserslist@4.24.5: + dependencies: + caniuse-lite: 1.0.30001717 + electron-to-chromium: 1.5.150 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.5) + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@8.0.0: {} + + caniuse-lite@1.0.30001717: {} + + canvas-confetti@1.9.3: {} + + ccount@2.0.1: {} + + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + check-error@2.1.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@1.1.4: {} + + ci-info@4.2.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-boxes@3.0.0: {} + + clone@2.1.2: {} + + clsx@2.1.1: {} + + cluster-key-slot@1.1.2: {} + + cmdk@1.1.1(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-dialog': 1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + collapse-white-space@2.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + common-ancestor-path@1.0.1: {} + + component-emitter@1.3.1: {} + + convert-source-map@2.0.0: {} + + cookie-es@1.2.2: {} + + cookie@1.0.2: {} + + cookiejar@2.1.4: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + crossws@0.3.4: + dependencies: + uncrypto: 0.1.3 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + cssesc@3.0.0: {} + + cssstyle@4.3.1: + dependencies: + '@asamuzakjp/css-color': 3.1.7 + rrweb-cssom: 0.8.0 + + csstype@3.1.3: {} + + data-uri-to-buffer@4.0.1: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decimal.js@10.5.0: {} + + decode-named-character-reference@1.1.0: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.0.2: {} + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + deterministic-object-hash@2.0.2: + dependencies: + base-64: 1.0.0 + + devalue@5.1.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + dfa@1.2.0: {} + + diff@5.2.0: {} + + dlv@1.1.3: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + drizzle-orm@0.41.0(@libsql/client@0.15.4)(@types/better-sqlite3@7.6.13)(better-sqlite3@9.6.0): + optionalDependencies: + '@libsql/client': 0.15.4 + '@types/better-sqlite3': 7.6.13 + better-sqlite3: 9.6.0 + + dset@3.1.4: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.150: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + entities@6.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.14.1 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.2 + + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@5.0.0: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.7 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.7 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.4 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.7 + + etag@1.8.1: {} + + eventemitter3@5.0.1: {} + + expand-template@2.0.3: {} + + expect-type@1.2.1: {} + + extend@3.0.2: {} + + fast-content-type-parse@2.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-safe-stringify@2.1.1: {} + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-uri-to-path@1.0.0: {} + + flattie@1.1.1: {} + + follow-redirects@1.15.9: {} + + fontkit@2.0.4: + dependencies: + '@swc/helpers': 0.5.17 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.2.2 + dezalgo: 1.0.4 + once: 1.4.0 + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fuse.js@7.1.0: {} + + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.3.0: + 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 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-from-package@0.0.0: {} + + github-slugger@2.0.0: {} + + globals@11.12.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + h3@1.15.3: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.4 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.0 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.0.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.7 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.16 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.7 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.16 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.1.1: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + import-meta-resolve@4.1.0: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inline-style-parser@0.2.4: {} + + ioredis@5.6.1: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.4.0 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + iron-webcrypto@1.2.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.3.2: + optional: true + + is-decimal@2.0.1: {} + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-hexadecimal@2.0.1: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-plain-obj@4.1.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + jiti@2.4.2: {} + + js-base64@3.7.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.3.1 + data-urls: 5.0.0 + decimal.js: 10.5.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.20 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.2 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.1 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + libsql@0.5.8: + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.5.8 + '@libsql/darwin-x64': 0.5.8 + '@libsql/linux-arm64-gnu': 0.5.8 + '@libsql/linux-arm64-musl': 0.5.8 + '@libsql/linux-x64-gnu': 0.5.8 + '@libsql/linux-x64-musl': 0.5.8 + '@libsql/win32-x64-msvc': 0.5.8 + + lightningcss-darwin-arm64@1.29.2: + optional: true + + lightningcss-darwin-x64@1.29.2: + optional: true + + lightningcss-freebsd-x64@1.29.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.2: + optional: true + + lightningcss-linux-arm64-gnu@1.29.2: + optional: true + + lightningcss-linux-arm64-musl@1.29.2: + optional: true + + lightningcss-linux-x64-gnu@1.29.2: + optional: true + + lightningcss-linux-x64-musl@1.29.2: + optional: true + + lightningcss-win32-arm64-msvc@1.29.2: + optional: true + + lightningcss-win32-x64-msvc@1.29.2: + optional: true + + lightningcss@1.29.2: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.2 + lightningcss-darwin-x64: 1.29.2 + lightningcss-freebsd-x64: 1.29.2 + lightningcss-linux-arm-gnueabihf: 1.29.2 + lightningcss-linux-arm64-gnu: 1.29.2 + lightningcss-linux-arm64-musl: 1.29.2 + lightningcss-linux-x64-gnu: 1.29.2 + lightningcss-linux-x64-musl: 1.29.2 + lightningcss-win32-arm64-msvc: 1.29.2 + lightningcss-win32-x64-msvc: 1.29.2 + + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lodash@4.17.21: {} + + longest-streak@3.1.0: {} + + loupe@3.1.3: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.488.0(react@19.1.0): + dependencies: + react: 19.1.0 + + lz-string@1.5.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.27.1 + '@babel/types': 7.27.1 + source-map-js: 1.2.1 + + markdown-extensions@2.0.0: {} + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.12.2: {} + + methods@1.1.2: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.7 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.7 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.2 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.7 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.7 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.7 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.2 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.0 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-response@3.1.0: {} + + min-indent@1.0.1: {} + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-build-utils@2.0.0: {} + + neotraverse@0.6.18: {} + + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-abi@3.75.0: + dependencies: + semver: 7.7.1 + + node-domexception@1.0.0: {} + + node-fetch-native@1.6.6: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-mock-http@1.0.0: {} + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + nwsapi@2.2.20: {} + + object-inspect@1.13.4: {} + + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.6 + ufo: 1.6.1 + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.3: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 + + p-limit@6.2.0: + dependencies: + yocto-queue: 1.2.1 + + p-queue@8.1.0: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.4 + + p-timeout@6.1.4: {} + + package-manager-detector@1.3.0: {} + + pako@0.2.9: {} + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.1.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.3.0: + dependencies: + entities: 6.0.0 + + pathe@2.0.3: {} + + pathval@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.0.4 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.75.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.2 + tunnel-agent: 0.6.0 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prismjs@1.30.0: {} + + promise-limit@2.7.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + property-information@7.0.0: {} + + proxy-from-env@1.1.0: {} + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + radix3@1.1.2: {} + + range-parser@1.2.1: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-icons@5.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + react-is@17.0.2: {} + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.1.3)(react@19.1.0): + dependencies: + react: 19.1.0 + react-style-singleton: 2.2.3(@types/react@19.1.3)(react@19.1.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.3 + + react-remove-scroll@2.6.3(@types/react@19.1.3)(react@19.1.0): + dependencies: + react: 19.1.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.3)(react@19.1.0) + react-style-singleton: 2.2.3(@types/react@19.1.3)(react@19.1.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.3)(react@19.1.0) + use-sidecar: 1.1.3(@types/react@19.1.3)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + + react-style-singleton@2.2.3(@types/react@19.1.3)(react@19.1.0): + dependencies: + get-nonce: 1.0.1 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.3 + + react@19.1.0: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.7 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.0(acorn@8.14.1): + dependencies: + acorn-jsx: 5.3.2(acorn@8.14.1) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.7 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.7 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.7 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.0: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + resolve-pkg-maps@1.0.0: {} + + restructure@3.0.2: {} + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.0.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + rollup@4.40.2: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.40.2 + '@rollup/rollup-android-arm64': 4.40.2 + '@rollup/rollup-darwin-arm64': 4.40.2 + '@rollup/rollup-darwin-x64': 4.40.2 + '@rollup/rollup-freebsd-arm64': 4.40.2 + '@rollup/rollup-freebsd-x64': 4.40.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.2 + '@rollup/rollup-linux-arm-musleabihf': 4.40.2 + '@rollup/rollup-linux-arm64-gnu': 4.40.2 + '@rollup/rollup-linux-arm64-musl': 4.40.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2 + '@rollup/rollup-linux-riscv64-gnu': 4.40.2 + '@rollup/rollup-linux-riscv64-musl': 4.40.2 + '@rollup/rollup-linux-s390x-gnu': 4.40.2 + '@rollup/rollup-linux-x64-gnu': 4.40.2 + '@rollup/rollup-linux-x64-musl': 4.40.2 + '@rollup/rollup-win32-arm64-msvc': 4.40.2 + '@rollup/rollup-win32-ia32-msvc': 4.40.2 + '@rollup/rollup-win32-x64-msvc': 4.40.2 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.1: {} + + send@1.2.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + server-destroy@1.0.1: {} + + setprototypeof@1.2.0: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + + shiki@3.4.0: + dependencies: + '@shikijs/core': 3.4.0 + '@shikijs/engine-javascript': 3.4.0 + '@shikijs/engine-oniguruma': 3.4.0 + '@shikijs/langs': 3.4.0 + '@shikijs/themes': 3.4.0 + '@shikijs/types': 3.4.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + sisteransi@1.0.5: {} + + smol-toml@1.3.4: {} + + sonner@2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + source-map-js@1.2.1: {} + + source-map@0.7.4: {} + + space-separated-tokens@2.0.2: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} + + statuses@2.0.1: {} + + std-env@3.9.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: {} + + style-to-js@1.1.16: + dependencies: + style-to-object: 1.0.8 + + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + + superagent@10.2.1: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.0 + fast-safe-stringify: 2.1.1 + form-data: 4.0.2 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.0 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + tailwind-merge@3.2.0: {} + + tailwindcss@4.1.5: {} + + tapable@2.2.1: {} + + tar-fs@2.1.2: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tiny-inflate@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.0.2: {} + + tinyrainbow@2.0.0: {} + + tinyspy@3.0.2: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + toidentifier@1.0.1: {} + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@0.0.3: {} + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tsconfck@3.1.5(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + tslib@2.8.1: {} + + tsx@4.19.4: + dependencies: + esbuild: 0.25.4 + get-tsconfig: 4.10.0 + optionalDependencies: + fsevents: 2.3.3 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tw-animate-css@1.2.9: {} + + type-fest@4.41.0: {} + + typescript@5.8.3: {} + + ufo@1.6.1: {} + + ultrahtml@1.6.0: {} + + uncrypto@0.1.3: {} + + undici-types@6.21.0: {} + + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.4.1: + dependencies: + css-tree: 3.1.0 + ohash: 2.0.11 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universal-user-agent@7.0.2: {} + + unstorage@1.16.0(ioredis@5.6.1): + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.5 + h3: 1.15.3 + lru-cache: 10.4.3 + node-fetch-native: 1.6.6 + ofetch: 1.4.1 + ufo: 1.6.1 + optionalDependencies: + ioredis: 5.6.1 + + update-browserslist-db@1.1.3(browserslist@4.24.5): + dependencies: + browserslist: 4.24.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@19.1.3)(react@19.1.0): + dependencies: + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.3 + + use-sidecar@1.1.3(@types/react@19.1.3)(react@19.1.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.3 + + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite-node@3.1.3(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4): + dependencies: + esbuild: 0.25.4 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.40.2 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 22.15.12 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.2 + tsx: 4.19.4 + + vitefu@1.0.6(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)): + optionalDependencies: + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + + vitest@3.1.3(@types/debug@4.1.12)(@types/node@22.15.12)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4): + dependencies: + '@vitest/expect': 3.1.3 + '@vitest/mocker': 3.1.3(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + '@vitest/pretty-format': 3.1.3 + '@vitest/runner': 3.1.3 + '@vitest/snapshot': 3.1.3 + '@vitest/spy': 3.1.3 + '@vitest/utils': 3.1.3 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.13 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + vite-node: 3.1.3(@types/node@22.15.12)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.15.12 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + web-namespaces@2.0.1: {} + + web-streams-polyfill@3.3.3: {} + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-pm-runs@1.1.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + ws@8.18.2: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + xxhash-wasm@1.1.0: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yocto-queue@1.2.1: {} + + yocto-spinner@0.2.2: + dependencies: + yoctocolors: 2.1.1 + + yoctocolors@2.1.1: {} + + zod-to-json-schema@3.24.5(zod@3.24.4): + dependencies: + zod: 3.24.4 + + zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.24.4): + dependencies: + typescript: 5.8.3 + zod: 3.24.4 + + zod@3.24.4: {} + + zwitch@2.0.4: {} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/scripts/README-docker.md b/scripts/README-docker.md new file mode 100644 index 0000000..952a4a8 --- /dev/null +++ b/scripts/README-docker.md @@ -0,0 +1,78 @@ +# Scripts Directory + +This directory contains utility scripts for the gitea-mirror project. + +## Docker Build Script + +### build-docker.sh + +This script simplifies the process of building and publishing multi-architecture Docker images for the gitea-mirror project. + +#### Usage + +```bash +./build-docker.sh [--load] [--push] +``` + +Options: +- `--load`: Load the built image into the local Docker daemon +- `--push`: Push the image to the configured Docker registry + +Without any flags, the script will build the image but leave it in the build cache only. + +#### Configuration + +The script uses environment variables from the `.env` file in the project root: + +- `DOCKER_REGISTRY`: The Docker registry to push to (default: ghcr.io) +- `DOCKER_IMAGE`: The image name (default: gitea-mirror) +- `DOCKER_TAG`: The image tag (default: latest) + +#### Examples + +1. Build for multiple architectures and load into Docker: + ```bash + ./scripts/build-docker.sh --load + ``` + +2. Build and push to the registry: + ```bash + ./scripts/build-docker.sh --push + ``` + +3. Using with docker-compose: + ```bash + # Ensure dependencies are installed and database is initialized + pnpm setup + + # First build the image + ./scripts/build-docker.sh --load + + # Then run using docker-compose for development + docker-compose -f ../docker-compose.dev.yml up -d + + # Or for production + docker-compose --profile production up -d + ``` + +## Diagnostics Script + +### docker-diagnostics.sh + +This utility script helps diagnose issues with your Docker setup for building and running Gitea Mirror. + +#### Usage + +```bash +./scripts/docker-diagnostics.sh +``` + +The script checks: +- Docker and Docker Compose installation +- Docker Buildx configuration +- QEMU availability for multi-architecture builds +- Docker resources (memory, CPU) +- Environment configuration +- Provides recommendations for building and troubleshooting + +Run this script before building if you're experiencing issues with Docker builds or want to validate your environment. diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..94e3f12 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,58 @@ +# Scripts Directory + +This folder contains utility scripts for database management. + +## Database Management Tool (manage-db.ts) + +This is a consolidated database management tool that handles all database-related operations. It combines the functionality of the previous separate scripts into a single, more intelligent script that can check, fix, and initialize the database as needed. + +### Features + +- **Check Mode**: Validates the existence and integrity of the database +- **Init Mode**: Creates the database only if it doesn't already exist +- **Fix Mode**: Corrects database file location issues +- **Reset Users Mode**: Removes all users and their data +- **Auto Mode**: Automatically checks, fixes, and initializes the database if needed + +## Running the Database Management Tool + +You can execute the database management tool using your package manager with various commands: + +```bash +# Checks database status (default action if no command is specified, equivalent to 'pnpm check-db') +pnpm manage-db + +# Check database status +pnpm check-db + +# Initialize the database (only if it doesn't exist) +pnpm init-db + +# Fix database location issues +pnpm fix-db + +# Automatic check, fix, and initialize if needed +pnpm db-auto + +# Reset all users (for testing signup flow) +pnpm reset-users + +# Update the database schema to the latest version +pnpm update-schema + +# Remove database files completely +pnpm cleanup-db + +# Complete setup (install dependencies and initialize database) +pnpm setup + +# Start development server with a fresh database +pnpm dev:clean + +# Start production server with a fresh database +pnpm start:fresh +``` + +## Database File Location + +The database file should be located in the `./data/gitea-mirror.db` directory. If the file is found in the root directory, the fix mode will move it to the correct location. diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh new file mode 100755 index 0000000..26e0061 --- /dev/null +++ b/scripts/build-docker.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Build and push the Gitea Mirror docker image for multiple architectures + +set -e # Exit on any error + +# Load environment variables if .env file exists +if [ -f .env ]; then + echo "Loading environment variables from .env" + export $(grep -v '^#' .env | xargs) +fi + +# Set default values if not set in environment +DOCKER_REGISTRY=${DOCKER_REGISTRY:-ghcr.io} +DOCKER_IMAGE=${DOCKER_IMAGE:-gitea-mirror} +DOCKER_TAG=${DOCKER_TAG:-latest} + +FULL_IMAGE_NAME="$DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG" +echo "Building image: $FULL_IMAGE_NAME" + +# Parse command line arguments +LOAD=false +PUSH=false + +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + --load) + LOAD=true + shift + ;; + --push) + PUSH=true + shift + ;; + *) + echo "Unknown option: $key" + echo "Usage: $0 [--load] [--push]" + echo " --load Load the image into Docker after build" + echo " --push Push the image to the registry after build" + exit 1 + ;; + esac +done + +# Build command construction +BUILD_CMD="docker buildx build --platform linux/amd64,linux/arm64 -t $FULL_IMAGE_NAME" + +# Add load or push flag if specified +if [ "$LOAD" = true ]; then + BUILD_CMD="$BUILD_CMD --load" +fi + +if [ "$PUSH" = true ]; then + BUILD_CMD="$BUILD_CMD --push" +fi + +# Add context directory +BUILD_CMD="$BUILD_CMD ." + +# Execute the build command +echo "Executing: $BUILD_CMD" + +# Function to execute with retries +execute_with_retry() { + local cmd="$1" + local max_attempts=${2:-3} + local attempt=1 + local delay=5 + + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts..." + if eval "$cmd"; then + echo "Command succeeded!" + return 0 + else + echo "Command failed, waiting $delay seconds before retry..." + sleep $delay + attempt=$((attempt + 1)) + delay=$((delay * 2)) # Exponential backoff + fi + done + + echo "All attempts failed!" + return 1 +} + +# Execute with retry +execute_with_retry "$BUILD_CMD" +BUILD_RESULT=$? + +if [ $BUILD_RESULT -eq 0 ]; then + echo "✅ Build successful!" +else + echo "❌ Build failed after multiple attempts." + exit 1 +fi + +# Print help message if neither --load nor --push was specified +if [ "$LOAD" = false ] && [ "$PUSH" = false ]; then + echo + echo "NOTE: Image was built but not loaded or pushed. To use this image, run again with:" + echo " $0 --load # to load into local Docker" + echo " $0 --push # to push to registry $DOCKER_REGISTRY" +fi diff --git a/scripts/docker-diagnostics.sh b/scripts/docker-diagnostics.sh new file mode 100755 index 0000000..77ca842 --- /dev/null +++ b/scripts/docker-diagnostics.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Docker setup diagnostics tool for Gitea Mirror + +# ANSI color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=====================================================${NC}" +echo -e "${BLUE} Gitea Mirror Docker Setup Diagnostics ${NC}" +echo -e "${BLUE}=====================================================${NC}" + +# Check if Docker is installed and running +echo -e "\n${YELLOW}Checking Docker...${NC}" +if command -v docker &> /dev/null; then + echo -e "${GREEN}✓ Docker is installed${NC}" + if docker info &> /dev/null; then + echo -e "${GREEN}✓ Docker daemon is running${NC}" + + # Get Docker version + DOCKER_VERSION=$(docker version --format '{{.Server.Version}}') + echo -e "${GREEN}✓ Docker version: $DOCKER_VERSION${NC}" + else + echo -e "${RED}✗ Docker daemon is not running${NC}" + echo -e " Run: ${YELLOW}open -a Docker${NC}" + fi +else + echo -e "${RED}✗ Docker is not installed${NC}" + echo -e " Visit: ${BLUE}https://www.docker.com/products/docker-desktop${NC}" +fi + +# Check for Docker Compose +echo -e "\n${YELLOW}Checking Docker Compose...${NC}" +if docker compose version &> /dev/null; then + COMPOSE_VERSION=$(docker compose version --short) + echo -e "${GREEN}✓ Docker Compose is installed (v$COMPOSE_VERSION)${NC}" +elif command -v docker-compose &> /dev/null; then + COMPOSE_VERSION=$(docker-compose --version | awk '{print $3}' | sed 's/,//') + echo -e "${GREEN}✓ Docker Compose is installed (v$COMPOSE_VERSION)${NC}" + echo -e "${YELLOW}⚠ Using legacy docker-compose - consider upgrading${NC}" +else + echo -e "${RED}✗ Docker Compose is not installed${NC}" +fi + +# Check for Docker Buildx +echo -e "\n${YELLOW}Checking Docker Buildx...${NC}" +if docker buildx version &> /dev/null; then + BUILDX_VERSION=$(docker buildx version | head -n1 | awk '{print $2}') + echo -e "${GREEN}✓ Docker Buildx is installed (v$BUILDX_VERSION)${NC}" + + # List available builders + echo -e "\n${YELLOW}Available builders:${NC}" + docker buildx ls +else + echo -e "${RED}✗ Docker Buildx is not installed or not activated${NC}" +fi + +# Check for QEMU +echo -e "\n${YELLOW}Checking QEMU for multi-platform builds...${NC}" +if docker run --rm --privileged multiarch/qemu-user-static --reset -p yes &> /dev/null; then + echo -e "${GREEN}✓ QEMU is available for multi-architecture builds${NC}" +else + echo -e "${RED}✗ QEMU setup issue - multi-platform builds may fail${NC}" + echo -e " Run: ${YELLOW}docker run --rm --privileged multiarch/qemu-user-static --reset -p yes${NC}" +fi + +# Check Docker resources +echo -e "\n${YELLOW}Checking Docker resources...${NC}" +if [ "$(uname)" == "Darwin" ]; then + # macOS + if command -v osascript &> /dev/null; then + SYS_MEM=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )) + echo -e "System memory: ${GREEN}$SYS_MEM GB${NC}" + echo -e "NOTE: Check Docker Desktop settings to see allocated resources" + echo -e "Recommended: At least 4GB RAM and 2 CPUs for multi-platform builds" + fi +fi + +# Check environment file +echo -e "\n${YELLOW}Checking environment configuration...${NC}" +if [ -f .env ]; then + echo -e "${GREEN}✓ .env file exists${NC}" + + # Parse .env file safely + if [ -f .env ]; then + REGISTRY=$(grep DOCKER_REGISTRY .env | cut -d= -f2) + IMAGE=$(grep DOCKER_IMAGE .env | cut -d= -f2) + TAG=$(grep DOCKER_TAG .env | cut -d= -f2) + + echo -e "Docker image configuration:" + echo -e " Registry: ${BLUE}${REGISTRY:-"Not set (will use default)"}${NC}" + echo -e " Image: ${BLUE}${IMAGE:-"Not set (will use default)"}${NC}" + echo -e " Tag: ${BLUE}${TAG:-"Not set (will use default)"}${NC}" + fi +else + echo -e "${YELLOW}⚠ .env file not found${NC}" + echo -e " Run: ${YELLOW}cp .env.example .env${NC}" +fi + +# Conclusion and recommendations +echo -e "\n${BLUE}=====================================================${NC}" +echo -e "${BLUE} Recommendations ${NC}" +echo -e "${BLUE}=====================================================${NC}" + +echo -e "\n${YELLOW}For local development:${NC}" +echo -e "1. ${GREEN}pnpm setup${NC} (initialize database and install dependencies)" +echo -e "2. ${GREEN}./scripts/build-docker.sh --load${NC} (build and load into Docker)" +echo -e "3. ${GREEN}docker-compose -f docker-compose.dev.yml up -d${NC} (start the development container)" + +echo -e "\n${YELLOW}For production deployment (using Docker Compose):${NC}" +echo -e "1. ${GREEN}pnpm setup${NC} (if not already done, to ensure database schema is ready)" +echo -e "2. ${GREEN}docker-compose --profile production up -d${NC} (start the production container)" + +echo -e "\n${YELLOW}For CI/CD builds:${NC}" +echo -e "1. Use GitHub Actions workflow with retry mechanism" +echo -e "2. If build fails, try running with: ${GREEN}DOCKER_BUILDKIT=1${NC}" +echo -e "3. Consider breaking the build into multiple steps for better reliability" + +echo -e "\n${YELLOW}For troubleshooting:${NC}" +echo -e "1. Check container logs: ${GREEN}docker logs gitea-mirror-dev${NC} (for development) or ${GREEN}docker logs gitea-mirror${NC} (for production)" +echo -e "2. Check health status: ${GREEN}docker inspect --format='{{.State.Health.Status}}' gitea-mirror-dev${NC} (for development) or ${GREEN}docker inspect --format='{{.State.Health.Status}}' gitea-mirror${NC} (for production)" +echo -e "3. See full documentation: ${BLUE}.github/workflows/TROUBLESHOOTING.md${NC}" +echo -e "" diff --git a/scripts/manage-db.ts b/scripts/manage-db.ts new file mode 100644 index 0000000..4d2b8b9 --- /dev/null +++ b/scripts/manage-db.ts @@ -0,0 +1,803 @@ +import fs from "fs"; +import path from "path"; +import { client, db } from "../src/lib/db"; +import { configs } from "../src/lib/db"; +import { v4 as uuidv4 } from "uuid"; + +// Command line arguments +const args = process.argv.slice(2); +const command = args[0] || "check"; + +// Ensure data directory exists +const dataDir = path.join(process.cwd(), "data"); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +// Database paths +const rootDbFile = path.join(process.cwd(), "gitea-mirror.db"); +const rootDevDbFile = path.join(process.cwd(), "gitea-mirror-dev.db"); +const dataDbFile = path.join(dataDir, "gitea-mirror.db"); +const dataDevDbFile = path.join(dataDir, "gitea-mirror-dev.db"); + +// Database path - ensure we use absolute path +const dbPath = + process.env.DATABASE_URL || `file:${path.join(dataDir, "gitea-mirror.db")}`; + +/** + * Ensure all required tables exist + */ +async function ensureTablesExist() { + const requiredTables = [ + "users", + "configs", + "repositories", + "organizations", + "mirror_jobs", + ]; + + for (const table of requiredTables) { + try { + await client.execute(`SELECT 1 FROM ${table} LIMIT 1`); + } catch (error) { + if (error instanceof Error && error.message.includes("SQLITE_ERROR")) { + console.warn(`âš ī¸ Table '${table}' is missing. Creating it now...`); + switch (table) { + case "users": + await client.execute( + `CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL, + email TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )` + ); + break; + case "configs": + await client.execute( + `CREATE TABLE configs ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1, + github_config TEXT NOT NULL, + gitea_config TEXT NOT NULL, + include TEXT NOT NULL DEFAULT '[]', + exclude TEXT NOT NULL DEFAULT '[]', + schedule_config TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + FOREIGN KEY (user_id) REFERENCES users(id) + )` + ); + break; + case "repositories": + await client.execute( + `CREATE TABLE repositories ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + config_id TEXT NOT NULL, + name TEXT NOT NULL, + full_name TEXT NOT NULL, + url TEXT NOT NULL, + clone_url TEXT NOT NULL, + owner TEXT NOT NULL, + organization TEXT, + is_private INTEGER NOT NULL DEFAULT 0, + is_fork INTEGER NOT NULL DEFAULT 0, + forked_from TEXT, + has_issues INTEGER NOT NULL DEFAULT 0, + is_starred INTEGER NOT NULL DEFAULT 0, + is_archived INTEGER NOT NULL DEFAULT 0, + size INTEGER NOT NULL DEFAULT 0, + has_lfs INTEGER NOT NULL DEFAULT 0, + has_submodules INTEGER NOT NULL DEFAULT 0, + default_branch TEXT NOT NULL, + visibility TEXT NOT NULL DEFAULT 'public', + status TEXT NOT NULL DEFAULT 'imported', + last_mirrored INTEGER, + error_message TEXT, + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (config_id) REFERENCES configs(id) + )` + ); + break; + case "organizations": + await client.execute( + `CREATE TABLE organizations ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + config_id TEXT NOT NULL, + name TEXT NOT NULL, + avatar_url TEXT NOT NULL, + membership_role TEXT NOT NULL DEFAULT 'member', + is_included INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'imported', + last_mirrored INTEGER, + error_message TEXT, + repository_count INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (config_id) REFERENCES configs(id) + )` + ); + break; + case "mirror_jobs": + await client.execute( + `CREATE TABLE mirror_jobs ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + repository_id TEXT, + repository_name TEXT, + organization_id TEXT, + organization_name TEXT, + details TEXT, + status TEXT NOT NULL DEFAULT 'imported', + message TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + )` + ); + break; + } + console.log(`✅ Table '${table}' created successfully.`); + } else { + console.error(`❌ Error checking table '${table}':`, error); + process.exit(1); + } + } + } +} + +/** + * Check database status + */ +async function checkDatabase() { + console.log("Checking database status..."); + + // Check for database files in the root directory (which is incorrect) + if (fs.existsSync(rootDbFile)) { + console.warn( + "âš ī¸ WARNING: Database file found in root directory: gitea-mirror.db" + ); + console.warn(" This file should be in the data directory."); + console.warn( + ' Run "pnpm manage-db fix" to fix this issue or "pnpm cleanup-db" to remove it.' + ); + } + + // Check if database files exist in the data directory (which is correct) + if (fs.existsSync(dataDbFile)) { + console.log( + "✅ Database file found in data directory: data/gitea-mirror.db" + ); + + // Check for users + try { + const userCountResult = await client.execute( + `SELECT COUNT(*) as count FROM users` + ); + const userCount = userCountResult.rows[0].count; + + if (userCount === 0) { + console.log("â„šī¸ No users found in the database."); + console.log( + " When you start the application, you will be directed to the signup page" + ); + console.log(" to create an initial admin account."); + } else { + console.log(`✅ ${userCount} user(s) found in the database.`); + console.log(" The application will show the login page on startup."); + } + + // Check for configurations + const configCountResult = await client.execute( + `SELECT COUNT(*) as count FROM configs` + ); + const configCount = configCountResult.rows[0].count; + + if (configCount === 0) { + console.log("â„šī¸ No configurations found in the database."); + console.log( + " You will need to set up your GitHub and Gitea configurations after login." + ); + } else { + console.log( + `✅ ${configCount} configuration(s) found in the database.` + ); + } + } catch (error) { + console.error("❌ Error connecting to the database:", error); + console.warn( + ' The database file might be corrupted. Consider running "pnpm manage-db init" to recreate it.' + ); + } + } else { + console.warn("âš ī¸ WARNING: Database file not found in data directory."); + console.warn(' Run "pnpm manage-db init" to create it.'); + } +} + +/** + * Update database schema + */ +async function updateSchema() { + console.log(`Checking and updating database schema at ${dbPath}...`); + + // Check if the database exists + if (!fs.existsSync(dataDbFile)) { + console.log( + "âš ī¸ Database file doesn't exist. Run 'pnpm manage-db init' first to create it." + ); + return; + } + + try { + console.log("Checking for missing columns in mirror_jobs table..."); + + // Check if repository_id column exists in mirror_jobs table + const tableInfoResult = await client.execute( + `PRAGMA table_info(mirror_jobs)` + ); + + // Get column names + const columns = tableInfoResult.rows.map((row) => row.name); + + // Check for repository_id column + if (!columns.includes("repository_id")) { + console.log( + "Adding missing repository_id column to mirror_jobs table..." + ); + await client.execute( + `ALTER TABLE mirror_jobs ADD COLUMN repository_id TEXT;` + ); + console.log("✅ Added repository_id column to mirror_jobs table."); + } + + // Check for repository_name column + if (!columns.includes("repository_name")) { + console.log( + "Adding missing repository_name column to mirror_jobs table..." + ); + await client.execute( + `ALTER TABLE mirror_jobs ADD COLUMN repository_name TEXT;` + ); + console.log("✅ Added repository_name column to mirror_jobs table."); + } + + // Check for organization_id column + if (!columns.includes("organization_id")) { + console.log( + "Adding missing organization_id column to mirror_jobs table..." + ); + await client.execute( + `ALTER TABLE mirror_jobs ADD COLUMN organization_id TEXT;` + ); + console.log("✅ Added organization_id column to mirror_jobs table."); + } + + // Check for organization_name column + if (!columns.includes("organization_name")) { + console.log( + "Adding missing organization_name column to mirror_jobs table..." + ); + await client.execute( + `ALTER TABLE mirror_jobs ADD COLUMN organization_name TEXT;` + ); + console.log("✅ Added organization_name column to mirror_jobs table."); + } + + // Check for details column + if (!columns.includes("details")) { + console.log("Adding missing details column to mirror_jobs table..."); + await client.execute(`ALTER TABLE mirror_jobs ADD COLUMN details TEXT;`); + console.log("✅ Added details column to mirror_jobs table."); + } + + // Check for mirrored_location column in repositories table + const repoColumns = await client.execute( + `PRAGMA table_info(repositories)` + ); + const repoColumnNames = repoColumns.rows.map((row: any) => row.name); + + if (!repoColumnNames.includes("mirrored_location")) { + console.log("Adding missing mirrored_location column to repositories table..."); + await client.execute( + `ALTER TABLE repositories ADD COLUMN mirrored_location TEXT DEFAULT '';` + ); + console.log("✅ Added mirrored_location column to repositories table."); + } + + console.log("✅ Schema update completed successfully."); + } catch (error) { + console.error("❌ Error updating schema:", error); + process.exit(1); + } +} + +/** + * Initialize the database + */ +async function initializeDatabase() { + // Check if database already exists first + if (fs.existsSync(dataDbFile)) { + console.log("âš ī¸ Database already exists at data/gitea-mirror.db"); + console.log( + ' If you want to recreate the database, run "pnpm cleanup-db" first.' + ); + console.log( + ' Or use "pnpm manage-db reset-users" to just remove users without recreating tables.' + ); + + // Check if we can connect to it + try { + await client.execute(`SELECT COUNT(*) as count FROM users`); + console.log("✅ Database is valid and accessible."); + return; + } catch (error) { + console.error("❌ Error connecting to the existing database:", error); + console.log( + " The database might be corrupted. Proceeding with reinitialization..." + ); + } + } + + console.log(`Initializing database at ${dbPath}...`); + + try { + // Create tables if they don't exist + await client.execute( + `CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL, + email TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )` + ); + + // NOTE: We no longer create a default admin user - user will create one via signup page + + await client.execute( + `CREATE TABLE IF NOT EXISTS configs ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1, + github_config TEXT NOT NULL, + gitea_config TEXT NOT NULL, + include TEXT NOT NULL DEFAULT '["*"]', + exclude TEXT NOT NULL DEFAULT '[]', + schedule_config TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + FOREIGN KEY (user_id) REFERENCES users(id) +); +` + ); + + await client.execute( + `CREATE TABLE IF NOT EXISTS repositories ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + config_id TEXT NOT NULL, + name TEXT NOT NULL, + full_name TEXT NOT NULL, + url TEXT NOT NULL, + clone_url TEXT NOT NULL, + owner TEXT NOT NULL, + organization TEXT, + mirrored_location TEXT DEFAULT '', + + is_private INTEGER NOT NULL DEFAULT 0, + is_fork INTEGER NOT NULL DEFAULT 0, + forked_from TEXT, + + has_issues INTEGER NOT NULL DEFAULT 0, + is_starred INTEGER NOT NULL DEFAULT 0, + is_archived INTEGER NOT NULL DEFAULT 0, + + size INTEGER NOT NULL DEFAULT 0, + has_lfs INTEGER NOT NULL DEFAULT 0, + has_submodules INTEGER NOT NULL DEFAULT 0, + + default_branch TEXT NOT NULL, + visibility TEXT NOT NULL DEFAULT 'public', + + status TEXT NOT NULL DEFAULT 'imported', + last_mirrored INTEGER, + error_message TEXT, + + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (config_id) REFERENCES configs(id) +); +` + ); + + await client.execute( + `CREATE TABLE IF NOT EXISTS organizations ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + config_id TEXT NOT NULL, + name TEXT NOT NULL, + + avatar_url TEXT NOT NULL, + membership_role TEXT NOT NULL DEFAULT 'member', + + is_included INTEGER NOT NULL DEFAULT 1, + + status TEXT NOT NULL DEFAULT 'imported', + last_mirrored INTEGER, + error_message TEXT, + + repository_count INTEGER NOT NULL DEFAULT 0, + + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), + + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (config_id) REFERENCES configs(id) +); +` + ); + + await client.execute( + `CREATE TABLE IF NOT EXISTS mirror_jobs ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + repository_id TEXT, + repository_name TEXT, + organization_id TEXT, + organization_name TEXT, + details TEXT, + status TEXT NOT NULL DEFAULT 'imported', + message TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); +` + ); + + // Insert default config if none exists + const configCountResult = await client.execute( + `SELECT COUNT(*) as count FROM configs` + ); + const configCount = configCountResult.rows[0].count; + if (configCount === 0) { + // Get the first user + const firstUserResult = await client.execute( + `SELECT id FROM users LIMIT 1` + ); + if (firstUserResult.rows.length > 0) { + const userId = firstUserResult.rows[0].id; + const configId = uuidv4(); + const githubConfig = JSON.stringify({ + username: process.env.GITHUB_USERNAME || "", + token: process.env.GITHUB_TOKEN || "", + skipForks: false, + privateRepositories: false, + mirrorIssues: false, + mirrorStarred: true, + useSpecificUser: false, + preserveOrgStructure: true, + skipStarredIssues: false, + }); + const giteaConfig = JSON.stringify({ + url: process.env.GITEA_URL || "", + token: process.env.GITEA_TOKEN || "", + username: process.env.GITEA_USERNAME || "", + organization: "", + visibility: "public", + starredReposOrg: "github", + }); + const include = JSON.stringify(["*"]); + const exclude = JSON.stringify([]); + const scheduleConfig = JSON.stringify({ + enabled: false, + interval: 3600, + lastRun: null, + nextRun: null, + }); + + await client.execute( + ` + INSERT INTO configs (id, user_id, name, is_active, github_config, gitea_config, include, exclude, schedule_config, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + [ + configId, + userId, + "Default Configuration", + 1, + githubConfig, + giteaConfig, + include, + exclude, + scheduleConfig, + Date.now(), + Date.now(), + ] + ); + } + } + + console.log("✅ Database initialization completed successfully."); + } catch (error) { + console.error("❌ Error initializing database:", error); + process.exit(1); + } +} + +/** + * Reset users in the database + */ +async function resetUsers() { + console.log(`Resetting users in database at ${dbPath}...`); + + try { + // Check if the database exists + const dbFilePath = dbPath.replace("file:", ""); + const doesDbExist = fs.existsSync(dbFilePath); + + if (!doesDbExist) { + console.log( + "❌ Database file doesn't exist. Run 'pnpm manage-db init' first to create it." + ); + return; + } + + // Count existing users + const userCountResult = await client.execute( + `SELECT COUNT(*) as count FROM users` + ); + const userCount = userCountResult.rows[0].count; + + if (userCount === 0) { + console.log("â„šī¸ No users found in the database. Nothing to reset."); + return; + } + + // Delete all users + await client.execute(`DELETE FROM users`); + console.log(`✅ Deleted ${userCount} users from the database.`); + + // Check dependent configurations that need to be removed + const configCount = await client.execute( + `SELECT COUNT(*) as count FROM configs` + ); + + if ( + configCount.rows && + configCount.rows[0] && + Number(configCount.rows[0].count) > 0 + ) { + await client.execute(`DELETE FROM configs`); + console.log(`✅ Deleted ${configCount.rows[0].count} configurations.`); + } + + // Check for dependent repositories + const repoCount = await client.execute( + `SELECT COUNT(*) as count FROM repositories` + ); + + if ( + repoCount.rows && + repoCount.rows[0] && + Number(repoCount.rows[0].count) > 0 + ) { + await client.execute(`DELETE FROM repositories`); + console.log(`✅ Deleted ${repoCount.rows[0].count} repositories.`); + } + + // Check for dependent organizations + const orgCount = await client.execute( + `SELECT COUNT(*) as count FROM organizations` + ); + + if ( + orgCount.rows && + orgCount.rows[0] && + Number(orgCount.rows[0].count) > 0 + ) { + await client.execute(`DELETE FROM organizations`); + console.log(`✅ Deleted ${orgCount.rows[0].count} organizations.`); + } + + // Check for dependent mirror jobs + const jobCount = await client.execute( + `SELECT COUNT(*) as count FROM mirror_jobs` + ); + + if ( + jobCount.rows && + jobCount.rows[0] && + Number(jobCount.rows[0].count) > 0 + ) { + await client.execute(`DELETE FROM mirror_jobs`); + console.log(`✅ Deleted ${jobCount.rows[0].count} mirror jobs.`); + } + + console.log( + "✅ Database has been reset. The application will now prompt for a new admin account setup on next run." + ); + } catch (error) { + console.error("❌ Error resetting users:", error); + process.exit(1); + } +} + +/** + * Fix database location issues + */ +async function fixDatabaseIssues() { + console.log("Checking for database issues..."); + + // Check for database files in the root directory + if (fs.existsSync(rootDbFile)) { + console.log("Found database file in root directory: gitea-mirror.db"); + + // If the data directory doesn't have the file, move it there + if (!fs.existsSync(dataDbFile)) { + console.log("Moving database file to data directory..."); + fs.copyFileSync(rootDbFile, dataDbFile); + console.log("Database file moved successfully."); + } else { + console.log( + "Database file already exists in data directory. Checking for differences..." + ); + + // Compare file sizes to see which is newer/larger + const rootStats = fs.statSync(rootDbFile); + const dataStats = fs.statSync(dataDbFile); + + if ( + rootStats.size > dataStats.size || + rootStats.mtime > dataStats.mtime + ) { + console.log( + "Root database file is newer or larger. Backing up data directory file and replacing it..." + ); + fs.copyFileSync(dataDbFile, `${dataDbFile}.backup-${Date.now()}`); + fs.copyFileSync(rootDbFile, dataDbFile); + console.log("Database file replaced successfully."); + } + } + + // Remove the root file + console.log("Removing database file from root directory..."); + fs.unlinkSync(rootDbFile); + console.log("Root database file removed."); + } + + // Do the same for dev database + if (fs.existsSync(rootDevDbFile)) { + console.log( + "Found development database file in root directory: gitea-mirror-dev.db" + ); + + // If the data directory doesn't have the file, move it there + if (!fs.existsSync(dataDevDbFile)) { + console.log("Moving development database file to data directory..."); + fs.copyFileSync(rootDevDbFile, dataDevDbFile); + console.log("Development database file moved successfully."); + } else { + console.log( + "Development database file already exists in data directory. Checking for differences..." + ); + + // Compare file sizes to see which is newer/larger + const rootStats = fs.statSync(rootDevDbFile); + const dataStats = fs.statSync(dataDevDbFile); + + if ( + rootStats.size > dataStats.size || + rootStats.mtime > dataStats.mtime + ) { + console.log( + "Root development database file is newer or larger. Backing up data directory file and replacing it..." + ); + fs.copyFileSync(dataDevDbFile, `${dataDevDbFile}.backup-${Date.now()}`); + fs.copyFileSync(rootDevDbFile, dataDevDbFile); + console.log("Development database file replaced successfully."); + } + } + + // Remove the root file + console.log("Removing development database file from root directory..."); + fs.unlinkSync(rootDevDbFile); + console.log("Root development database file removed."); + } + + // Check if database files exist in the data directory + if (!fs.existsSync(dataDbFile)) { + console.warn( + "âš ī¸ WARNING: Production database file not found in data directory." + ); + console.warn(' Run "pnpm manage-db init" to create it.'); + } else { + console.log("✅ Production database file found in data directory."); + + // Check if we can connect to the database + try { + // Try to query the database + const configCount = await db.select().from(configs).limit(1); + console.log(`✅ Successfully connected to the database.`); + } catch (error) { + console.error("❌ Error connecting to the database:", error); + console.warn( + ' The database file might be corrupted. Consider running "pnpm manage-db init" to recreate it.' + ); + } + } + + console.log("Database check completed."); +} + +/** + * Main function to handle the command + */ +async function main() { + console.log(`Database Management Tool for Gitea Mirror`); + + // Ensure all required tables exist + console.log("Ensuring all required tables exist..."); + await ensureTablesExist(); + + switch (command) { + case "check": + await checkDatabase(); + break; + case "init": + await initializeDatabase(); + break; + case "fix": + await fixDatabaseIssues(); + break; + case "reset-users": + await resetUsers(); + break; + case "update-schema": + await updateSchema(); + break; + case "auto": + // Auto mode: check, fix, and initialize if needed + console.log("Running in auto mode: check, fix, and initialize if needed"); + await fixDatabaseIssues(); + + // Also update schema in auto mode + await updateSchema(); + + if (!fs.existsSync(dataDbFile)) { + await initializeDatabase(); + } else { + await checkDatabase(); + } + break; + default: + console.log(` +Available commands: + check - Check database status + init - Initialize the database (only if it doesn't exist) + fix - Fix database location issues + reset-users - Remove all users and their data + update-schema - Update the database schema to the latest version + auto - Automatic mode: check, fix, and initialize if needed + +Usage: pnpm manage-db [command] +`); + } +} + +main().catch((error) => { + console.error("Error during database management:", error); + process.exit(1); +}); diff --git a/scripts/run-migrations.ts b/scripts/run-migrations.ts new file mode 100644 index 0000000..722b7e7 --- /dev/null +++ b/scripts/run-migrations.ts @@ -0,0 +1,18 @@ +import { addMirroredLocationColumn } from "../src/lib/db/migrations/add-mirrored-location"; + +async function runMigrations() { + try { + console.log("Running database migrations..."); + + // Run the migration to add the mirrored_location column + await addMirroredLocationColumn(); + + console.log("All migrations completed successfully"); + process.exit(0); + } catch (error) { + console.error("Migration failed:", error); + process.exit(1); + } +} + +runMigrations(); diff --git a/src/components/activity/ActivityList.tsx b/src/components/activity/ActivityList.tsx new file mode 100644 index 0000000..75583e8 --- /dev/null +++ b/src/components/activity/ActivityList.tsx @@ -0,0 +1,202 @@ +import { useMemo, useRef, useState, useEffect } from "react"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import type { MirrorJob } from "@/lib/db/schema"; +import Fuse from "fuse.js"; +import { Button } from "../ui/button"; +import { RefreshCw } from "lucide-react"; +import { Card } from "../ui/card"; +import { formatDate, getStatusColor } from "@/lib/utils"; +import { Skeleton } from "../ui/skeleton"; +import type { FilterParams } from "@/types/filter"; + +interface ActivityListProps { + activities: MirrorJob[]; + isLoading: boolean; + filter: FilterParams; + setFilter: (filter: FilterParams) => void; +} + +export default function ActivityList({ + activities, + isLoading, + filter, + setFilter, +}: ActivityListProps) { + const [expandedItems, setExpandedItems] = useState>(new Set()); + const parentRef = useRef(null); + const rowRefs = useRef>(new Map()); + + const filteredActivities = useMemo(() => { + let result = activities; + + if (filter.status) { + result = result.filter((activity) => activity.status === filter.status); + } + + if (filter.type) { + if (filter.type === 'repository') { + result = result.filter((activity) => !!activity.repositoryId); + } else if (filter.type === 'organization') { + result = result.filter((activity) => !!activity.organizationId); + } + } + + if (filter.name) { + result = result.filter((activity) => + activity.repositoryName === filter.name || + activity.organizationName === filter.name + ); + } + + if (filter.searchTerm) { + const fuse = new Fuse(result, { + keys: ["message", "details", "organizationName", "repositoryName"], + threshold: 0.3, + }); + result = fuse.search(filter.searchTerm).map((res) => res.item); + } + + return result; + }, [activities, filter]); + + const virtualizer = useVirtualizer({ + count: filteredActivities.length, + getScrollElement: () => parentRef.current, + estimateSize: (index) => { + const activity = filteredActivities[index]; + return expandedItems.has(activity.id || "") ? 217 : 120; + }, + overscan: 5, + measureElement: (el) => el.getBoundingClientRect().height + 8, + }); + + useEffect(() => { + virtualizer.measure(); + }, [expandedItems, virtualizer]); + + return isLoading ? ( +
+ {Array.from({ length: 5 }, (_, index) => ( + + ))} +
+ ) : filteredActivities.length === 0 ? ( +
+ +

No activities found

+

+ {filter.searchTerm || filter.status || filter.type || filter.name + ? "Try adjusting your search or filter criteria." + : "No mirroring activities have been recorded yet."} +

+ {filter.searchTerm || filter.status || filter.type || filter.name ? ( + + ) : ( + + )} +
+ ) : ( + +
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const activity = filteredActivities[virtualRow.index]; + const isExpanded = expandedItems.has(activity.id || ""); + const key = activity.id || String(virtualRow.index); + + return ( +
{ + if (node) { + rowRefs.current.set(key, node); + virtualizer.measureElement(node); + } + }} + style={{ + position: "absolute", + top: 0, + left: 0, + width: "100%", + transform: `translateY(${virtualRow.start}px)`, + paddingBottom: "8px", + }} + className="border-b px-4 pt-4" + > +
+
+
+
+
+
+

{activity.message}

+

+ {formatDate(activity.timestamp)} +

+
+ + {activity.repositoryName && ( +

+ Repository: {activity.repositoryName} +

+ )} + + {activity.organizationName && ( +

+ Organization: {activity.organizationName} +

+ )} + + {activity.details && ( +
+ + + {isExpanded && ( +
+                          {activity.details}
+                        
+ )} +
+ )} +
+
+
+ ); + })} +
+ + ); +} diff --git a/src/components/activity/ActivityLog.tsx b/src/components/activity/ActivityLog.tsx new file mode 100644 index 0000000..af77839 --- /dev/null +++ b/src/components/activity/ActivityLog.tsx @@ -0,0 +1,313 @@ +import { useCallback, useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Search, Download, RefreshCw, ChevronDown } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { apiRequest, formatDate } from "@/lib/utils"; +import { useAuth } from "@/hooks/useAuth"; +import type { MirrorJob } from "@/lib/db/schema"; +import type { ActivityApiResponse } from "@/types/activities"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { repoStatusEnum, type RepoStatus } from "@/types/Repository"; +import ActivityList from "./ActivityList"; +import { ActivityNameCombobox } from "./ActivityNameCombobox"; +import { useSSE } from "@/hooks/useSEE"; +import { useFilterParams } from "@/hooks/useFilterParams"; +import { toast } from "sonner"; + +export function ActivityLog() { + const { user } = useAuth(); + const [activities, setActivities] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const { filter, setFilter } = useFilterParams({ + searchTerm: "", + status: "", + type: "", + name: "", + }); + + const handleNewMessage = useCallback((data: MirrorJob) => { + setActivities((prevActivities) => [data, ...prevActivities]); + + console.log("Received new log:", data); + }, []); + + // Use the SSE hook + const { connected } = useSSE({ + userId: user?.id, + onMessage: handleNewMessage, + }); + + const fetchActivities = useCallback(async () => { + if (!user) return false; + + try { + setIsLoading(true); + + const response = await apiRequest( + `/activities?userId=${user.id}`, + { + method: "GET", + } + ); + + if (response.success) { + setActivities(response.activities); + return true; + } else { + toast.error(response.message || "Failed to fetch activities."); + return false; + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to fetch activities." + ); + return false; + } finally { + setIsLoading(false); + } + }, [user]); + + useEffect(() => { + fetchActivities(); + }, [fetchActivities]); + + const handleRefreshActivities = async () => { + const success = await fetchActivities(); + if (success) { + toast.success("Activities refreshed successfully."); + } + }; + + // Get the currently filtered activities + const getFilteredActivities = () => { + return activities.filter(activity => { + let isIncluded = true; + + if (filter.status) { + isIncluded = isIncluded && activity.status === filter.status; + } + + if (filter.type) { + if (filter.type === 'repository') { + isIncluded = isIncluded && !!activity.repositoryId; + } else if (filter.type === 'organization') { + isIncluded = isIncluded && !!activity.organizationId; + } + } + + if (filter.name) { + isIncluded = isIncluded && ( + activity.repositoryName === filter.name || + activity.organizationName === filter.name + ); + } + + // Note: We're not applying the search term filter here as that would require + // re-implementing the Fuse.js search logic + + return isIncluded; + }); + }; + + // Function to export activities as CSV + const exportAsCSV = () => { + const filteredActivities = getFilteredActivities(); + + if (filteredActivities.length === 0) { + toast.error("No activities to export."); + return; + } + + // Create CSV content + const headers = ["Timestamp", "Message", "Status", "Repository", "Organization", "Details"]; + const csvRows = [ + headers.join(","), + ...filteredActivities.map(activity => { + const formattedDate = formatDate(activity.timestamp); + // Escape fields that might contain commas or quotes + const escapeCsvField = (field: string | null | undefined) => { + if (!field) return ''; + if (field.includes(',') || field.includes('"') || field.includes('\n')) { + return `"${field.replace(/"/g, '""')}"`; + } + return field; + }; + + return [ + formattedDate, + escapeCsvField(activity.message), + activity.status, + escapeCsvField(activity.repositoryName || ''), + escapeCsvField(activity.organizationName || ''), + escapeCsvField(activity.details || '') + ].join(','); + }) + ]; + + const csvContent = csvRows.join('\n'); + + // Download the CSV file + downloadFile(csvContent, 'text/csv;charset=utf-8;', 'activity_log_export.csv'); + + toast.success("Activity log exported as CSV successfully."); + }; + + // Function to export activities as JSON + const exportAsJSON = () => { + const filteredActivities = getFilteredActivities(); + + if (filteredActivities.length === 0) { + toast.error("No activities to export."); + return; + } + + // Format the activities for export (removing any sensitive or unnecessary fields if needed) + const activitiesForExport = filteredActivities.map(activity => ({ + id: activity.id, + timestamp: activity.timestamp, + formattedTime: formatDate(activity.timestamp), + message: activity.message, + status: activity.status, + repositoryId: activity.repositoryId, + repositoryName: activity.repositoryName, + organizationId: activity.organizationId, + organizationName: activity.organizationName, + details: activity.details + })); + + const jsonContent = JSON.stringify(activitiesForExport, null, 2); + + // Download the JSON file + downloadFile(jsonContent, 'application/json', 'activity_log_export.json'); + + toast.success("Activity log exported as JSON successfully."); + }; + + // Generic function to download a file + const downloadFile = (content: string, mimeType: string, filename: string) => { + // Add date to filename + const date = new Date(); + const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + const filenameWithDate = filename.replace('.', `_${dateStr}.`); + + // Create a download link + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + + link.href = url; + link.setAttribute('download', filenameWithDate); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + return ( +
+
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + + {/* Repository/Organization Name Combobox */} + setFilter((prev) => ({ ...prev, name }))} + /> + {/* Filter by type: repository/org/all */} + + + + + + + + Export as CSV + + + Export as JSON + + + + +
+
+ +
+
+ ); +} diff --git a/src/components/activity/ActivityNameCombobox.tsx b/src/components/activity/ActivityNameCombobox.tsx new file mode 100644 index 0000000..bdac6f7 --- /dev/null +++ b/src/components/activity/ActivityNameCombobox.tsx @@ -0,0 +1,86 @@ +import * as React from "react"; +import { ChevronsUpDown, Check } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +type ActivityNameComboboxProps = { + activities: any[]; + value: string; + onChange: (value: string) => void; +}; + +export function ActivityNameCombobox({ activities, value, onChange }: ActivityNameComboboxProps) { + // Collect unique names from repositoryName and organizationName + const names = React.useMemo(() => { + const set = new Set(); + activities.forEach((a) => { + if (a.repositoryName) set.add(a.repositoryName); + if (a.organizationName) set.add(a.organizationName); + }); + return Array.from(set).sort(); + }, [activities]); + + const [open, setOpen] = React.useState(false); + return ( + + + + + + + + + No name found. + + { + onChange(""); + setOpen(false); + }} + > + + All Names + + {names.map((name) => ( + { + onChange(name); + setOpen(false); + }} + > + + {name} + + ))} + + + + + + ); +} diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..20c2b36 --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,117 @@ +'use client'; + +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { SiGitea } from 'react-icons/si'; +import { toast, Toaster } from 'sonner'; +import { FlipHorizontal } from 'lucide-react'; + +export function LoginForm() { + const [isLoading, setIsLoading] = useState(false); + + async function handleLogin(e: React.FormEvent) { + e.preventDefault(); + setIsLoading(true); + const form = e.currentTarget; + const formData = new FormData(form); + const username = formData.get('username') as string | null; + const password = formData.get('password') as string | null; + + if (!username || !password) { + toast.error('Please enter both username and password'); + setIsLoading(false); + return; + } + + const loginData = { username, password }; + + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(loginData), + }); + + const data = await response.json(); + + if (response.ok) { + toast.success('Login successful!'); + // Small delay before redirecting to see the success message + setTimeout(() => { + window.location.href = '/'; + }, 1000); + } else { + toast.error(data.error || 'Login failed. Please try again.'); + } + } catch (error) { + toast.error('An error occurred while logging in. Please try again.'); + } finally { + setIsLoading(false); + } + } + + return ( + <> + + +
+ +
+ Gitea Mirror + + Log in to manage your GitHub to Gitea mirroring + +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ + + +
+

+ Don't have an account? Contact your administrator. +

+
+
+ + + ); +} diff --git a/src/components/auth/SignupForm.tsx b/src/components/auth/SignupForm.tsx new file mode 100644 index 0000000..fc82a90 --- /dev/null +++ b/src/components/auth/SignupForm.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { GitMerge } from 'lucide-react'; +import { toast, Toaster } from 'sonner'; + +export function SignupForm() { + const [isLoading, setIsLoading] = useState(false); + + async function handleSignup(e: React.FormEvent) { + e.preventDefault(); + setIsLoading(true); + const form = e.currentTarget; + const formData = new FormData(form); + const username = formData.get('username') as string | null; + const email = formData.get('email') as string | null; + const password = formData.get('password') as string | null; + const confirmPassword = formData.get('confirmPassword') as string | null; + + if (!username || !email || !password || !confirmPassword) { + toast.error('Please fill in all fields'); + setIsLoading(false); + return; + } + + if (password !== confirmPassword) { + toast.error('Passwords do not match'); + setIsLoading(false); + return; + } + + const signupData = { username, email, password }; + + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(signupData), + }); + + const data = await response.json(); + + if (response.ok) { + toast.success('Account created successfully! Redirecting to dashboard...'); + // Small delay before redirecting to see the success message + setTimeout(() => { + window.location.href = '/'; + }, 1500); + } else { + toast.error(data.error || 'Failed to create account. Please try again.'); + } + } catch (error) { + toast.error('An error occurred while creating your account. Please try again.'); + } finally { + setIsLoading(false); + } + } + + return ( + <> + + +
+ +
+ Create Admin Account + + Set up your administrator account for Gitea Mirror + +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + +
+ + + ); +} diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx new file mode 100644 index 0000000..931e70f --- /dev/null +++ b/src/components/config/ConfigTabs.tsx @@ -0,0 +1,398 @@ +import { useEffect, useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { GitHubConfigForm } from "./GitHubConfigForm"; +import { GiteaConfigForm } from "./GiteaConfigForm"; +import { ScheduleConfigForm } from "./ScheduleConfigForm"; +import type { + ConfigApiResponse, + GiteaConfig, + GitHubConfig, + SaveConfigApiRequest, + SaveConfigApiResponse, + ScheduleConfig, +} from "@/types/config"; +import { Button } from "../ui/button"; +import { useAuth } from "@/hooks/useAuth"; +import { apiRequest } from "@/lib/utils"; +import { Copy, CopyCheck, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; + +type ConfigState = { + githubConfig: GitHubConfig; + giteaConfig: GiteaConfig; + scheduleConfig: ScheduleConfig; +}; + +export function ConfigTabs() { + const [config, setConfig] = useState({ + githubConfig: { + username: "", + token: "", + skipForks: false, + privateRepositories: false, + mirrorIssues: false, + mirrorStarred: false, + preserveOrgStructure: false, + skipStarredIssues: false, + }, + + giteaConfig: { + url: "", + username: "", + token: "", + organization: "github-mirrors", + visibility: "public", + starredReposOrg: "github", + }, + + scheduleConfig: { + enabled: false, + interval: 3600, + }, + }); + const { user, refreshUser } = useAuth(); + const [isLoading, setIsLoading] = useState(true); + const [dockerCode, setDockerCode] = useState(""); + const [isCopied, setIsCopied] = useState(false); + const [isSyncing, setIsSyncing] = useState(false); + const [isConfigSaved, setIsConfigSaved] = useState(false); + + // Check if all required fields are filled to enable the Save Configuration button + const isConfigFormValid = (): boolean => { + const { githubConfig, giteaConfig } = config; + + // Check GitHub required fields + const isGitHubValid = !!( + githubConfig.username?.trim() && githubConfig.token?.trim() + ); + + // Check Gitea required fields + const isGiteaValid = !!( + giteaConfig.url?.trim() && + giteaConfig.username?.trim() && + giteaConfig.token?.trim() + ); + + return isGitHubValid && isGiteaValid; + }; + + useEffect(() => { + const updateLastAndNextRun = () => { + const lastRun = config.scheduleConfig.lastRun + ? new Date(config.scheduleConfig.lastRun) + : new Date(); // fallback to now if lastRun is null + const intervalInSeconds = config.scheduleConfig.interval; + const nextRun = new Date(lastRun.getTime() + intervalInSeconds * 1000); + + setConfig((prev) => ({ + ...prev, + scheduleConfig: { + ...prev.scheduleConfig, + lastRun, + nextRun, + }, + })); + }; + + updateLastAndNextRun(); + }, [config.scheduleConfig.interval]); + + const handleImportGitHubData = async () => { + try { + if (!user?.id) return; + + setIsSyncing(true); + + const result = await apiRequest<{ success: boolean; message?: string }>( + `/sync?userId=${user.id}`, + { + method: "POST", + } + ); + + if (result.success) { + toast.success( + "GitHub data imported successfully! Head to the Dashboard to start mirroring repositories." + ); + } else { + toast.error( + `Failed to import GitHub data: ${result.message || "Unknown error"}` + ); + } + } catch (error) { + toast.error( + `Error importing GitHub data: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } finally { + setIsSyncing(false); + } + }; + + const handleSaveConfig = async () => { + try { + if (!user || !user.id) { + return; + } + + const reqPyload: SaveConfigApiRequest = { + userId: user.id, + githubConfig: config.githubConfig, + giteaConfig: config.giteaConfig, + scheduleConfig: config.scheduleConfig, + }; + const response = await fetch("/api/config", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(reqPyload), + }); + + const result: SaveConfigApiResponse = await response.json(); + + if (result.success) { + await refreshUser(); + setIsConfigSaved(true); + + toast.success( + "Configuration saved successfully! Now import your GitHub data to begin." + ); + } else { + toast.error( + `Failed to save configuration: ${result.message || "Unknown error"}` + ); + } + } catch (error) { + toast.error( + `An error occurred while saving the configuration: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; + + useEffect(() => { + const fetchConfig = async () => { + try { + if (!user) { + return; + } + + setIsLoading(true); + + const response = await apiRequest( + `/config?userId=${user.id}`, + { + method: "GET", + } + ); + + // Check if we have a valid config response + if (response && !response.error) { + setConfig({ + githubConfig: response.githubConfig || config.githubConfig, + giteaConfig: response.giteaConfig || config.giteaConfig, + scheduleConfig: response.scheduleConfig || config.scheduleConfig, + }); + + // If we got a valid config from the server, it means it was previously saved + if (response.id) { + setIsConfigSaved(true); + } + } + // If there's an error, we'll just use the default config defined in state + + setIsLoading(false); + } catch (error) { + // Don't show error for first-time users, just use the default config + console.warn("Could not fetch configuration, using defaults:", error); + } finally { + setIsLoading(false); + } + }; + + fetchConfig(); + }, [user]); + + useEffect(() => { + const generateDockerCode = () => { + return `version: "3.3" +services: + gitea-mirror: + image: arunavo4/gitea-mirror:latest + restart: unless-stopped + container_name: gitea-mirror + environment: + - GITHUB_USERNAME=${config.githubConfig.username} + - GITEA_URL=${config.giteaConfig.url} + - GITEA_TOKEN=${config.giteaConfig.token} + - GITHUB_TOKEN=${config.githubConfig.token} + - SKIP_FORKS=${config.githubConfig.skipForks} + - PRIVATE_REPOSITORIES=${config.githubConfig.privateRepositories} + - MIRROR_ISSUES=${config.githubConfig.mirrorIssues} + - MIRROR_STARRED=${config.githubConfig.mirrorStarred} + - PRESERVE_ORG_STRUCTURE=${config.githubConfig.preserveOrgStructure} + - SKIP_STARRED_ISSUES=${config.githubConfig.skipStarredIssues} + - GITEA_ORGANIZATION=${config.giteaConfig.organization} + - GITEA_ORG_VISIBILITY=${config.giteaConfig.visibility} + - DELAY=${config.scheduleConfig.interval}`; + }; + + const code = generateDockerCode(); + setDockerCode(code); + }, [config]); + + const handleCopyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).then( + () => { + setIsCopied(true); + toast.success("Docker configuration copied to clipboard!"); + setTimeout(() => setIsCopied(false), 2000); + }, + (err) => { + toast.error("Could not copy text to clipboard."); + } + ); + }; + + return isLoading ? ( +
loading...
+ ) : ( +
+ + +
+ Configuration Settings + + Configure your GitHub and Gitea connections, and set up automatic + mirroring. + +
+ +
+ + +
+
+ + +
+
+ + setConfig((prev) => ({ + ...prev, + githubConfig: + typeof update === "function" + ? update(prev.githubConfig) + : update, + })) + } + /> + + + setConfig((prev) => ({ + ...prev, + giteaConfig: + typeof update === "function" + ? update(prev.giteaConfig) + : update, + githubConfig: prev?.githubConfig ?? ({} as GitHubConfig), + scheduleConfig: + prev?.scheduleConfig ?? ({} as ScheduleConfig), + })) + } + /> +
+ + + setConfig((prev) => ({ + ...prev, + scheduleConfig: + typeof update === "function" + ? update(prev.scheduleConfig) + : update, + githubConfig: prev?.githubConfig ?? ({} as GitHubConfig), + giteaConfig: prev?.giteaConfig ?? ({} as GiteaConfig), + })) + } + /> +
+
+
+ + + + Docker Configuration + + Equivalent Docker configuration for your current settings. + + + + + + +
+            {dockerCode}
+          
+
+
+
+ ); +} diff --git a/src/components/config/GitHubConfigForm.tsx b/src/components/config/GitHubConfigForm.tsx new file mode 100644 index 0000000..e4b1801 --- /dev/null +++ b/src/components/config/GitHubConfigForm.tsx @@ -0,0 +1,340 @@ +import React, { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { githubApi } from "@/lib/api"; +import type { GitHubConfig } from "@/types/config"; +import { Input } from "../ui/input"; +import { Checkbox } from "../ui/checkbox"; +import { toast } from "sonner"; +import { AlertTriangle } from "lucide-react"; +import { Alert, AlertDescription } from "../ui/alert"; +import { Info } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; + +interface GitHubConfigFormProps { + config: GitHubConfig; + setConfig: React.Dispatch>; +} + +export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) { + const [isLoading, setIsLoading] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + + // Special handling for preserveOrgStructure changes + if ( + name === "preserveOrgStructure" && + config.preserveOrgStructure !== checked + ) { + toast.info( + "Changing this setting may affect how repositories are accessed in Gitea. " + + "Existing mirrored repositories will still be accessible during sync operations.", + { + duration: 6000, + position: "top-center", + } + ); + } + + setConfig({ + ...config, + [name]: type === "checkbox" ? checked : value, + }); + }; + + const testConnection = async () => { + if (!config.token) { + toast.error("GitHub token is required to test the connection"); + return; + } + + setIsLoading(true); + + try { + const result = await githubApi.testConnection(config.token); + if (result.success) { + toast.success("Successfully connected to GitHub!"); + } else { + toast.error("Failed to connect to GitHub. Please check your token."); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "An unknown error occurred" + ); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + GitHub Configuration + + + + + +
+ + +
+ +
+ + +

+ Required for private repositories, organizations, and starred + repositories. +

+
+ +
+
+
+ + handleChange({ + target: { + name: "skipForks", + type: "checkbox", + checked: Boolean(checked), + value: "", + }, + } as React.ChangeEvent) + } + /> + +
+ +
+ + handleChange({ + target: { + name: "privateRepositories", + type: "checkbox", + checked: Boolean(checked), + value: "", + }, + } as React.ChangeEvent) + } + /> + +
+ +
+ + handleChange({ + target: { + name: "mirrorStarred", + type: "checkbox", + checked: Boolean(checked), + value: "", + }, + } as React.ChangeEvent) + } + /> + +
+
+ +
+
+ + handleChange({ + target: { + name: "mirrorIssues", + type: "checkbox", + checked: Boolean(checked), + value: "", + }, + } as React.ChangeEvent) + } + /> + +
+ +
+ + handleChange({ + target: { + name: "preserveOrgStructure", + type: "checkbox", + checked: Boolean(checked), + value: "", + }, + } as React.ChangeEvent) + } + /> + +
+ +
+ + handleChange({ + target: { + name: "skipStarredIssues", + type: "checkbox", + checked: Boolean(checked), + value: "", + }, + } as React.ChangeEvent) + } + /> + +
+
+
+
+ + + + + +
Note:
+
+ You need to create a{" "} + Classic GitHub PAT Token{" "} + with following scopes: +
+
    +
  • + repo +
  • +
  • + admin:org +
  • +
+
+ The organization access is required for mirroring organization + repositories. +
+
+ You can generate tokens at{" "} + + github.com/settings/tokens + + . +
+
+
+
+
+ ); +} diff --git a/src/components/config/GiteaConfigForm.tsx b/src/components/config/GiteaConfigForm.tsx new file mode 100644 index 0000000..beeb8b3 --- /dev/null +++ b/src/components/config/GiteaConfigForm.tsx @@ -0,0 +1,228 @@ +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { giteaApi } from "@/lib/api"; +import type { GiteaConfig, GiteaOrgVisibility } from "@/types/config"; +import { toast } from "sonner"; + +interface GiteaConfigFormProps { + config: GiteaConfig; + setConfig: React.Dispatch>; +} + +export function GiteaConfigForm({ config, setConfig }: GiteaConfigFormProps) { + const [isLoading, setIsLoading] = useState(false); + + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setConfig({ + ...config, + [name]: value, + }); + }; + + const testConnection = async () => { + if (!config.url || !config.token) { + toast.error("Gitea URL and token are required to test the connection"); + return; + } + + setIsLoading(true); + + try { + const result = await giteaApi.testConnection(config.url, config.token); + if (result.success) { + toast.success("Successfully connected to Gitea!"); + } else { + toast.error( + "Failed to connect to Gitea. Please check your URL and token." + ); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "An unknown error occurred" + ); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + Gitea Configuration + + + + + +
+ + +
+ +
+ + +
+ +
+ + +

+ Create a token in your Gitea instance under Settings > + Applications. +

+
+ +
+ + +

+ If specified, repositories will be mirrored to this organization. +

+
+ +
+
+ + +
+ +
+ + +

+ Organization for starred repositories (default: github) +

+
+
+
+ + + {/* Footer content can be added here if needed */} + +
+ ); +} diff --git a/src/components/config/ScheduleConfigForm.tsx b/src/components/config/ScheduleConfigForm.tsx new file mode 100644 index 0000000..405b5b5 --- /dev/null +++ b/src/components/config/ScheduleConfigForm.tsx @@ -0,0 +1,139 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Checkbox } from "../ui/checkbox"; +import type { ScheduleConfig } from "@/types/config"; +import { formatDate } from "@/lib/utils"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; + +interface ScheduleConfigFormProps { + config: ScheduleConfig; + setConfig: React.Dispatch>; +} + +export function ScheduleConfigForm({ + config, + setConfig, +}: ScheduleConfigFormProps) { + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value, type } = e.target; + setConfig({ + ...config, + [name]: + type === "checkbox" ? (e.target as HTMLInputElement).checked : value, + }); + }; + + // Convert seconds to human-readable format + const formatInterval = (seconds: number): string => { + if (seconds < 60) return `${seconds} seconds`; + if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`; + return `${Math.floor(seconds / 86400)} days`; + }; + + // Predefined intervals + const intervals: { value: number; label: string }[] = [ + // { value: 120, label: "2 minutes" }, //for testing + { value: 900, label: "15 minutes" }, + { value: 1800, label: "30 minutes" }, + { value: 3600, label: "1 hour" }, + { value: 7200, label: "2 hours" }, + { value: 14400, label: "4 hours" }, + { value: 28800, label: "8 hours" }, + { value: 43200, label: "12 hours" }, + { value: 86400, label: "1 day" }, + { value: 172800, label: "2 days" }, + { value: 604800, label: "1 week" }, + ]; + + return ( + + +
+
+ + handleChange({ + target: { + name: "enabled", + type: "checkbox", + checked: Boolean(checked), + value: "", + }, + } as React.ChangeEvent) + } + /> + +
+ +
+ + + + +

+ How often the mirroring process should run. +

+
+ + {config.lastRun && ( +
+ +
{formatDate(config.lastRun)}
+
+ )} + + {config.nextRun && config.enabled && ( +
+ +
{formatDate(config.nextRun)}
+
+ )} +
+
+
+ ); +} diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx new file mode 100644 index 0000000..41156dd --- /dev/null +++ b/src/components/dashboard/Dashboard.tsx @@ -0,0 +1,145 @@ +import { StatusCard } from "./StatusCard"; +import { RecentActivity } from "./RecentActivity"; +import { RepositoryList } from "./RepositoryList"; +import { GitFork, Clock, FlipHorizontal, Building2 } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import type { MirrorJob, Organization, Repository } from "@/lib/db/schema"; +import { useAuth } from "@/hooks/useAuth"; +import { apiRequest } from "@/lib/utils"; +import type { DashboardApiResponse } from "@/types/dashboard"; +import { useSSE } from "@/hooks/useSEE"; +import { toast } from "sonner"; + +export function Dashboard() { + const { user } = useAuth(); + const [repositories, setRepositories] = useState([]); + const [organizations, setOrganizations] = useState([]); + const [activities, setActivities] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [repoCount, setRepoCount] = useState(0); + const [orgCount, setOrgCount] = useState(0); + const [mirroredCount, setMirroredCount] = useState(0); + const [lastSync, setLastSync] = useState(null); + + // Create a stable callback using useCallback + const handleNewMessage = useCallback((data: MirrorJob) => { + if (data.repositoryId) { + setRepositories((prevRepos) => + prevRepos.map((repo) => + repo.id === data.repositoryId + ? { ...repo, status: data.status, details: data.details } + : repo + ) + ); + } else if (data.organizationId) { + setOrganizations((prevOrgs) => + prevOrgs.map((org) => + org.id === data.organizationId + ? { ...org, status: data.status, details: data.details } + : org + ) + ); + } + + setActivities((prevActivities) => [data, ...prevActivities]); + + console.log("Received new log:", data); + }, []); + + // Use the SSE hook + const { connected } = useSSE({ + userId: user?.id, + onMessage: handleNewMessage, + }); + + useEffect(() => { + const fetchDashboardData = async () => { + try { + if (!user || !user.id) { + return; + } + + setIsLoading(false); + + const response = await apiRequest( + `/dashboard?userId=${user.id}`, + { + method: "GET", + } + ); + + if (response.success) { + setRepositories(response.repositories); + setOrganizations(response.organizations); + setActivities(response.activities); + setRepoCount(response.repoCount); + setOrgCount(response.orgCount); + setMirroredCount(response.mirroredCount); + setLastSync(response.lastSync); + } else { + toast.error(response.error || "Error fetching dashboard data"); + } + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Error fetching dashboard data" + ); + } finally { + setIsLoading(false); + } + }; + + fetchDashboardData(); + }, [user]); + + return isLoading || !connected ? ( +
loading...
+ ) : ( +
+
+ } + description="Repositories being mirrored" + /> + } + description="Successfully mirrored" + /> + } + description="GitHub organizations" + /> + } + description="Last successful sync" + /> +
+ +
+ + + {/* the api already sends 10 activities only but slicing in case of realtime updates */} + +
+
+ ); +} diff --git a/src/components/dashboard/RecentActivity.tsx b/src/components/dashboard/RecentActivity.tsx new file mode 100644 index 0000000..f1b3bac --- /dev/null +++ b/src/components/dashboard/RecentActivity.tsx @@ -0,0 +1,48 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { MirrorJob } from "@/lib/db/schema"; +import { formatDate, getStatusColor } from "@/lib/utils"; +import { Button } from "../ui/button"; + +interface RecentActivityProps { + activities: MirrorJob[]; +} + +export function RecentActivity({ activities }: RecentActivityProps) { + return ( + + + Recent Activity + + + +
+ {activities.length === 0 ? ( +

No recent activity

+ ) : ( + activities.map((activity, index) => ( +
+
+
+
+
+

+ {activity.message} +

+

+ {formatDate(activity.timestamp)} +

+
+
+ )) + )} +
+ + + ); +} diff --git a/src/components/dashboard/RepositoryList.tsx b/src/components/dashboard/RepositoryList.tsx new file mode 100644 index 0000000..f4ef5bd --- /dev/null +++ b/src/components/dashboard/RepositoryList.tsx @@ -0,0 +1,92 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { GitFork } from "lucide-react"; +import { SiGithub } from "react-icons/si"; +import type { Repository } from "@/lib/db/schema"; +import { getStatusColor } from "@/lib/utils"; + +interface RepositoryListProps { + repositories: Repository[]; +} + +export function RepositoryList({ repositories }: RepositoryListProps) { + return ( + + {/* calculating the max height based non the other elements and sizing styles */} + + Repositories + + + + {repositories.length === 0 ? ( +
+ +

No repositories found

+

+ Configure your GitHub connection to start mirroring repositories. +

+ +
+ ) : ( +
+ {repositories.map((repo, index) => ( +
+
+
+

{repo.name}

+ {repo.isPrivate && ( + + Private + + )} +
+
+ + {repo.owner} + + {repo.organization && ( + + â€ĸ {repo.organization} + + )} +
+
+ +
+
+ + {/* setting the minimum width to 3rem corresponding to the largest status (mirrored) so that all are left alligned */} + {repo.status} + + + +
+
+ ))} +
+ )} + + + ); +} diff --git a/src/components/dashboard/StatusCard.tsx b/src/components/dashboard/StatusCard.tsx new file mode 100644 index 0000000..8758840 --- /dev/null +++ b/src/components/dashboard/StatusCard.tsx @@ -0,0 +1,33 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +interface StatusCardProps { + title: string; + value: string | number; + icon: React.ReactNode; + description?: string; + className?: string; +} + +export function StatusCard({ + title, + value, + icon, + description, + className, +}: StatusCardProps) { + return ( + + + {title} +
{icon}
+
+ +
{value}
+ {description && ( +

{description}

+ )} +
+
+ ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..f469f7e --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,49 @@ +import { useAuth } from "@/hooks/useAuth"; +import { Button } from "@/components/ui/button"; +import { SiGitea } from "react-icons/si"; +import { ModeToggle } from "@/components/theme/ModeToggle"; +import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; +import { toast } from "sonner"; + +export function Header() { + const { user, logout } = useAuth(); + + const handleLogout = async () => { + toast.success("Logged out successfully"); + // Small delay to show the toast before redirecting + await new Promise((resolve) => setTimeout(resolve, 500)); + logout(); + }; + + return ( +
+
+ + + Gitea Mirror + + +
+ + {user ? ( + <> + + + + {user.username.charAt(0).toUpperCase()} + + + + + ) : ( + + )} +
+
+
+ ); +} diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx new file mode 100644 index 0000000..3506c11 --- /dev/null +++ b/src/components/layout/MainLayout.tsx @@ -0,0 +1,61 @@ +import { Header } from "./Header"; +import { Sidebar } from "./Sidebar"; +import { Dashboard } from "@/components/dashboard/Dashboard"; +import Repository from "../repositories/Repository"; +import Providers from "./Providers"; +import { ConfigTabs } from "../config/ConfigTabs"; +import { ActivityLog } from "../activity/ActivityLog"; +import { Organization } from "../organizations/Organization"; +import { Toaster } from "@/components/ui/sonner"; +import { useAuth } from "@/hooks/useAuth"; +import { useRepoSync } from "@/hooks/useSyncRepo"; + +interface AppProps { + page: + | "dashboard" + | "repositories" + | "organizations" + | "configuration" + | "activity-log"; + "client:load"?: boolean; + "client:idle"?: boolean; + "client:visible"?: boolean; + "client:media"?: string; + "client:only"?: boolean | string; +} + +export default function App({ page }: AppProps) { + return ( + + + + ); +} + +function AppWithProviders({ page }: AppProps) { + const { user } = useAuth(); + useRepoSync({ + userId: user?.id, + enabled: user?.syncEnabled, + interval: user?.syncInterval, + lastSync: user?.lastSync, + nextSync: user?.nextSync, + }); + + return ( +
+
+
+ +
+ {page === "dashboard" && } + {page === "repositories" && } + {page === "organizations" && } + {page === "configuration" && } + {page === "activity-log" && } +
+
+ +
+ ); +} diff --git a/src/components/layout/Providers.tsx b/src/components/layout/Providers.tsx new file mode 100644 index 0000000..59ecbbe --- /dev/null +++ b/src/components/layout/Providers.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; +import { AuthProvider } from "@/hooks/useAuth"; +import { TooltipProvider } from "@/components/ui/tooltip"; + +export default function Providers({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..774b1ab --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; +import { ExternalLink } from "lucide-react"; +import { links } from "@/data/Sidebar"; + +interface SidebarProps { + className?: string; +} + +export function Sidebar({ className }: SidebarProps) { + const [currentPath, setCurrentPath] = useState(""); + + useEffect(() => { + // Hydration happens here + const path = window.location.pathname; + setCurrentPath(path); + console.log("Hydrated path:", path); // Should log now + }, []); + + return ( + + ); +} diff --git a/src/components/organizations/AddOrganizationDialog.tsx b/src/components/organizations/AddOrganizationDialog.tsx new file mode 100644 index 0000000..f2e50c3 --- /dev/null +++ b/src/components/organizations/AddOrganizationDialog.tsx @@ -0,0 +1,150 @@ +import * as React from "react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import { LoaderCircle, Plus } from "lucide-react"; +import type { MembershipRole } from "@/types/organizations"; +import { RadioGroup, RadioGroupItem } from "../ui/radio"; +import { Label } from "../ui/label"; + +interface AddOrganizationDialogProps { + isDialogOpen: boolean; + setIsDialogOpen: (isOpen: boolean) => void; + onAddOrganization: ({ + org, + role, + }: { + org: string; + role: MembershipRole; + }) => Promise; +} + +export default function AddOrganizationDialog({ + isDialogOpen, + setIsDialogOpen, + onAddOrganization, +}: AddOrganizationDialogProps) { + const [org, setOrg] = useState(""); + const [role, setRole] = useState("member"); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!org || org.trim() === "") { + setError("Please enter a valid organization name."); + return; + } + + try { + setIsLoading(true); + + await onAddOrganization({ org, role }); + + setError(""); + setOrg(""); + setRole("member"); + setIsDialogOpen(false); + } catch (err: any) { + setError(err?.message || "Failed to add repository."); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + + + + Add Organization + + You can add public organizations + + + +
+
+
+ + setOrg(e.target.value)} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + placeholder="e.g., microsoft" + autoComplete="off" + autoFocus + required + /> +
+ +
+ + + setRole(val as MembershipRole)} + className="flex flex-col gap-y-2" + > +
+ + +
+
+ + +
+
+ + +
+
+
+ + {error &&

{error}

} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/organizations/Organization.tsx b/src/components/organizations/Organization.tsx new file mode 100644 index 0000000..b9efe63 --- /dev/null +++ b/src/components/organizations/Organization.tsx @@ -0,0 +1,377 @@ +import { useCallback, useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Search, RefreshCw, FlipHorizontal, Plus } from "lucide-react"; +import type { MirrorJob, Organization } from "@/lib/db/schema"; +import { OrganizationList } from "./OrganizationsList"; +import AddOrganizationDialog from "./AddOrganizationDialog"; +import { useAuth } from "@/hooks/useAuth"; +import { apiRequest } from "@/lib/utils"; +import { + membershipRoleEnum, + type AddOrganizationApiRequest, + type AddOrganizationApiResponse, + type MembershipRole, + type OrganizationsApiResponse, +} from "@/types/organizations"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import type { MirrorOrgRequest, MirrorOrgResponse } from "@/types/mirror"; +import { useSSE } from "@/hooks/useSEE"; +import { useFilterParams } from "@/hooks/useFilterParams"; +import { toast } from "sonner"; + +export function Organization() { + const [organizations, setOrganizations] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const { user } = useAuth(); + const { filter, setFilter } = useFilterParams({ + searchTerm: "", + membershipRole: "", + status: "", + }); + const [loadingOrgIds, setLoadingOrgIds] = useState>(new Set()); // this is used when the api actions are performed + + // Create a stable callback using useCallback + const handleNewMessage = useCallback((data: MirrorJob) => { + if (data.organizationId) { + setOrganizations((prevOrgs) => + prevOrgs.map((org) => + org.id === data.organizationId + ? { ...org, status: data.status, details: data.details } + : org + ) + ); + } + + console.log("Received new log:", data); + }, []); + + // Use the SSE hook + const { connected } = useSSE({ + userId: user?.id, + onMessage: handleNewMessage, + }); + + const fetchOrganizations = useCallback(async () => { + if (!user || !user.id) { + return false; + } + + try { + setIsLoading(true); + + const response = await apiRequest( + `/github/organizations?userId=${user.id}`, + { + method: "GET", + } + ); + + if (response.success) { + setOrganizations(response.organizations); + return true; + } else { + toast.error(response.error || "Error fetching organizations"); + return false; + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error fetching organizations" + ); + return false; + } finally { + setIsLoading(false); + } + }, [user]); + + useEffect(() => { + fetchOrganizations(); + }, [fetchOrganizations]); + + const handleRefresh = async () => { + const success = await fetchOrganizations(); + if (success) { + toast.success("Organizations refreshed successfully."); + } + }; + + const handleMirrorOrg = async ({ orgId }: { orgId: string }) => { + try { + if (!user || !user.id) { + return; + } + + setLoadingOrgIds((prev) => new Set(prev).add(orgId)); + + const reqPayload: MirrorOrgRequest = { + userId: user.id, + organizationIds: [orgId], + }; + + const response = await apiRequest("/job/mirror-org", { + method: "POST", + data: reqPayload, + }); + + if (response.success) { + toast.success(`Mirroring started for organization ID: ${orgId}`); + + setOrganizations((prevOrgs) => + prevOrgs.map((org) => { + const updated = response.organizations.find((o) => o.id === org.id); + return updated ? updated : org; + }) + ); + } else { + toast.error(response.error || "Error starting mirror job"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error starting mirror job" + ); + } finally { + setLoadingOrgIds((prev) => { + const newSet = new Set(prev); + newSet.delete(orgId); + return newSet; + }); + } + }; + + const handleAddOrganization = async ({ + org, + role, + }: { + org: string; + role: MembershipRole; + }) => { + try { + if (!user || !user.id) { + return; + } + + const reqPayload: AddOrganizationApiRequest = { + userId: user.id, + org, + role, + }; + + const response = await apiRequest( + "/sync/organization", + { + method: "POST", + data: reqPayload, + } + ); + + if (response.success) { + toast.success(`Organization added successfully`); + setOrganizations((prev) => [...prev, response.organization]); + + await fetchOrganizations(); + + setFilter((prev) => ({ + ...prev, + searchTerm: org, + })); + } else { + toast.error(response.error || "Error adding organization"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error adding organization" + ); + } finally { + setIsLoading(false); + } + }; + + const handleMirrorAllOrgs = async () => { + try { + if (!user || !user.id || organizations.length === 0) { + return; + } + + // Filter out organizations that are already mirrored to avoid duplicate operations + const eligibleOrgs = organizations.filter( + (org) => + org.status !== "mirroring" && org.status !== "mirrored" && org.id + ); + + if (eligibleOrgs.length === 0) { + toast.info("No eligible organizations to mirror"); + return; + } + + // Get all organization IDs + const orgIds = eligibleOrgs.map((org) => org.id as string); + + // Set loading state for all organizations being mirrored + setLoadingOrgIds((prev) => { + const newSet = new Set(prev); + orgIds.forEach((id) => newSet.add(id)); + return newSet; + }); + + const reqPayload: MirrorOrgRequest = { + userId: user.id, + organizationIds: orgIds, + }; + + const response = await apiRequest("/job/mirror-org", { + method: "POST", + data: reqPayload, + }); + + if (response.success) { + toast.success(`Mirroring started for ${orgIds.length} organizations`); + setOrganizations((prevOrgs) => + prevOrgs.map((org) => { + const updated = response.organizations.find((o) => o.id === org.id); + return updated ? updated : org; + }) + ); + } else { + toast.error(response.error || "Error starting mirror jobs"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error starting mirror jobs" + ); + } finally { + // Reset loading states - we'll let the SSE updates handle status changes + setLoadingOrgIds(new Set()); + } + }; + + // Get unique organization names for combobox (since Organization has no owner field) + const ownerOptions = Array.from( + new Set( + organizations.map((org) => org.name).filter((v): v is string => !!v) + ) + ).sort(); + + return ( +
+ {/* Combine search and actions into a single flex row */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Membership Role Filter */} + + + {/* Status Filter */} + + + + + +
+ + setIsDialogOpen(true)} + /> + + +
+ ); +} diff --git a/src/components/organizations/OrganizationsList.tsx b/src/components/organizations/OrganizationsList.tsx new file mode 100644 index 0000000..841b890 --- /dev/null +++ b/src/components/organizations/OrganizationsList.tsx @@ -0,0 +1,178 @@ +import { useMemo } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Plus, RefreshCw, Building2 } from "lucide-react"; +import { SiGithub } from "react-icons/si"; +import type { Organization } from "@/lib/db/schema"; +import type { FilterParams } from "@/types/filter"; +import Fuse from "fuse.js"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Checkbox } from "@/components/ui/checkbox"; +import { getStatusColor } from "@/lib/utils"; + +interface OrganizationListProps { + organizations: Organization[]; + isLoading: boolean; + filter: FilterParams; + setFilter: (filter: FilterParams) => void; + onMirror: ({ orgId }: { orgId: string }) => Promise; + loadingOrgIds: Set; + onAddOrganization?: () => void; +} + +export function OrganizationList({ + organizations, + isLoading, + filter, + setFilter, + onMirror, + loadingOrgIds, + onAddOrganization, +}: OrganizationListProps) { + const hasAnyFilter = Object.values(filter).some( + (val) => val?.toString().trim() !== "" + ); + + const filteredOrganizations = useMemo(() => { + let result = organizations; + + if (filter.membershipRole) { + result = result.filter((org) => org.membershipRole === filter.membershipRole); + } + + if (filter.status) { + result = result.filter((org) => org.status === filter.status); + } + + if (filter.searchTerm) { + const fuse = new Fuse(result, { + keys: ["name", "type"], + threshold: 0.3, + }); + result = fuse.search(filter.searchTerm).map((res) => res.item); + } + + return result; + }, [organizations, filter]); + + return isLoading ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ) : filteredOrganizations.length === 0 ? ( +
+ +

No organizations found

+

+ {hasAnyFilter + ? "Try adjusting your search or filter criteria." + : "Add GitHub organizations to mirror their repositories."} +

+ {hasAnyFilter ? ( + + ) : ( + + )} +
+ ) : ( +
+ {filteredOrganizations.map((org, index) => { + const isLoading = loadingOrgIds.has(org.id ?? ""); + + return ( + +
+ + + {org.membershipRole} + {/* needs to be updated */} + +
+ +

+ {org.repositoryCount}{" "} + {org.repositoryCount === 1 ? "repository" : "repositories"} +

+ +
+
+ { + if (checked && !org.isIncluded && org.id) { + onMirror({ orgId: org.id }); + } + }} + /> + + + {isLoading && ( + + )} +
+ + +
+ + {/* dont know if this looks good. maybe revised */} +
+
+ {org.status} +
+ + ); + })} +
+ ); +} diff --git a/src/components/repositories/AddRepositoryDialog.tsx b/src/components/repositories/AddRepositoryDialog.tsx new file mode 100644 index 0000000..73e572b --- /dev/null +++ b/src/components/repositories/AddRepositoryDialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import { LoaderCircle, Plus } from "lucide-react"; + +interface AddRepositoryDialogProps { + isDialogOpen: boolean; + setIsDialogOpen: (isOpen: boolean) => void; + onAddRepository: ({ + repo, + owner, + }: { + repo: string; + owner: string; + }) => Promise; +} + +export default function AddRepositoryDialog({ + isDialogOpen, + setIsDialogOpen, + onAddRepository, +}: AddRepositoryDialogProps) { + const [repo, setRepo] = useState(""); + const [owner, setOwner] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!repo || !owner || repo.trim() === "" || owner.trim() === "") { + setError("Please enter a valid repository name and owner."); + return; + } + + try { + setIsLoading(true); + + await onAddRepository({ repo, owner }); + + setError(""); + setRepo(""); + setOwner(""); + setIsDialogOpen(false); + } catch (err: any) { + setError(err?.message || "Failed to add repository."); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + + + + Add Repository + + You can add public repositories of others + + + +
+
+
+ + setRepo(e.target.value)} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + placeholder="e.g., next.js" + autoComplete="off" + autoFocus + required + /> +
+ +
+ + setOwner(e.target.value)} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + placeholder="e.g., vercel" + autoComplete="off" + required + /> +
+ + {error &&

{error}

} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/repositories/Repository.tsx b/src/components/repositories/Repository.tsx new file mode 100644 index 0000000..7db7da6 --- /dev/null +++ b/src/components/repositories/Repository.tsx @@ -0,0 +1,457 @@ +import { useCallback, useEffect, useState } from "react"; +import RepositoryTable from "./RepositoryTable"; +import type { MirrorJob, Repository } from "@/lib/db/schema"; +import { useAuth } from "@/hooks/useAuth"; +import { + repoStatusEnum, + type AddRepositoriesApiRequest, + type AddRepositoriesApiResponse, + type RepositoryApiResponse, + type RepoStatus, +} from "@/types/Repository"; +import { apiRequest } from "@/lib/utils"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { Button } from "@/components/ui/button"; +import { Search, RefreshCw, FlipHorizontal } from "lucide-react"; +import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror"; +import { useSSE } from "@/hooks/useSEE"; +import { useFilterParams } from "@/hooks/useFilterParams"; +import { toast } from "sonner"; +import type { SyncRepoRequest, SyncRepoResponse } from "@/types/sync"; +import { OwnerCombobox, OrganizationCombobox } from "./RepositoryComboboxes"; +import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry"; +import AddRepositoryDialog from "./AddRepositoryDialog"; + +export default function Repository() { + const [repositories, setRepositories] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { user } = useAuth(); + const { filter, setFilter } = useFilterParams({ + searchTerm: "", + status: "", + organization: "", + owner: "", + }); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + // Read organization filter from URL when component mounts + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const orgParam = urlParams.get("organization"); + + if (orgParam) { + setFilter((prev) => ({ ...prev, organization: orgParam })); + } + }, [setFilter]); + + const [loadingRepoIds, setLoadingRepoIds] = useState>(new Set()); // this is used when the api actions are performed + + // Create a stable callback using useCallback + const handleNewMessage = useCallback((data: MirrorJob) => { + if (data.repositoryId) { + setRepositories((prevRepos) => + prevRepos.map((repo) => + repo.id === data.repositoryId + ? { ...repo, status: data.status, details: data.details } + : repo + ) + ); + } + + console.log("Received new log:", data); + }, []); + + // Use the SSE hook + const { connected } = useSSE({ + userId: user?.id, + onMessage: handleNewMessage, + }); + + const fetchRepositories = useCallback(async () => { + if (!user) return; + + setIsLoading(true); + try { + const response = await apiRequest( + `/github/repositories?userId=${user.id}`, + { + method: "GET", + } + ); + + if (response.success) { + setRepositories(response.repositories); + return true; + } else { + toast.error(response.error || "Error fetching repositories"); + return false; + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error fetching repositories" + ); + return false; + } finally { + setIsLoading(false); + } + }, [user]); + + useEffect(() => { + fetchRepositories(); + }, [fetchRepositories]); + + const handleRefresh = async () => { + const success = await fetchRepositories(); + if (success) { + toast.success("Repositories refreshed successfully."); + } + }; + + const handleMirrorRepo = async ({ repoId }: { repoId: string }) => { + try { + if (!user || !user.id) { + return; + } + + setLoadingRepoIds((prev) => new Set(prev).add(repoId)); + + const reqPayload: MirrorRepoRequest = { + userId: user.id, + repositoryIds: [repoId], + }; + + const response = await apiRequest( + "/job/mirror-repo", + { + method: "POST", + data: reqPayload, + } + ); + + if (response.success) { + toast.success(`Mirroring started for repository ID: ${repoId}`); + setRepositories((prevRepos) => + prevRepos.map((repo) => { + const updated = response.repositories.find((r) => r.id === repo.id); + return updated ? updated : repo; + }) + ); + } else { + toast.error(response.error || "Error starting mirror job"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error starting mirror job" + ); + } finally { + setLoadingRepoIds((prev) => { + const newSet = new Set(prev); + newSet.delete(repoId); + return newSet; + }); + } + }; + + const handleMirrorAllRepos = async () => { + try { + if (!user || !user.id || repositories.length === 0) { + return; + } + + // Filter out repositories that are already mirroring to avoid duplicate operations. also filter out mirrored (mirrored can be synced and not mirrored again) + const eligibleRepos = repositories.filter( + (repo) => + repo.status !== "mirroring" && repo.status !== "mirrored" && repo.id //not ignoring failed ones because we want to retry them if not mirrored. if mirrored, gitea fucnion handlers will silently ignore them + ); + + if (eligibleRepos.length === 0) { + toast.info("No eligible repositories to mirror"); + return; + } + + // Get all repository IDs + const repoIds = eligibleRepos.map((repo) => repo.id as string); + + // Set loading state for all repositories being mirrored + setLoadingRepoIds((prev) => { + const newSet = new Set(prev); + repoIds.forEach((id) => newSet.add(id)); + return newSet; + }); + + const reqPayload: MirrorRepoRequest = { + userId: user.id, + repositoryIds: repoIds, + }; + + const response = await apiRequest( + "/job/mirror-repo", + { + method: "POST", + data: reqPayload, + } + ); + + if (response.success) { + toast.success(`Mirroring started for ${repoIds.length} repositories`); + setRepositories((prevRepos) => + prevRepos.map((repo) => { + const updated = response.repositories.find((r) => r.id === repo.id); + return updated ? updated : repo; + }) + ); + } else { + toast.error(response.error || "Error starting mirror jobs"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error starting mirror jobs" + ); + } finally { + // Reset loading states - we'll let the SSE updates handle status changes + setLoadingRepoIds(new Set()); + } + }; + + const handleSyncRepo = async ({ repoId }: { repoId: string }) => { + try { + if (!user || !user.id) { + return; + } + + setLoadingRepoIds((prev) => new Set(prev).add(repoId)); + + const reqPayload: SyncRepoRequest = { + userId: user.id, + repositoryIds: [repoId], + }; + + const response = await apiRequest("/job/sync-repo", { + method: "POST", + data: reqPayload, + }); + + if (response.success) { + toast.success(`Syncing started for repository ID: ${repoId}`); + setRepositories((prevRepos) => + prevRepos.map((repo) => { + const updated = response.repositories.find((r) => r.id === repo.id); + return updated ? updated : repo; + }) + ); + } else { + toast.error(response.error || "Error starting sync job"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error starting sync job" + ); + } finally { + setLoadingRepoIds((prev) => { + const newSet = new Set(prev); + newSet.delete(repoId); + return newSet; + }); + } + }; + + const handleRetryRepoAction = async ({ repoId }: { repoId: string }) => { + try { + if (!user || !user.id) { + return; + } + + setLoadingRepoIds((prev) => new Set(prev).add(repoId)); + + const reqPayload: RetryRepoRequest = { + userId: user.id, + repositoryIds: [repoId], + }; + + const response = await apiRequest("/job/retry-repo", { + method: "POST", + data: reqPayload, + }); + + if (response.success) { + toast.success(`Retrying job for repository ID: ${repoId}`); + setRepositories((prevRepos) => + prevRepos.map((repo) => { + const updated = response.repositories.find((r) => r.id === repo.id); + return updated ? updated : repo; + }) + ); + } else { + toast.error(response.error || "Error retrying job"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error retrying job" + ); + } finally { + setLoadingRepoIds((prev) => { + const newSet = new Set(prev); + newSet.delete(repoId); + return newSet; + }); + } + }; + + const handleAddRepository = async ({ + repo, + owner, + }: { + repo: string; + owner: string; + }) => { + try { + if (!user || !user.id) { + return; + } + + const reqPayload: AddRepositoriesApiRequest = { + userId: user.id, + repo, + owner, + }; + + const response = await apiRequest( + "/sync/repository", + { + method: "POST", + data: reqPayload, + } + ); + + if (response.success) { + toast.success(`Repository added successfully`); + setRepositories((prevRepos) => [...prevRepos, response.repository]); + + await fetchRepositories(); + + setFilter((prev) => ({ + ...prev, + searchTerm: repo, + })); + } else { + toast.error(response.error || "Error adding repository"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error adding repository" + ); + } + }; + + // Get unique owners and organizations for comboboxes + const ownerOptions = Array.from( + new Set( + repositories.map((repo) => repo.owner).filter((v): v is string => !!v) + ) + ).sort(); + const orgOptions = Array.from( + new Set( + repositories + .map((repo) => repo.organization) + .filter((v): v is string => !!v) + ) + ).sort(); + + return ( +
+ {/* Combine search and actions into a single flex row */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Owner Combobox */} + + setFilter((prev) => ({ ...prev, owner })) + } + /> + + {/* Organization Combobox */} + + setFilter((prev) => ({ ...prev, organization })) + } + /> + + + + + + +
+ + + + +
+ ); +} diff --git a/src/components/repositories/RepositoryComboboxes.tsx b/src/components/repositories/RepositoryComboboxes.tsx new file mode 100644 index 0000000..f77c11a --- /dev/null +++ b/src/components/repositories/RepositoryComboboxes.tsx @@ -0,0 +1,131 @@ +import * as React from "react"; +import { ChevronsUpDown, Check } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +type ComboboxProps = { + options: string[]; + value: string; + onChange: (value: string) => void; + placeholder?: string; + label?: string; +}; + +export function OwnerCombobox({ options, value, onChange, placeholder = "Owner" }: ComboboxProps) { + const [open, setOpen] = React.useState(false); + return ( + + + + + + + + + No {placeholder.toLowerCase()} found. + + { + onChange(""); + setOpen(false); + }} + > + + All + + {options.map((option) => ( + { + onChange(option); + setOpen(false); + }} + > + + {option} + + ))} + + + + + + ); +} + +export function OrganizationCombobox({ options, value, onChange, placeholder = "Organization" }: ComboboxProps) { + const [open, setOpen] = React.useState(false); + return ( + + + + + + + + + No {placeholder.toLowerCase()} found. + + { + onChange(""); + setOpen(false); + }} + > + + All + + {options.map((option) => ( + { + onChange(option); + setOpen(false); + }} + > + + {option} + + ))} + + + + + + ); +} diff --git a/src/components/repositories/RepositoryTable.tsx b/src/components/repositories/RepositoryTable.tsx new file mode 100644 index 0000000..8cd43dd --- /dev/null +++ b/src/components/repositories/RepositoryTable.tsx @@ -0,0 +1,363 @@ +import { useMemo, useRef } from "react"; +import Fuse from "fuse.js"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import { GitFork, RefreshCw, RotateCcw } from "lucide-react"; +import { SiGithub } from "react-icons/si"; +import type { Repository } from "@/lib/db/schema"; +import { Button } from "@/components/ui/button"; +import { formatDate, getStatusColor } from "@/lib/utils"; +import type { FilterParams } from "@/types/filter"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface RepositoryTableProps { + repositories: Repository[]; + isLoading: boolean; + filter: FilterParams; + setFilter: (filter: FilterParams) => void; + onMirror: ({ repoId }: { repoId: string }) => Promise; + onSync: ({ repoId }: { repoId: string }) => Promise; + onRetry: ({ repoId }: { repoId: string }) => Promise; + loadingRepoIds: Set; +} + +export default function RepositoryTable({ + repositories, + isLoading, + filter, + setFilter, + onMirror, + onSync, + onRetry, + loadingRepoIds, +}: RepositoryTableProps) { + const tableParentRef = useRef(null); + + const hasAnyFilter = Object.values(filter).some( + (val) => val?.toString().trim() !== "" + ); + + const filteredRepositories = useMemo(() => { + let result = repositories; + + if (filter.status) { + result = result.filter((repo) => repo.status === filter.status); + } + + if (filter.owner) { + result = result.filter((repo) => repo.owner === filter.owner); + } + + if (filter.organization) { + result = result.filter( + (repo) => repo.organization === filter.organization + ); + } + + if (filter.searchTerm) { + const fuse = new Fuse(result, { + keys: ["name", "fullName", "owner", "organization"], + threshold: 0.3, + }); + result = fuse.search(filter.searchTerm).map((res) => res.item); + } + + return result; + }, [repositories, filter]); + + const rowVirtualizer = useVirtualizer({ + count: filteredRepositories.length, + getScrollElement: () => tableParentRef.current, + estimateSize: () => 65, + overscan: 5, + }); + + return isLoading ? ( +
+
+
+ Repository +
+
Owner
+
+ Organization +
+
+ Last Mirrored +
+
Status
+
+ Actions +
+
+ + {Array.from({ length: 5 }).map((_, i) => ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ))} +
+ ) : filteredRepositories.length === 0 ? ( +
+ +

No repositories found

+

+ {hasAnyFilter + ? "Try adjusting your search or filter criteria." + : "Configure your GitHub connection to start mirroring repositories."} +

+ {hasAnyFilter ? ( + + ) : ( + + )} +
+ ) : ( +
+ {/* table header */} +
+
+ Repository +
+
Owner
+
+ Organization +
+
+ Last Mirrored +
+
Status
+
+ Actions +
+
+ + {/* table body wrapper (for a parent in virtualization) */} +
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow, index) => { + const repo = filteredRepositories[virtualRow.index]; + const isLoading = loadingRepoIds.has(repo.id ?? ""); + + return ( +
+ {/* Repository */} +
+ +
+
{repo.name}
+
+ {repo.fullName} +
+
+ {repo.isPrivate && ( + + Private + + )} +
+ + {/* Owner */} +
+

{repo.owner}

+
+ + {/* Organization */} +
+

{repo.organization || "-"}

+
+ + {/* Last Mirrored */} +
+

+ {repo.lastMirrored + ? formatDate(new Date(repo.lastMirrored)) + : "Never"} +

+
+ + {/* Status */} +
+
+ {repo.status} +
+ + {/* Actions */} +
+ {/* {repo.status === "mirrored" || + repo.status === "syncing" || + repo.status === "synced" ? ( + + ) : ( + + )} */} + + + onMirror({ repoId: repo.id ?? "" }) + } + onSync={({ repoId }) => onSync({ repoId: repo.id ?? "" })} + onRetry={({ repoId }) => onRetry({ repoId: repo.id ?? "" })} + /> + +
+
+ ); + })} +
+
+
+ ); +} + +function RepoActionButton({ + repo, + isLoading, + onMirror, + onSync, + onRetry, +}: { + repo: { id: string; status: string }; + isLoading: boolean; + onMirror: ({ repoId }: { repoId: string }) => void; + onSync: ({ repoId }: { repoId: string }) => void; + onRetry: ({ repoId }: { repoId: string }) => void; +}) { + const repoId = repo.id ?? ""; + + let label = ""; + let icon = <>; + let onClick = () => {}; + let disabled = isLoading; + + if (repo.status === "failed") { + label = "Retry"; + icon = ; + onClick = () => onRetry({ repoId }); + } else if (["mirrored", "synced", "syncing"].includes(repo.status)) { + label = "Sync"; + icon = ; + onClick = () => onSync({ repoId }); + disabled ||= repo.status === "syncing"; + } else if (["imported", "mirroring"].includes(repo.status)) { + label = "Mirror"; + icon = ; + onClick = () => onMirror({ repoId }); + disabled ||= repo.status === "mirroring"; + } else { + return null; // unsupported status + } + + return ( + + ); +} diff --git a/src/components/theme/ModeToggle.tsx b/src/components/theme/ModeToggle.tsx new file mode 100644 index 0000000..1b582c4 --- /dev/null +++ b/src/components/theme/ModeToggle.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import { Moon, Sun } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ModeToggle() { + const [theme, setThemeState] = React.useState<"light" | "dark" | "system">( + "light" + ); + + React.useEffect(() => { + const isDarkMode = document.documentElement.classList.contains("dark"); + setThemeState(isDarkMode ? "dark" : "light"); + }, []); + + React.useEffect(() => { + const isDark = + theme === "dark" || + (theme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches); + document.documentElement.classList[isDark ? "add" : "remove"]("dark"); + }, [theme]); + + return ( + + + + + + setThemeState("light")}> + Light + + setThemeState("dark")}> + Dark + + setThemeState("system")}> + System + + + + ); +} diff --git a/src/components/theme/ThemeScript.astro b/src/components/theme/ThemeScript.astro new file mode 100644 index 0000000..5471c28 --- /dev/null +++ b/src/components/theme/ThemeScript.astro @@ -0,0 +1,21 @@ +--- +--- + + diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..55363cc --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + warning: + "bg-amber-50 border-amber-200 text-amber-800 dark:bg-amber-950/30 dark:border-amber-800 dark:text-amber-300 [&>svg]:text-amber-600 dark:[&>svg]:text-amber-500", + note: + "bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950/30 dark:border-blue-800 dark:text-blue-200 [&>svg]:text-blue-600 dark:[&>svg]:text-blue-400", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..fda1c3d --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..32f8210 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..88fa63d --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..5360f6d --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,175 @@ +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + ...props +}: React.ComponentProps & { + title?: string + description?: string +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..7d7a9d3 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0d6741b --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..7bad21e --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..40378d4 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..6d51b6c --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/radio.tsx b/src/components/ui/radio.tsx new file mode 100644 index 0000000..4331575 --- /dev/null +++ b/src/components/ui/radio.tsx @@ -0,0 +1,44 @@ +"use client"; + +import * as React from "react"; +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; +import { Circle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { RadioGroup, RadioGroupItem }; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..6168841 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,183 @@ +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Select({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default"; +}) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..c26c4ea --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..a922088 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,24 @@ +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" +import type { ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..85d83be --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..012ad74 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..c013911 --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,17 @@ +import { defineCollection, z } from 'astro:content'; + +// Define a schema for the documentation collection +const docsCollection = defineCollection({ + type: 'content', + schema: z.object({ + title: z.string(), + description: z.string(), + order: z.number().optional(), + updatedDate: z.date().optional(), + }), +}); + +// Export the collections +export const collections = { + 'docs': docsCollection, +}; diff --git a/src/content/docs/architecture.md b/src/content/docs/architecture.md new file mode 100644 index 0000000..7b8b7a9 --- /dev/null +++ b/src/content/docs/architecture.md @@ -0,0 +1,103 @@ +--- +title: "Architecture" +description: "Comprehensive overview of the Gitea Mirror application architecture." +order: 1 +updatedDate: 2023-10-15 +--- + +
+

Gitea Mirror Architecture

+

This document provides a comprehensive overview of the Gitea Mirror application architecture, including component diagrams, project structure, and detailed explanations of each part of the system.

+
+ +## System Overview + +
+

Gitea Mirror is a web application that automates the mirroring of GitHub repositories to Gitea instances. It provides a user-friendly interface for configuring, monitoring, and managing mirroring operations without requiring users to edit configuration files or run Docker commands.

+
+ +The application is built using: + +- Astro: Web framework for the frontend +- React: Component library for interactive UI elements +- Shadcn UI: UI component library built on Tailwind CSS +- SQLite: Database for storing configuration and state +- Node.js: Runtime environment for the backend + +## Architecture Diagram + +```mermaid +graph TD + subgraph "Gitea Mirror" + Frontend["Frontend
(Astro)"] + Backend["Backend
(Node.js)"] + Database["Database
(SQLite)"] + + Frontend <--> Backend + Backend <--> Database + end + + subgraph "External APIs" + GitHub["GitHub API"] + Gitea["Gitea API"] + end + + Backend --> GitHub + Backend --> Gitea +``` + +## Component Breakdown + +### Frontend (Astro + React) + +The frontend is built with Astro, a modern web framework that allows for server-side rendering and partial hydration. React components are used for interactive elements, providing a responsive and dynamic user interface. + +Key frontend components: + +- **Dashboard**: Overview of mirroring status and recent activity +- **Repository Management**: Interface for managing repositories to mirror +- **Organization Management**: Interface for managing GitHub organizations +- **Configuration**: Settings for GitHub and Gitea connections +- **Activity Log**: Detailed log of mirroring operations + +### Backend (Node.js) + +The backend is built with Node.js and provides API endpoints for the frontend to interact with. It handles: + +- Authentication and user management +- GitHub API integration +- Gitea API integration +- Mirroring operations +- Database interactions + +### Database (SQLite) + +SQLite is used for data persistence, storing: + +- User accounts and authentication data +- GitHub and Gitea configuration +- Repository and organization information +- Mirroring job history and status + +## Data Flow + +1. **User Authentication**: Users authenticate through the frontend, which communicates with the backend to validate credentials. +2. **Configuration**: Users configure GitHub and Gitea settings through the UI, which are stored in the SQLite database. +3. **Repository Discovery**: The backend queries the GitHub API to discover repositories based on user configuration. +4. **Mirroring Process**: When triggered, the backend fetches repository data from GitHub and pushes it to Gitea. +5. **Status Tracking**: All operations are logged in the database and displayed in the Activity Log. + +## Project Structure + +``` +gitea-mirror/ +├── src/ # Source code +│ ├── components/ # React components +│ ├── layouts/ # Astro layout components +│ ├── lib/ # Utility functions and database +│ ├── pages/ # Astro pages and API routes +│ └── styles/ # CSS and Tailwind styles +├── public/ # Static assets +├── data/ # Database and persistent data +└── docker/ # Docker configuration +``` diff --git a/src/content/docs/configuration.md b/src/content/docs/configuration.md new file mode 100644 index 0000000..381ff41 --- /dev/null +++ b/src/content/docs/configuration.md @@ -0,0 +1,120 @@ +--- +title: "Configuration" +description: "Guide to configuring Gitea Mirror for your environment." +order: 2 +updatedDate: 2023-10-15 +--- + +
+

Gitea Mirror Configuration Guide

+

This guide provides detailed information on how to configure Gitea Mirror for your environment.

+
+ +## Configuration Methods + +Gitea Mirror can be configured using: + +1. Environment Variables: Set configuration options through environment variables +2. Web UI: Configure the application through the web interface after installation + +## Environment Variables + +The following environment variables can be used to configure Gitea Mirror: + +| Variable | Description | Default Value | Example | +|----------|-------------|---------------|---------| +| `NODE_ENV` | Node environment (development, production, test) | `development` | `production` | +| `DATABASE_URL` | SQLite database URL | `sqlite://data/gitea-mirror.db` | `sqlite://path/to/your/database.db` | +| `JWT_SECRET` | Secret key for JWT authentication | `your-secret-key-change-this-in-production` | `your-secure-random-string` | +| `HOST` | Server host | `localhost` | `0.0.0.0` | +| `PORT` | Server port | `3000` | `8080` | + +### Important Security Note + +In production environments, you should always set a strong, unique `JWT_SECRET` to ensure secure authentication. + +## Web UI Configuration + +After installing and starting Gitea Mirror, you can configure it through the web interface: + +1. Navigate to `http://your-server:port/` +2. If this is your first time, you'll be guided through creating an admin account +3. Log in with your credentials +4. Go to the Configuration page + +### GitHub Configuration + +The GitHub configuration section allows you to connect to GitHub and specify which repositories to mirror. + +| Option | Description | Default | +|--------|-------------|---------| +| Username | Your GitHub username | - | +| Token | GitHub personal access token | - | +| Skip Forks | Skip forked repositories | `false` | +| Private Repositories | Include private repositories | `false` | +| Mirror Issues | Mirror issues from GitHub to Gitea | `false` | +| Mirror Starred | Mirror starred repositories | `false` | +| Mirror Organizations | Mirror organization repositories | `false` | +| Only Mirror Orgs | Only mirror organization repositories | `false` | +| Preserve Org Structure | Maintain organization structure in Gitea | `false` | +| Skip Starred Issues | Skip mirroring issues for starred repositories | `false` | + +#### GitHub Token Permissions + +Your GitHub token needs the following permissions: + +- `repo` - Full control of private repositories +- `read:org` - Read organization membership +- `read:user` - Read user profile data + +To create a GitHub token: + +1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) +2. Click "Generate new token" +3. Select the required permissions +4. Copy the generated token and paste it into Gitea Mirror + +### Gitea Configuration + +The Gitea configuration section allows you to connect to your Gitea instance and specify how repositories should be mirrored. + +| Option | Description | Default | +|--------|-------------|---------| +| URL | Gitea server URL | - | +| Token | Gitea access token | - | +| Organization | Default organization for mirrored repositories | - | +| Visibility | Default visibility for mirrored repositories | `public` | +| Starred Repos Org | Organization for starred repositories | `github` | + +#### Gitea Token Creation + +To create a Gitea access token: + +1. Log in to your Gitea instance +2. Go to Settings > Applications +3. Under "Generate New Token", enter a name for your token +4. Click "Generate Token" +5. Copy the generated token and paste it into Gitea Mirror + +### Schedule Configuration + +You can configure automatic mirroring on a schedule: + +| Option | Description | Default | +|--------|-------------|---------| +| Enable Scheduling | Enable automatic mirroring | `false` | +| Interval (seconds) | Time between mirroring operations | `3600` (1 hour) | + +## Advanced Configuration + +### Repository Filtering + +You can include or exclude specific repositories using patterns: + +- Include patterns: Only repositories matching these patterns will be mirrored +- Exclude patterns: Repositories matching these patterns will be skipped + +Example patterns: +- `*` - All repositories +- `org-name/*` - All repositories in a specific organization +- `username/repo-name` - A specific repository diff --git a/src/content/docs/quickstart.md b/src/content/docs/quickstart.md new file mode 100644 index 0000000..8efa109 --- /dev/null +++ b/src/content/docs/quickstart.md @@ -0,0 +1,127 @@ +--- +title: "Quick Start Guide" +description: "Get started with Gitea Mirror quickly." +order: 3 +updatedDate: 2023-10-15 +--- + +
+

Gitea Mirror Quick Start Guide

+

This guide will help you get Gitea Mirror up and running quickly.

+
+ +## Prerequisites + +Before you begin, make sure you have: + +1. A GitHub account with a personal access token +2. A Gitea instance with an access token +3. Docker and docker-compose (recommended) or Node.js 18+ installed + +## Installation Options + +Choose the installation method that works best for your environment. + +### Using Docker (Recommended) + +Docker provides the easiest way to get started with minimal configuration. + +1. Clone the repository: + ```bash + git clone https://github.com/arunavo4/gitea-mirror.git + cd gitea-mirror + ``` + +2. Start the application in production mode: + ```bash + docker-compose --profile production up -d + ``` + +3. Access the application at [http://localhost:4321](http://localhost:4321) + +### Manual Installation + +If you prefer to run the application directly on your system: + +1. Clone the repository: + ```bash + git clone https://github.com/arunavo4/gitea-mirror.git + cd gitea-mirror + ``` + +2. Run the quick setup script: + ```bash + pnpm setup + ``` + This installs dependencies and initializes the database. + +3. Choose how to run the application: + + **Development Mode:** + ```bash + pnpm dev + ``` + + **Production Mode:** + ```bash + pnpm build + pnpm start + ``` + +4. Access the application at [http://localhost:4321](http://localhost:4321) + +## Initial Configuration + +Follow these steps to configure Gitea Mirror for first use: + +1. **Create Admin Account** + - Upon first access, you'll be prompted to create an admin account + - Choose a secure username and password + - This will be your administrator account + +2. **Configure GitHub Connection** + - Navigate to the Configuration page + - Enter your GitHub username + - Enter your GitHub personal access token + - Select which repositories to mirror (all, starred, organizations) + - Configure repository filtering options + +3. **Configure Gitea Connection** + - Enter your Gitea server URL + - Enter your Gitea access token + - Configure organization and visibility settings + +4. **Set Up Scheduling (Optional)** + - Enable automatic mirroring if desired + - Set the mirroring interval (in seconds) + +5. **Save Configuration** + - Click the "Save" button to store your settings + +## Performing Your First Mirror + +After completing the configuration, you can start mirroring repositories: + +1. Click "Import GitHub Data" to fetch repositories from GitHub +2. Go to the Repositories page to view your imported repositories +3. Select the repositories you want to mirror +4. Click "Mirror Selected" to start the mirroring process +5. Monitor the progress on the Activity page +6. You'll receive toast notifications about the success or failure of operations + +## Troubleshooting + +If you encounter any issues: + +- Check the Activity Log for detailed error messages +- Verify your GitHub and Gitea tokens have the correct permissions +- Ensure your Gitea instance is accessible from the machine running Gitea Mirror +- For Docker installations, check container logs with `docker logs gitea-mirror` + +## Next Steps + +After your initial setup: + +- Explore the dashboard for an overview of your mirroring status +- Set up automatic mirroring schedules for hands-off operation +- Configure organization mirroring for team repositories diff --git a/src/data/Sidebar.ts b/src/data/Sidebar.ts new file mode 100644 index 0000000..2e1bee4 --- /dev/null +++ b/src/data/Sidebar.ts @@ -0,0 +1,16 @@ +import { + LayoutDashboard, + GitFork, + Settings, + Activity, + Building2, +} from "lucide-react"; +import type { SidebarItem } from "@/types/Sidebar"; + +export const links: SidebarItem[] = [ + { href: "/", label: "Dashboard", icon: LayoutDashboard }, + { href: "/repositories", label: "Repositories", icon: GitFork }, + { href: "/organizations", label: "Organizations", icon: Building2 }, + { href: "/config", label: "Configuration", icon: Settings }, + { href: "/activity", label: "Activity Log", icon: Activity }, +]; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..9c0eb8d --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,150 @@ +import * as React from "react"; +import { + useState, + useEffect, + createContext, + useContext, + type Context, +} from "react"; +import { authApi } from "@/lib/api"; +import type { ExtendedUser } from "@/types/user"; + +interface AuthContextType { + user: ExtendedUser | null; + isLoading: boolean; + error: string | null; + login: (username: string, password: string) => Promise; + register: ( + username: string, + email: string, + password: string + ) => Promise; + logout: () => Promise; + refreshUser: () => Promise; // Added refreshUser function +} + +const AuthContext: Context = createContext< + AuthContextType | undefined +>(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Function to refetch the user data + const refreshUser = async () => { + // not using loading state to keep the ui seamless and refresh the data in bg + // setIsLoading(true); + try { + const user = await authApi.getCurrentUser(); + + console.log("User data refreshed:", user); + + setUser(user); + } catch (err: any) { + setUser(null); + console.error("Failed to refresh user data", err); + } finally { + // setIsLoading(false); + } + }; + + // Automatically check the user status when the app loads + useEffect(() => { + const checkAuth = async () => { + try { + const user = await authApi.getCurrentUser(); + + console.log("User data fetched:", user); + + setUser(user); + } catch (err: any) { + setUser(null); + + // Redirect user based on error + if (err?.message === "No users found") { + window.location.href = "/signup"; + } else { + window.location.href = "/login"; + } + console.error("Auth check failed", err); + } finally { + setIsLoading(false); + } + }; + + checkAuth(); + }, []); + + const login = async (username: string, password: string) => { + setIsLoading(true); + setError(null); + try { + const user = await authApi.login(username, password); + setUser(user); + } catch (err) { + setError(err instanceof Error ? err.message : "Login failed"); + throw err; + } finally { + setIsLoading(false); + } + }; + + const register = async ( + username: string, + email: string, + password: string + ) => { + setIsLoading(true); + setError(null); + try { + const user = await authApi.register(username, email, password); + setUser(user); + } catch (err) { + setError(err instanceof Error ? err.message : "Registration failed"); + throw err; + } finally { + setIsLoading(false); + } + }; + + const logout = async () => { + setIsLoading(true); + try { + await authApi.logout(); + setUser(null); + window.location.href = "/login"; + } catch (err) { + console.error("Logout error:", err); + } finally { + setIsLoading(false); + } + }; + + // Create the context value with the added refreshUser function + const contextValue = { + user, + isLoading, + error, + login, + register, + logout, + refreshUser, + }; + + // Return the provider with the context value + return React.createElement( + AuthContext.Provider, + { value: contextValue }, + children + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/src/hooks/useFilterParams.ts b/src/hooks/useFilterParams.ts new file mode 100644 index 0000000..b61de00 --- /dev/null +++ b/src/hooks/useFilterParams.ts @@ -0,0 +1,59 @@ +import { useState, useEffect } from "react"; +import type { FilterParams } from "@/types/filter"; + +const FILTER_KEYS: (keyof FilterParams)[] = [ + "searchTerm", + "status", + "membershipRole", + "owner", + "organization", + "type", + "name", +]; + +export const useFilterParams = ( + defaultFilters: FilterParams, + debounceDelay = 300 +) => { + const getInitialFilter = (): FilterParams => { + if (typeof window === "undefined") return defaultFilters; + + const params = new URLSearchParams(window.location.search); + const result: FilterParams = { ...defaultFilters }; + + FILTER_KEYS.forEach((key) => { + const value = params.get(key); + if (value !== null) { + (result as any)[key] = value; + } + }); + + return result; + }; + + const [filter, setFilter] = useState(() => getInitialFilter()); + + // Debounced URL update + useEffect(() => { + const handler = setTimeout(() => { + const params = new URLSearchParams(); + + FILTER_KEYS.forEach((key) => { + const value = filter[key]; + if (value) { + params.set(key, String(value)); + } + }); + + const newUrl = `${window.location.pathname}?${params.toString()}`; + window.history.replaceState({}, "", newUrl); + }, debounceDelay); + + return () => clearTimeout(handler); // Cleanup on unmount or when `filter` changes + }, [filter, debounceDelay]); + + return { + filter, + setFilter, + }; +}; diff --git a/src/hooks/useMirror.ts b/src/hooks/useMirror.ts new file mode 100644 index 0000000..b0a8baa --- /dev/null +++ b/src/hooks/useMirror.ts @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import { mirrorApi } from '@/lib/api'; +import type { MirrorJob } from '@/lib/db/schema'; + +export function useMirror() { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [currentJob, setCurrentJob] = useState(null); + const [jobs, setJobs] = useState([]); + + const startMirror = async (configId: string, repositoryIds?: string[]) => { + setIsLoading(true); + setError(null); + try { + const job = await mirrorApi.startMirror(configId, repositoryIds); + setCurrentJob(job); + return job; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to start mirroring'); + throw err; + } finally { + setIsLoading(false); + } + }; + + const getMirrorJobs = async (configId: string) => { + setIsLoading(true); + setError(null); + try { + const fetchedJobs = await mirrorApi.getMirrorJobs(configId); + setJobs(fetchedJobs); + return fetchedJobs; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch mirror jobs'); + throw err; + } finally { + setIsLoading(false); + } + }; + + const getMirrorJob = async (jobId: string) => { + setIsLoading(true); + setError(null); + try { + const job = await mirrorApi.getMirrorJob(jobId); + setCurrentJob(job); + return job; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch mirror job'); + throw err; + } finally { + setIsLoading(false); + } + }; + + const cancelMirrorJob = async (jobId: string) => { + setIsLoading(true); + setError(null); + try { + const result = await mirrorApi.cancelMirrorJob(jobId); + if (result.success && currentJob?.id === jobId) { + setCurrentJob({ ...currentJob, status: 'failed' }); + } + return result; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to cancel mirror job'); + throw err; + } finally { + setIsLoading(false); + } + }; + + return { + isLoading, + error, + currentJob, + jobs, + startMirror, + getMirrorJobs, + getMirrorJob, + cancelMirrorJob, + }; +} diff --git a/src/hooks/useSEE.ts b/src/hooks/useSEE.ts new file mode 100644 index 0000000..314b6ce --- /dev/null +++ b/src/hooks/useSEE.ts @@ -0,0 +1,54 @@ +import { useEffect, useState, useRef } from "react"; +import type { MirrorJob } from "@/lib/db/schema"; + +interface UseSSEOptions { + userId?: string; + onMessage: (data: MirrorJob) => void; +} + +export const useSSE = ({ userId, onMessage }: UseSSEOptions) => { + const [connected, setConnected] = useState(false); + const onMessageRef = useRef(onMessage); + + // Update the ref when onMessage changes + useEffect(() => { + onMessageRef.current = onMessage; + }, [onMessage]); + + useEffect(() => { + if (!userId) return; + + const eventSource = new EventSource(`/api/sse?userId=${userId}`); + + const handleMessage = (event: MessageEvent) => { + try { + const parsedMessage: MirrorJob = JSON.parse(event.data); + + // console.log("Received new log:", parsedMessage); + + onMessageRef.current(parsedMessage); // Use ref instead of prop directly + } catch (error) { + console.error("Error parsing message:", error); + } + }; + + eventSource.onmessage = handleMessage; + + eventSource.onopen = () => { + setConnected(true); + console.log(`Connected to SSE for user: ${userId}`); + }; + + eventSource.onerror = () => { + console.error("SSE connection error"); + setConnected(false); + eventSource.close(); + }; + + return () => { + eventSource.close(); + }; + }, [userId]); // Only depends on userId now + + return { connected }; +}; diff --git a/src/hooks/useSyncRepo.ts b/src/hooks/useSyncRepo.ts new file mode 100644 index 0000000..7d78a2f --- /dev/null +++ b/src/hooks/useSyncRepo.ts @@ -0,0 +1,102 @@ +import { useEffect, useRef } from "react"; +import { useAuth } from "./useAuth"; + +interface UseRepoSyncOptions { + userId?: string; + enabled?: boolean; + interval?: number; + lastSync?: Date | null; + nextSync?: Date | null; +} + +export function useRepoSync({ + userId, + enabled = true, + interval = 3600, + lastSync, + nextSync, +}: UseRepoSyncOptions) { + const intervalRef = useRef(null); + const { refreshUser } = useAuth(); + + useEffect(() => { + if (!enabled || !userId) { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + return; + } + + // Helper to convert possible nextSync types to Date + const getNextSyncDate = () => { + if (!nextSync) return null; + if (nextSync instanceof Date) return nextSync; + return new Date(nextSync); // Handles strings and numbers + }; + + const getLastSyncDate = () => { + if (!lastSync) return null; + if (lastSync instanceof Date) return lastSync; + return new Date(lastSync); + }; + + const isTimeToSync = () => { + const nextSyncDate = getNextSyncDate(); + if (!nextSyncDate) return true; // No nextSync means sync immediately + + const currentTime = new Date(); + return currentTime >= nextSyncDate; + }; + + const sync = async () => { + try { + console.log("Attempting to sync..."); + const response = await fetch("/api/job/schedule-sync-repo", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userId }), + }); + + if (!response.ok) { + console.error("Sync failed:", await response.text()); + return; + } + + await refreshUser(); // refresh user data to get latest sync times. this can be taken from the schedule-sync-repo response but might not be reliable in cases of errors + + const result = await response.json(); + console.log("Sync successful:", result); + return result; + } catch (error) { + console.error("Sync failed:", error); + } + }; + + // Check if sync is overdue when the component mounts or interval passes + if (isTimeToSync()) { + sync(); + } + + // Periodically check if it's time to sync + intervalRef.current = setInterval(() => { + if (isTimeToSync()) { + sync(); + } + }, interval * 1000); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [ + enabled, + interval, + userId, + nextSync instanceof Date ? nextSync.getTime() : nextSync, + lastSync instanceof Date ? lastSync.getTime() : lastSync, + ]); +} diff --git a/src/layouts/main.astro b/src/layouts/main.astro new file mode 100644 index 0000000..f50e901 --- /dev/null +++ b/src/layouts/main.astro @@ -0,0 +1,21 @@ +--- +import '../styles/global.css'; +import '../styles/docs.css'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; + +// Accept title as a prop with a default value +const { title = 'Gitea Mirror' } = Astro.props; +--- + + + + + + + {title} + + + + + + diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..eb27948 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,90 @@ +// Base API URL +const API_BASE = "/api"; + +// Helper function for API requests +async function apiRequest( + endpoint: string, + options: RequestInit = {} +): Promise { + const url = `${API_BASE}${endpoint}`; + const headers = { + "Content-Type": "application/json", + ...options.headers, + }; + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: "An unknown error occurred", + })); + throw new Error(error.message || "An unknown error occurred"); + } + + return response.json(); +} + +// Auth API +export const authApi = { + login: async (username: string, password: string) => { + const res = await fetch(`${API_BASE}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", // Send cookies + body: JSON.stringify({ username, password }), + }); + + if (!res.ok) throw new Error("Login failed"); + return await res.json(); // returns user + }, + + register: async (username: string, email: string, password: string) => { + const res = await fetch(`${API_BASE}/auth/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ username, email, password }), + }); + + if (!res.ok) throw new Error("Registration failed"); + return await res.json(); // returns user + }, + + getCurrentUser: async () => { + const res = await fetch(`${API_BASE}/auth`, { + method: "GET", + credentials: "include", // Send cookies + }); + + if (!res.ok) throw new Error("Not authenticated"); + return await res.json(); + }, + + logout: async () => { + await fetch(`${API_BASE}/auth/logout`, { + method: "POST", + credentials: "include", + }); + }, +}; + +// GitHub API +export const githubApi = { + testConnection: (token: string) => + apiRequest<{ success: boolean }>("/github/test-connection", { + method: "POST", + body: JSON.stringify({ token }), + }), +}; + +// Gitea API +export const giteaApi = { + testConnection: (url: string, token: string) => + apiRequest<{ success: boolean }>("/gitea/test-connection", { + method: "POST", + body: JSON.stringify({ url, token }), + }), +}; diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..58618c1 --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,28 @@ +/** + * Application configuration + */ + +// Environment variables +export const ENV = { + // Node environment (development, production, test) + NODE_ENV: process.env.NODE_ENV || "development", + + // Database URL - use SQLite by default + get DATABASE_URL() { + // If explicitly set, use the provided DATABASE_URL + if (process.env.DATABASE_URL) { + return process.env.DATABASE_URL; + } + + // Otherwise, use the default database + return "sqlite://data/gitea-mirror.db"; + }, + + // JWT secret for authentication + JWT_SECRET: + process.env.JWT_SECRET || "your-secret-key-change-this-in-production", + + // Server host and port + HOST: process.env.HOST || "localhost", + PORT: parseInt(process.env.PORT || "3000", 10), +}; diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts new file mode 100644 index 0000000..0ae592a --- /dev/null +++ b/src/lib/db/index.ts @@ -0,0 +1,177 @@ +import { z } from "zod"; +import { createClient } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; + +import path from "path"; +import { configSchema } from "./schema"; + +// Define the database URL - for development we'll use a local SQLite file +const dataDir = path.join(process.cwd(), "data"); +const dbUrl = + process.env.DATABASE_URL || `file:${path.join(dataDir, "gitea-mirror.db")}`; + +// Create a client connection to the database +export const client = createClient({ url: dbUrl }); + +// Create a drizzle instance +export const db = drizzle(client); + +// Define the tables +export const users = sqliteTable("users", { + id: text("id").primaryKey(), + username: text("username").notNull(), + password: text("password").notNull(), + email: text("email").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), +}); + +const githubSchema = configSchema.shape.githubConfig; +const giteaSchema = configSchema.shape.giteaConfig; +const scheduleSchema = configSchema.shape.scheduleConfig; + +export const configs = sqliteTable("configs", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + name: text("name").notNull(), + isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), + + githubConfig: text("github_config", { mode: "json" }) + .$type>() + .notNull(), + + giteaConfig: text("gitea_config", { mode: "json" }) + .$type>() + .notNull(), + + include: text("include", { mode: "json" }) + .$type() + .notNull() + .default(["*"]), + + exclude: text("exclude", { mode: "json" }) + .$type() + .notNull() + .default([]), + + scheduleConfig: text("schedule_config", { mode: "json" }) + .$type>() + .notNull(), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), + + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), +}); + +export const repositories = sqliteTable("repositories", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + configId: text("config_id") + .notNull() + .references(() => configs.id), + name: text("name").notNull(), + fullName: text("full_name").notNull(), + url: text("url").notNull(), + cloneUrl: text("clone_url").notNull(), + owner: text("owner").notNull(), + organization: text("organization"), + mirroredLocation: text("mirrored_location").default(""), + + isPrivate: integer("is_private", { mode: "boolean" }) + .notNull() + .default(false), + isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false), + forkedFrom: text("forked_from"), + + hasIssues: integer("has_issues", { mode: "boolean" }) + .notNull() + .default(false), + isStarred: integer("is_starred", { mode: "boolean" }) + .notNull() + .default(false), + isArchived: integer("is_archived", { mode: "boolean" }) + .notNull() + .default(false), + + size: integer("size").notNull().default(0), + hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false), + hasSubmodules: integer("has_submodules", { mode: "boolean" }) + .notNull() + .default(false), + + defaultBranch: text("default_branch").notNull(), + visibility: text("visibility").notNull().default("public"), + + status: text("status").notNull().default("imported"), + lastMirrored: integer("last_mirrored", { mode: "timestamp" }), + errorMessage: text("error_message"), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), +}); + +export const mirrorJobs = sqliteTable("mirror_jobs", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + repositoryId: text("repository_id"), + repositoryName: text("repository_name"), + organizationId: text("organization_id"), + organizationName: text("organization_name"), + details: text("details"), + status: text("status").notNull().default("imported"), + message: text("message").notNull(), + timestamp: integer("timestamp", { mode: "timestamp" }) + .notNull() + .default(new Date()), +}); + +export const organizations = sqliteTable("organizations", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + configId: text("config_id") + .notNull() + .references(() => configs.id), + name: text("name").notNull(), + + avatarUrl: text("avatar_url").notNull(), + + membershipRole: text("membership_role").notNull().default("member"), + + isIncluded: integer("is_included", { mode: "boolean" }) + .notNull() + .default(true), + + status: text("status").notNull().default("imported"), + lastMirrored: integer("last_mirrored", { mode: "timestamp" }), + errorMessage: text("error_message"), + + repositoryCount: integer("repository_count").notNull().default(0), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(new Date()), +}); diff --git a/src/lib/db/migrations/add-mirrored-location.ts b/src/lib/db/migrations/add-mirrored-location.ts new file mode 100644 index 0000000..a3c61cd --- /dev/null +++ b/src/lib/db/migrations/add-mirrored-location.ts @@ -0,0 +1,27 @@ +import { client } from "@/lib/db"; + +/** + * Migration script to add the mirrored_location column to the repositories table + */ +export async function addMirroredLocationColumn() { + try { + console.log("Starting migration: Adding mirrored_location column to repositories table"); + + // Check if the column already exists + const tableInfo = await client.execute(`PRAGMA table_info(repositories)`); + const columnExists = tableInfo.rows.some((row: any) => row.name === "mirrored_location"); + + if (columnExists) { + console.log("Column mirrored_location already exists, skipping migration"); + return; + } + + // Add the mirrored_location column + await client.execute(`ALTER TABLE repositories ADD COLUMN mirrored_location TEXT DEFAULT ''`); + + console.log("Migration completed successfully: mirrored_location column added"); + } catch (error) { + console.error("Migration failed:", error); + throw error; + } +} diff --git a/src/lib/db/schema.sql b/src/lib/db/schema.sql new file mode 100644 index 0000000..264645b --- /dev/null +++ b/src/lib/db/schema.sql @@ -0,0 +1,75 @@ +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + email TEXT NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL +); + +-- Configurations table +CREATE TABLE IF NOT EXISTS configs ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT 1, + github_config TEXT NOT NULL, + gitea_config TEXT NOT NULL, + schedule_config TEXT NOT NULL, + include TEXT NOT NULL, + exclude TEXT NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +-- Repositories table +CREATE TABLE IF NOT EXISTS repositories ( + id TEXT PRIMARY KEY, + config_id TEXT NOT NULL, + name TEXT NOT NULL, + full_name TEXT NOT NULL, + url TEXT NOT NULL, + is_private BOOLEAN NOT NULL, + is_fork BOOLEAN NOT NULL, + owner TEXT NOT NULL, + organization TEXT, + mirrored_location TEXT DEFAULT '', + has_issues BOOLEAN NOT NULL, + is_starred BOOLEAN NOT NULL, + status TEXT NOT NULL, + error_message TEXT, + last_mirrored DATETIME, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE +); + +-- Organizations table +CREATE TABLE IF NOT EXISTS organizations ( + id TEXT PRIMARY KEY, + config_id TEXT NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL, + is_included BOOLEAN NOT NULL, + repository_count INTEGER NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE +); + +-- Mirror jobs table +CREATE TABLE IF NOT EXISTS mirror_jobs ( + id TEXT PRIMARY KEY, + config_id TEXT NOT NULL, + repository_id TEXT, + status TEXT NOT NULL, + started_at DATETIME NOT NULL, + completed_at DATETIME, + log TEXT NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE, + FOREIGN KEY (repository_id) REFERENCES repositories (id) ON DELETE SET NULL +); diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts new file mode 100644 index 0000000..dec34aa --- /dev/null +++ b/src/lib/db/schema.ts @@ -0,0 +1,142 @@ +import { z } from "zod"; +import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; +import { membershipRoleEnum } from "@/types/organizations"; + +// User schema +export const userSchema = z.object({ + id: z.string().uuid().optional(), + username: z.string().min(3), + password: z.string().min(8).optional(), // Hashed password + email: z.string().email(), + createdAt: z.date().default(() => new Date()), + updatedAt: z.date().default(() => new Date()), +}); + +export type User = z.infer; + +// Configuration schema +export const configSchema = z.object({ + id: z.string().uuid().optional(), + userId: z.string().uuid(), + name: z.string().min(1), + isActive: z.boolean().default(true), + githubConfig: z.object({ + username: z.string().min(1), + token: z.string().optional(), + skipForks: z.boolean().default(false), + privateRepositories: z.boolean().default(false), + mirrorIssues: z.boolean().default(false), + mirrorStarred: z.boolean().default(false), + useSpecificUser: z.boolean().default(false), + singleRepo: z.string().optional(), + includeOrgs: z.array(z.string()).default([]), + excludeOrgs: z.array(z.string()).default([]), + mirrorPublicOrgs: z.boolean().default(false), + publicOrgs: z.array(z.string()).default([]), + preserveOrgStructure: z.boolean().default(false), + skipStarredIssues: z.boolean().default(false), + }), + giteaConfig: z.object({ + username: z.string().min(1), + url: z.string().url(), + token: z.string().min(1), + organization: z.string().optional(), + visibility: z.enum(["public", "private", "limited"]).default("public"), + starredReposOrg: z.string().default("github"), + }), + include: z.array(z.string()).default(["*"]), + exclude: z.array(z.string()).default([]), + scheduleConfig: z.object({ + enabled: z.boolean().default(false), + interval: z.number().min(1).default(3600), // in seconds + lastRun: z.date().optional(), + nextRun: z.date().optional(), + }), + createdAt: z.date().default(() => new Date()), + updatedAt: z.date().default(() => new Date()), +}); + +export type Config = z.infer; + +// Repository schema +export const repositorySchema = z.object({ + id: z.string().uuid().optional(), + userId: z.string().uuid().optional(), + configId: z.string().uuid(), + + name: z.string().min(1), + fullName: z.string().min(1), + url: z.string().url(), + cloneUrl: z.string().url(), + + owner: z.string().min(1), + organization: z.string().optional(), + + isPrivate: z.boolean().default(false), + isForked: z.boolean().default(false), + forkedFrom: z.string().optional(), + + hasIssues: z.boolean().default(false), + isStarred: z.boolean().default(false), + isArchived: z.boolean().default(false), + + size: z.number(), + hasLFS: z.boolean().default(false), + hasSubmodules: z.boolean().default(false), + + defaultBranch: z.string(), + visibility: repositoryVisibilityEnum.default("public"), + + status: repoStatusEnum.default("imported"), + lastMirrored: z.date().optional(), + errorMessage: z.string().optional(), + + mirroredLocation: z.string().default(""), // Store the full Gitea path where repo was mirrored + + createdAt: z.date().default(() => new Date()), + updatedAt: z.date().default(() => new Date()), +}); + +export type Repository = z.infer; + +// Mirror job schema +export const mirrorJobSchema = z.object({ + id: z.string().uuid().optional(), + userId: z.string().uuid().optional(), + repositoryId: z.string().uuid().optional(), + repositoryName: z.string().optional(), + organizationId: z.string().uuid().optional(), + organizationName: z.string().optional(), + details: z.string().optional(), + status: repoStatusEnum.default("imported"), + message: z.string(), + timestamp: z.date().default(() => new Date()), +}); + +export type MirrorJob = z.infer; + +// Organization schema +export const organizationSchema = z.object({ + id: z.string().uuid().optional(), + userId: z.string().uuid().optional(), + configId: z.string().uuid(), + + avatarUrl: z.string().url(), + + name: z.string().min(1), + + membershipRole: membershipRoleEnum.default("member"), + + isIncluded: z.boolean().default(false), + + status: repoStatusEnum.default("imported"), + lastMirrored: z.date().optional(), + errorMessage: z.string().optional(), + + repositoryCount: z.number().default(0), + + createdAt: z.date().default(() => new Date()), + updatedAt: z.date().default(() => new Date()), +}); + +export type Organization = z.infer; diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts new file mode 100644 index 0000000..cf5a714 --- /dev/null +++ b/src/lib/gitea.ts @@ -0,0 +1,962 @@ +import { + repoStatusEnum, + type RepositoryVisibility, + type RepoStatus, +} from "@/types/Repository"; +import { Octokit } from "@octokit/rest"; +import type { Config } from "@/types/config"; +import type { Organization, Repository } from "./db/schema"; +import superagent from "superagent"; +import { createMirrorJob } from "./helpers"; +import { db, organizations, repositories } from "./db"; +import { eq } from "drizzle-orm"; + +export const getGiteaRepoOwner = ({ + config, + repository, +}: { + config: Partial; + repository: Repository; +}): string => { + if (!config.githubConfig || !config.giteaConfig) { + throw new Error("GitHub or Gitea config is required."); + } + + if (!config.giteaConfig.username) { + throw new Error("Gitea username is required."); + } + + // if the config has preserveOrgStructure set to true, then use the org name as the owner + if (config.githubConfig.preserveOrgStructure && repository.organization) { + return repository.organization; + } + + // if the config has preserveOrgStructure set to false, then use the gitea username as the owner + return config.giteaConfig.username; +}; + +export const isRepoPresentInGitea = async ({ + config, + owner, + repoName, +}: { + config: Partial; + owner: string; + repoName: string; +}): Promise => { + try { + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + throw new Error("Gitea config is required."); + } + + // Check if the repository exists at the specified owner location + const response = await fetch( + `${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`, + { + headers: { + Authorization: `token ${config.giteaConfig.token}`, + }, + } + ); + + return response.ok; + } catch (error) { + console.error("Error checking if repo exists in Gitea:", error); + return false; + } +}; + +/** + * Helper function to check if a repository exists in Gitea. + * First checks the recorded mirroredLocation, then falls back to the expected location. + */ +export const checkRepoLocation = async ({ + config, + repository, + expectedOwner, +}: { + config: Partial; + repository: Repository; + expectedOwner: string; +}): Promise<{ present: boolean; actualOwner: string }> => { + // First check if we have a recorded mirroredLocation and if the repo exists there + if (repository.mirroredLocation && repository.mirroredLocation.trim() !== "") { + const [mirroredOwner] = repository.mirroredLocation.split('/'); + if (mirroredOwner) { + const mirroredPresent = await isRepoPresentInGitea({ + config, + owner: mirroredOwner, + repoName: repository.name, + }); + + if (mirroredPresent) { + console.log(`Repository found at recorded mirrored location: ${repository.mirroredLocation}`); + return { present: true, actualOwner: mirroredOwner }; + } + } + } + + // If not found at the recorded location, check the expected location + const present = await isRepoPresentInGitea({ + config, + owner: expectedOwner, + repoName: repository.name, + }); + + if (present) { + return { present: true, actualOwner: expectedOwner }; + } + + // Repository not found at any location + return { present: false, actualOwner: expectedOwner }; +}; + +export const mirrorGithubRepoToGitea = async ({ + octokit, + repository, + config, +}: { + octokit: Octokit; + repository: Repository; + config: Partial; +}): Promise => { + try { + if (!config.userId || !config.githubConfig || !config.giteaConfig) { + throw new Error("github config and gitea config are required."); + } + + if (!config.giteaConfig.username) { + throw new Error("Gitea username is required."); + } + + const isExisting = await isRepoPresentInGitea({ + config, + owner: config.giteaConfig.username, + repoName: repository.name, + }); + + if (isExisting) { + console.log( + `Repository ${repository.name} already exists in Gitea. Skipping migration.` + ); + return; + } + + console.log(`Mirroring repository ${repository.name}`); + + // Mark repos as "mirroring" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("mirroring"), + updatedAt: new Date(), + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for "mirroring" status + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Started mirroring repository: ${repository.name}`, + details: `Repository ${repository.name} is now in the mirroring state.`, + status: "mirroring", + }); + + let cloneAddress = repository.cloneUrl; + + // If the repository is private, inject the GitHub token into the clone URL + if (repository.isPrivate) { + if (!config.githubConfig.token) { + throw new Error( + "GitHub token is required to mirror private repositories." + ); + } + + cloneAddress = repository.cloneUrl.replace( + "https://", + `https://${config.githubConfig.token}@` + ); + } + + const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`; + + const response = await superagent + .post(apiUrl) + .set("Authorization", `token ${config.giteaConfig.token}`) + .set("Content-Type", "application/json") + .send({ + clone_addr: cloneAddress, + repo_name: repository.name, + mirror: true, + private: repository.isPrivate, + repo_owner: config.giteaConfig.username, + description: "", + service: "git", + }); + + // clone issues + if (config.githubConfig.mirrorIssues) { + await mirrorGitRepoIssuesToGitea({ + config, + octokit, + repository, + isRepoInOrg: false, + }); + } + + console.log(`Repository ${repository.name} mirrored successfully`); + + // Mark repos as "mirrored" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("mirrored"), + updatedAt: new Date(), + lastMirrored: new Date(), + errorMessage: null, + mirroredLocation: `${config.giteaConfig.username}/${repository.name}`, + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for "mirrored" status + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Successfully mirrored repository: ${repository.name}`, + details: `Repository ${repository.name} was mirrored to Gitea.`, + status: "mirrored", + }); + + return response.body; + } catch (error) { + console.error( + `Error while mirroring repository ${repository.name}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + + // Mark repos as "failed" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("failed"), + updatedAt: new Date(), + errorMessage: error instanceof Error ? error.message : "Unknown error", + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for failure + await createMirrorJob({ + userId: config.userId ?? "", // userId is going to be there anyways + repositoryId: repository.id, + repositoryName: repository.name, + message: `Failed to mirror repository: ${repository.name}`, + details: `Repository ${repository.name} failed to mirror. Error: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + status: "failed", + }); + if (error instanceof Error) { + throw new Error(`Failed to mirror repository: ${error.message}`); + } + throw new Error("Failed to mirror repository: An unknown error occurred."); + } +}; + +export async function getOrCreateGiteaOrg({ + orgName, + orgId, + config, +}: { + orgId?: string; //db id + orgName: string; + config: Partial; +}): Promise { + if ( + !config.giteaConfig?.url || + !config.giteaConfig?.token || + !config.userId + ) { + throw new Error("Gitea config is required."); + } + + try { + const orgRes = await fetch( + `${config.giteaConfig.url}/api/v1/orgs/${orgName}`, + { + headers: { + Authorization: `token ${config.giteaConfig.token}`, + "Content-Type": "application/json", + }, + } + ); + + if (orgRes.ok) { + const org = await orgRes.json(); + + await createMirrorJob({ + userId: config.userId, + organizationId: org.id, + organizationName: orgName, + status: "imported", + message: `Organization ${orgName} fetched successfully`, + details: `Organization ${orgName} was fetched from GitHub`, + }); + return org.id; + } + + const createRes = await fetch(`${config.giteaConfig.url}/api/v1/orgs`, { + method: "POST", + headers: { + Authorization: `token ${config.giteaConfig.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: orgName, + full_name: `${orgName} Org`, + description: `Mirrored organization from GitHub ${orgName}`, + visibility: "public", + }), + }); + + if (!createRes.ok) { + throw new Error(`Failed to create Gitea org: ${await createRes.text()}`); + } + + await createMirrorJob({ + userId: config.userId, + organizationName: orgName, + status: "imported", + message: `Organization ${orgName} created successfully`, + details: `Organization ${orgName} was created in Gitea`, + }); + + const newOrg = await createRes.json(); + return newOrg.id; + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : "Unknown error occurred in getOrCreateGiteaOrg."; + + await createMirrorJob({ + userId: config.userId, + organizationId: orgId, + organizationName: orgName, + message: `Failed to create or fetch Gitea organization: ${orgName}`, + status: "failed", + details: `Error: ${errorMessage}`, + }); + + throw new Error(`Error in getOrCreateGiteaOrg: ${errorMessage}`); + } +} + +export async function mirrorGitHubRepoToGiteaOrg({ + octokit, + config, + repository, + giteaOrgId, + orgName, +}: { + octokit: Octokit; + config: Partial; + repository: Repository; + giteaOrgId: number; + orgName: string; +}) { + try { + if ( + !config.giteaConfig?.url || + !config.giteaConfig?.token || + !config.userId + ) { + throw new Error("Gitea config is required."); + } + + const isExisting = await isRepoPresentInGitea({ + config, + owner: orgName, + repoName: repository.name, + }); + + if (isExisting) { + console.log( + `Repository ${repository.name} already exists in Gitea. Skipping migration.` + ); + return; + } + + console.log( + `Mirroring repository ${repository.name} to organization ${orgName}` + ); + + let cloneAddress = repository.cloneUrl; + + if (repository.isPrivate) { + if (!config.githubConfig?.token) { + throw new Error( + "GitHub token is required to mirror private repositories." + ); + } + + cloneAddress = repository.cloneUrl.replace( + "https://", + `https://${config.githubConfig.token}@` + ); + } + + // Mark repos as "mirroring" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("mirroring"), + updatedAt: new Date(), + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for "mirroring" status + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Started mirroring repository: ${repository.name}`, + details: `Repository ${repository.name} is now in the mirroring state.`, + status: "mirroring", + }); + + const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`; + + const migrateRes = await superagent + .post(apiUrl) + .set("Authorization", `token ${config.giteaConfig.token}`) + .set("Content-Type", "application/json") + .send({ + clone_addr: cloneAddress, + uid: giteaOrgId, + repo_name: repository.name, + mirror: true, + private: repository.isPrivate, + }); + + // Clone issues + if (config.githubConfig?.mirrorIssues) { + await mirrorGitRepoIssuesToGitea({ + config, + octokit, + repository, + isRepoInOrg: true, + }); + } + + console.log( + `Repository ${repository.name} mirrored successfully to organization ${orgName}` + ); + + // Mark repos as "mirrored" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("mirrored"), + updatedAt: new Date(), + lastMirrored: new Date(), + errorMessage: null, + mirroredLocation: `${orgName}/${repository.name}`, + }) + .where(eq(repositories.id, repository.id!)); + + //create a mirror job + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Repository ${repository.name} mirrored successfully`, + details: `Repository ${repository.name} was mirrored to Gitea`, + status: "mirrored", + }); + + return migrateRes.body; + } catch (error) { + console.error( + `Error while mirroring repository ${repository.name}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + // Mark repos as "failed" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("failed"), + updatedAt: new Date(), + errorMessage: error instanceof Error ? error.message : "Unknown error", + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for failure + await createMirrorJob({ + userId: config.userId || "", // userId is going to be there anyways + repositoryId: repository.id, + repositoryName: repository.name, + message: `Failed to mirror repository: ${repository.name}`, + details: `Repository ${repository.name} failed to mirror. Error: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + status: "failed", + }); + if (error instanceof Error) { + throw new Error(`Failed to mirror repository: ${error.message}`); + } + throw new Error("Failed to mirror repository: An unknown error occurred."); + } +} + +export async function mirrorGitHubOrgRepoToGiteaOrg({ + config, + octokit, + repository, + orgName, +}: { + config: Partial; + octokit: Octokit; + repository: Repository; + orgName: string; +}) { + try { + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + throw new Error("Gitea config is required."); + } + + const giteaOrgId = await getOrCreateGiteaOrg({ + orgName, + config, + }); + + await mirrorGitHubRepoToGiteaOrg({ + octokit, + config, + repository, + giteaOrgId, + orgName, + }); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to mirror repository: ${error.message}`); + } + throw new Error("Failed to mirror repository: An unknown error occurred."); + } +} + +export async function mirrorGitHubOrgToGitea({ + organization, + octokit, + config, +}: { + organization: Organization; + octokit: Octokit; + config: Partial; +}) { + try { + if ( + !config.userId || + !config.id || + !config.githubConfig?.token || + !config.giteaConfig?.url + ) { + throw new Error("Config, GitHub token and Gitea URL are required."); + } + + console.log(`Mirroring organization ${organization.name}`); + + //mark the org as "mirroring" in DB + await db + .update(organizations) + .set({ + isIncluded: true, + status: repoStatusEnum.parse("mirroring"), + updatedAt: new Date(), + }) + .where(eq(organizations.id, organization.id!)); + + // Append log for "mirroring" status + await createMirrorJob({ + userId: config.userId, + organizationId: organization.id, + organizationName: organization.name, + message: `Started mirroring organization: ${organization.name}`, + details: `Organization ${organization.name} is now in the mirroring state.`, + status: repoStatusEnum.parse("mirroring"), + }); + + const giteaOrgId = await getOrCreateGiteaOrg({ + orgId: organization.id, + orgName: organization.name, + config, + }); + + //query the db with the org name and get the repos + const orgRepos = await db + .select() + .from(repositories) + .where(eq(repositories.organization, organization.name)); + + for (const repo of orgRepos) { + await mirrorGitHubRepoToGiteaOrg({ + octokit, + config, + repository: { + ...repo, + status: repo.status as RepoStatus, + visibility: repo.visibility as RepositoryVisibility, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + organization: repo.organization ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + mirroredLocation: repo.mirroredLocation || "", + }, + giteaOrgId, + orgName: organization.name, + }); + } + + console.log(`Organization ${organization.name} mirrored successfully`); + + // Mark org as "mirrored" in DB + await db + .update(organizations) + .set({ + status: repoStatusEnum.parse("mirrored"), + updatedAt: new Date(), + lastMirrored: new Date(), + errorMessage: null, + }) + .where(eq(organizations.id, organization.id!)); + + // Append log for "mirrored" status + await createMirrorJob({ + userId: config.userId, + organizationId: organization.id, + organizationName: organization.name, + message: `Successfully mirrored organization: ${organization.name}`, + details: `Organization ${organization.name} was mirrored to Gitea.`, + status: repoStatusEnum.parse("mirrored"), + }); + } catch (error) { + console.error( + `Error while mirroring organization ${organization.name}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + + // Mark org as "failed" in DB + await db + .update(organizations) + .set({ + status: repoStatusEnum.parse("failed"), + updatedAt: new Date(), + errorMessage: error instanceof Error ? error.message : "Unknown error", + }) + .where(eq(organizations.id, organization.id!)); + + // Append log for failure + await createMirrorJob({ + userId: config.userId || "", // userId is going to be there anyways + organizationId: organization.id, + organizationName: organization.name, + message: `Failed to mirror organization: ${organization.name}`, + details: `Organization ${organization.name} failed to mirror. Error: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + status: repoStatusEnum.parse("failed"), + }); + + if (error instanceof Error) { + throw new Error(`Failed to mirror repository: ${error.message}`); + } + throw new Error("Failed to mirror repository: An unknown error occurred."); + } +} + +export const syncGiteaRepo = async ({ + config, + repository, +}: { + config: Partial; + repository: Repository; +}) => { + try { + if ( + !config.userId || + !config.giteaConfig?.url || + !config.giteaConfig?.token || + !config.giteaConfig?.username + ) { + throw new Error("Gitea config is required."); + } + + console.log(`Syncing repository ${repository.name}`); + + // Mark repo as "syncing" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("syncing"), + updatedAt: new Date(), + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for "syncing" status + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Started syncing repository: ${repository.name}`, + details: `Repository ${repository.name} is now in the syncing state.`, + status: repoStatusEnum.parse("syncing"), + }); + + // Get the expected owner based on current config + const repoOwner = getGiteaRepoOwner({ config, repository }); + + // Check if repo exists at the expected location or alternate location + const { present, actualOwner } = await checkRepoLocation({ + config, + repository, + expectedOwner: repoOwner + }); + + if (!present) { + throw new Error(`Repository ${repository.name} not found in Gitea at any expected location`); + } + + // Use the actual owner where the repo was found + const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`; + + const response = await superagent + .post(apiUrl) + .set("Authorization", `token ${config.giteaConfig.token}`); + + // Mark repo as "synced" in DB + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("synced"), + updatedAt: new Date(), + lastMirrored: new Date(), + errorMessage: null, + mirroredLocation: `${actualOwner}/${repository.name}`, + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for "synced" status + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Successfully synced repository: ${repository.name}`, + details: `Repository ${repository.name} was synced with Gitea.`, + status: repoStatusEnum.parse("synced"), + }); + + console.log(`Repository ${repository.name} synced successfully`); + + return response.body; + } catch (error) { + console.error( + `Error while syncing repository ${repository.name}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + + // Optional: update repo with error status + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("failed"), + updatedAt: new Date(), + errorMessage: (error as Error).message, + }) + .where(eq(repositories.id, repository.id!)); + + // Append log for "error" status + if (config.userId && repository.id && repository.name) { + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Failed to sync repository: ${repository.name}`, + details: (error as Error).message, + status: repoStatusEnum.parse("failed"), + }); + } + + if (error instanceof Error) { + throw new Error(`Failed to sync repository: ${error.message}`); + } + throw new Error("Failed to sync repository: An unknown error occurred."); + } +}; + +export const mirrorGitRepoIssuesToGitea = async ({ + config, + octokit, + repository, + isRepoInOrg, +}: { + config: Partial; + octokit: Octokit; + repository: Repository; + isRepoInOrg: boolean; +}) => { + //things covered here are- issue, title, body, labels, comments and assignees + if ( + !config.githubConfig?.token || + !config.giteaConfig?.token || + !config.giteaConfig?.url || + !config.giteaConfig?.username + ) { + throw new Error("Missing GitHub or Gitea configuration."); + } + + const repoOrigin = isRepoInOrg + ? repository.organization + : config.githubConfig.username; + + const [owner, repo] = repository.fullName.split("/"); + + // Fetch GitHub issues + const issues = await octokit.paginate( + octokit.rest.issues.listForRepo, + { + owner, + repo, + state: "all", + per_page: 100, + }, + (res) => res.data + ); + + console.log(`Mirroring ${issues.length} issues from ${repository.fullName}`); + + // Get existing labels from Gitea + const giteaLabelsRes = await superagent + .get( + `${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels` + ) + .set("Authorization", `token ${config.giteaConfig.token}`); + + const giteaLabels = giteaLabelsRes.body; + const labelMap = new Map( + giteaLabels.map((label: any) => [label.name, label.id]) + ); + + for (const issue of issues) { + if ((issue as any).pull_request) { + continue; + } + + const githubLabelNames = + issue.labels + ?.map((l) => (typeof l === "string" ? l : l.name)) + .filter((l): l is string => !!l) || []; + + const giteaLabelIds: number[] = []; + + // Resolve or create labels in Gitea + for (const name of githubLabelNames) { + if (labelMap.has(name)) { + giteaLabelIds.push(labelMap.get(name)!); + } else { + try { + const created = await superagent + .post( + `${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels` + ) + .set("Authorization", `token ${config.giteaConfig.token}`) + .send({ name, color: "#ededed" }); // Default color + + labelMap.set(name, created.body.id); + giteaLabelIds.push(created.body.id); + } catch (labelErr) { + console.error( + `Failed to create label "${name}" in Gitea: ${labelErr}` + ); + } + } + } + + const originalAssignees = + issue.assignees && issue.assignees.length > 0 + ? `\n\nOriginally assigned to: ${issue.assignees + .map((a) => `@${a.login}`) + .join(", ")} on GitHub.` + : ""; + + const issuePayload: any = { + title: issue.title, + body: `Originally created by @${ + issue.user?.login + } on GitHub.${originalAssignees}\n\n${issue.body || ""}`, + closed: issue.state === "closed", + labels: giteaLabelIds, + }; + + try { + const createdIssue = await superagent + .post( + `${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/issues` + ) + .set("Authorization", `token ${config.giteaConfig.token}`) + .send(issuePayload); + + // Clone comments + const comments = await octokit.paginate( + octokit.rest.issues.listComments, + { + owner, + repo, + issue_number: issue.number, + per_page: 100, + }, + (res) => res.data + ); + + for (const comment of comments) { + try { + await superagent + .post( + `${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/issues/${createdIssue.body.number}/comments` + ) + .set("Authorization", `token ${config.giteaConfig.token}`) + .send({ + body: `@${comment.user?.login} commented on GitHub:\n\n${comment.body}`, + }); + } catch (commentErr) { + console.error( + `Failed to copy comment to Gitea for issue "${issue.title}": ${ + commentErr instanceof Error + ? commentErr.message + : String(commentErr) + }` + ); + } + } + } catch (err) { + if (err instanceof Error && (err as any).response) { + console.error( + `Failed to create issue "${issue.title}" in Gitea: ${err.message}` + ); + console.error( + `Response body: ${JSON.stringify((err as any).response.body)}` + ); + } else { + console.error( + `Failed to create issue "${issue.title}" in Gitea: ${ + err instanceof Error ? err.message : String(err) + }` + ); + } + } + } +}; diff --git a/src/lib/github.ts b/src/lib/github.ts new file mode 100644 index 0000000..975aa48 --- /dev/null +++ b/src/lib/github.ts @@ -0,0 +1,265 @@ +import type { GitOrg, MembershipRole } from "@/types/organizations"; +import type { GitRepo, RepoStatus } from "@/types/Repository"; +import { Octokit } from "@octokit/rest"; +import type { Config } from "@/types/config"; + +/** + * Creates an authenticated Octokit instance + */ +export function createGitHubClient(token: string): Octokit { + return new Octokit({ + auth: token, + }); +} + +/** + * Clone a repository from GitHub + */ +export async function getGithubRepoCloneUrl({ + octokit, + owner, + repo, +}: { + octokit: Octokit; + owner: string; + repo: string; +}): Promise<{ url: string; cloneUrl: string }> { + const { data } = await octokit.repos.get({ + owner, + repo, + }); + + return { + url: data.html_url, + cloneUrl: data.clone_url, + }; +} + +/** + * Get user repositories from GitHub + * todo: need to handle pagination and apply more filters based on user config + */ +export async function getGithubRepositories({ + octokit, + config, +}: { + octokit: Octokit; + config: Partial; +}): Promise { + try { + const repos = await octokit.paginate( + octokit.repos.listForAuthenticatedUser, + { per_page: 100 } + ); + + const includePrivate = config.githubConfig?.privateRepositories ?? false; + const skipForks = config.githubConfig?.skipForks ?? false; + + const filteredRepos = repos.filter((repo) => { + const isPrivateAllowed = includePrivate || !repo.private; + const isForkAllowed = !skipForks || !repo.fork; + return isPrivateAllowed && isForkAllowed; + }); + + return filteredRepos.map((repo) => ({ + name: repo.name, + fullName: repo.full_name, + url: repo.html_url, + cloneUrl: repo.clone_url, + + owner: repo.owner.login, + organization: + repo.owner.type === "Organization" ? repo.owner.login : undefined, + + isPrivate: repo.private, + isForked: repo.fork, + forkedFrom: (repo as typeof repo & { parent?: { full_name: string } }) + .parent?.full_name, + + hasIssues: repo.has_issues, + isStarred: false, + isArchived: repo.archived, + + size: repo.size, + hasLFS: false, + hasSubmodules: false, + + defaultBranch: repo.default_branch, + visibility: (repo.visibility ?? "public") as GitRepo["visibility"], + + status: "imported", + lastMirrored: undefined, + errorMessage: undefined, + + createdAt: repo.created_at ? new Date(repo.created_at) : new Date(), + updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(), + })); + } catch (error) { + throw new Error( + `Error fetching repositories: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } +} + +export async function getGithubStarredRepositories({ + octokit, + config, +}: { + octokit: Octokit; + config: Partial; +}) { + try { + const starredRepos = await octokit.paginate( + octokit.activity.listReposStarredByAuthenticatedUser, + { + per_page: 100, + } + ); + + return starredRepos.map((repo) => ({ + name: repo.name, + fullName: repo.full_name, + url: repo.html_url, + cloneUrl: repo.clone_url, + + owner: repo.owner.login, + organization: + repo.owner.type === "Organization" ? repo.owner.login : undefined, + + isPrivate: repo.private, + isForked: repo.fork, + forkedFrom: undefined, + + hasIssues: repo.has_issues, + isStarred: true, + isArchived: repo.archived, + + size: repo.size, + hasLFS: false, // Placeholder + hasSubmodules: false, // Placeholder + + defaultBranch: repo.default_branch, + visibility: (repo.visibility ?? "public") as GitRepo["visibility"], + + status: "imported", + lastMirrored: undefined, + errorMessage: undefined, + + createdAt: repo.created_at ? new Date(repo.created_at) : new Date(), + updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(), + })); + } catch (error) { + throw new Error( + `Error fetching starred repositories: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } +} + +/** + * Get user github organizations + */ +export async function getGithubOrganizations({ + octokit, + config, +}: { + octokit: Octokit; + config: Partial; +}): Promise { + try { + const { data: orgs } = await octokit.orgs.listForAuthenticatedUser({ + per_page: 100, + }); + + const organizations = await Promise.all( + orgs.map(async (org) => { + const [{ data: orgDetails }, { data: membership }] = await Promise.all([ + octokit.orgs.get({ org: org.login }), + octokit.orgs.getMembershipForAuthenticatedUser({ org: org.login }), + ]); + + const totalRepos = + orgDetails.public_repos + (orgDetails.total_private_repos ?? 0); + + return { + name: org.login, + avatarUrl: org.avatar_url, + membershipRole: membership.role as MembershipRole, + isIncluded: false, + status: "imported" as RepoStatus, + repositoryCount: totalRepos, + createdAt: new Date(), + updatedAt: new Date(), + }; + }) + ); + + return organizations; + } catch (error) { + throw new Error( + `Error fetching organizations: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } +} + +/** + * Get repositories for a specific organization + */ +export async function getGithubOrganizationRepositories({ + octokit, + organizationName, +}: { + octokit: Octokit; + organizationName: string; +}): Promise { + try { + const repos = await octokit.paginate(octokit.repos.listForOrg, { + org: organizationName, + per_page: 100, + }); + + return repos.map((repo) => ({ + name: repo.name, + fullName: repo.full_name, + url: repo.html_url, + cloneUrl: repo.clone_url ?? "", + + owner: repo.owner.login, + organization: repo.owner.login, + + isPrivate: repo.private, + isForked: repo.fork, + forkedFrom: (repo as typeof repo & { parent?: { full_name: string } }) + .parent?.full_name, + + hasIssues: repo.has_issues ?? false, + isStarred: false, // Organization starred repos are separate API + isArchived: repo.archived ?? false, + + size: repo.size ?? 0, + hasLFS: false, + hasSubmodules: false, + + defaultBranch: repo.default_branch ?? "main", + visibility: (repo.visibility ?? "public") as GitRepo["visibility"], + + status: "imported", + lastMirrored: undefined, + errorMessage: undefined, + + createdAt: repo.created_at ? new Date(repo.created_at) : new Date(), + updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(), + })); + } catch (error) { + throw new Error( + `Error fetching organization repositories: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } +} diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts new file mode 100644 index 0000000..69469ff --- /dev/null +++ b/src/lib/helpers.ts @@ -0,0 +1,53 @@ +import type { RepoStatus } from "@/types/Repository"; +import { db, mirrorJobs } from "./db"; +import { v4 as uuidv4 } from "uuid"; +import { redisPublisher } from "./redis"; + +export async function createMirrorJob({ + userId, + organizationId, + organizationName, + repositoryId, + repositoryName, + message, + status, + details, +}: { + userId: string; + organizationId?: string; + organizationName?: string; + repositoryId?: string; + repositoryName?: string; + details?: string; + message: string; + status: RepoStatus; +}) { + const jobId = uuidv4(); + const currentTimestamp = new Date(); + + const job = { + id: jobId, + userId, + repositoryId, + repositoryName, + organizationId, + organizationName, + configId: uuidv4(), + details, + message: message, + status: status, + timestamp: currentTimestamp, + }; + + try { + await db.insert(mirrorJobs).values(job); + + const channel = `mirror-status:${userId}`; + await redisPublisher.publish(channel, JSON.stringify(job)); + + return jobId; + } catch (error) { + console.error("Error creating mirror job:", error); + throw new Error("Error creating mirror job"); + } +} diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..d46e501 --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,4 @@ +import Redis from "ioredis"; + +export const redisPublisher = new Redis(); // For publishing +export const redisSubscriber = new Redis(); // For subscribing diff --git a/src/lib/rough.ts b/src/lib/rough.ts new file mode 100644 index 0000000..dd1d835 --- /dev/null +++ b/src/lib/rough.ts @@ -0,0 +1,160 @@ +// this is a temporary file for testing purposes +import type { Config } from "@/types/config"; + +export async function deleteAllReposInOrg({ + config, + org, +}: { + config: Partial; + org: string; +}) { + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + throw new Error("Gitea config is required."); + } + + // Step 1: Get all repositories in the organization + const repoRes = await fetch( + `${config.giteaConfig.url}/api/v1/orgs/${org}/repos`, + { + headers: { + Authorization: `token ${config.giteaConfig.token}`, + }, + } + ); + + if (!repoRes.ok) { + console.error( + `Failed to fetch repos for org ${org}: ${await repoRes.text()}` + ); + return; + } + + const repos = await repoRes.json(); + + // Step 2: Delete each repository + for (const repo of repos) { + const deleteRes = await fetch( + `${config.giteaConfig.url}/api/v1/repos/${org}/${repo.name}`, + { + method: "DELETE", + headers: { + Authorization: `token ${config.giteaConfig.token}`, + }, + } + ); + + if (!deleteRes.ok) { + console.error( + `Failed to delete repo ${repo.name}: ${await deleteRes.text()}` + ); + } else { + console.log(`Successfully deleted repo ${repo.name}`); + } + } +} + +export async function deleteOrg({ + config, + org, +}: { + config: Partial; + org: string; +}) { + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + throw new Error("Gitea config is required."); + } + + const deleteOrgRes = await fetch( + `${config.giteaConfig.url}/api/v1/orgs/${org}`, + { + method: "DELETE", + headers: { + Authorization: `token ${config.giteaConfig.token}`, + }, + } + ); + + if (!deleteOrgRes.ok) { + console.error(`Failed to delete org ${org}: ${await deleteOrgRes.text()}`); + } else { + console.log(`Successfully deleted org ${org}`); + } +} + +export async function deleteAllOrgs({ + config, + orgs, +}: { + config: Partial; + orgs: string[]; +}) { + for (const org of orgs) { + console.log(`Starting deletion for org: ${org}`); + + // First, delete all repositories in the organization + await deleteAllReposInOrg({ config, org }); + + // Then, delete the organization itself + await deleteOrg({ config, org }); + + console.log(`Finished deletion for org: ${org}`); + } +} + +export async function deleteAllReposInGitea({ + config, +}: { + config: Partial; +}) { + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + throw new Error("Gitea config is required."); + } + + console.log("Fetching all repositories..."); + + // Step 1: Get all repositories (user + org repos) + const repoRes = await fetch(`${config.giteaConfig.url}/api/v1/user/repos`, { + headers: { + Authorization: `token ${config.giteaConfig.token}`, + }, + }); + + if (!repoRes.ok) { + console.error(`Failed to fetch repositories: ${await repoRes.text()}`); + return; + } + + const repos = await repoRes.json(); + + if (repos.length === 0) { + console.log("No repositories found to delete."); + return; + } + + console.log(`Found ${repos.length} repositories. Starting deletion...`); + + // Step 2: Delete all repositories in parallel + await Promise.allSettled( + repos.map((repo: any) => + fetch( + `${config.giteaConfig?.url}/api/v1/repos/${repo.owner.username}/${repo.name}`, + { + method: "DELETE", + headers: { + Authorization: `token ${config.giteaConfig?.token}`, + }, + } + ).then(async (res) => { + if (!res.ok) { + console.error( + `Failed to delete repo ${repo.full_name}: ${await res.text()}` + ); + } else { + console.log(`Successfully deleted repo ${repo.full_name}`); + } + }) + ) + ); + + console.log("Finished deleting all repositories."); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..3ebe90b --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,98 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; +import axios from "axios"; +import type { AxiosError, AxiosRequestConfig } from "axios"; +import type { RepoStatus } from "@/types/Repository"; + +export const API_BASE = "/api"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatDate(date?: Date | string | null): string { + if (!date) return "Never"; + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(date)); +} + +export function truncate(str: string, length: number): string { + if (str.length <= length) return str; + return str.slice(0, length) + "..."; +} + +export function safeParse(value: unknown): T | undefined { + if (typeof value === "string") { + try { + return JSON.parse(value) as T; + } catch { + return undefined; + } + } + return value as T; +} + +// Helper function for API requests + +export async function apiRequest( + endpoint: string, + options: AxiosRequestConfig = {} +): Promise { + try { + const response = await axios(`${API_BASE}${endpoint}`, { + headers: { + "Content-Type": "application/json", + ...(options.headers || {}), + }, + ...options, + }); + + return response.data; + } catch (err) { + const error = err as AxiosError<{ message?: string }>; + + const message = + error.response?.data?.message || + error.message || + "An unknown error occurred"; + + throw new Error(message); + } +} + +export const getStatusColor = (status: RepoStatus): string => { + switch (status) { + case "imported": + return "bg-blue-500"; // Info/primary-like + case "mirroring": + return "bg-yellow-400"; // In progress + case "mirrored": + return "bg-emerald-500"; // Success + case "failed": + return "bg-rose-500"; // Error + case "syncing": + return "bg-indigo-500"; // Sync in progress + case "synced": + return "bg-teal-500"; // Sync complete + default: + return "bg-gray-400"; // Unknown/neutral + } +}; + +export const jsonResponse = ({ + data, + status = 200, +}: { + data: unknown; + status?: number; +}): Response => { + return new Response(JSON.stringify(data), { + status, + headers: { "Content-Type": "application/json" }, + }); +}; diff --git a/src/pages/activity.astro b/src/pages/activity.astro new file mode 100644 index 0000000..c1e3925 --- /dev/null +++ b/src/pages/activity.astro @@ -0,0 +1,65 @@ +--- +import '../styles/global.css'; +import App from '@/components/layout/MainLayout'; +import { db, mirrorJobs } from '@/lib/db'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; + +// Fetch activity data from the database +let activityData = []; + +try { + // Fetch activity from mirror jobs + const jobs = await db.select().from(mirrorJobs).limit(20); + activityData = jobs.flatMap((job: any) => { + // Check if log exists before parsing + if (!job.log) { + console.warn(`Job ${job.id} has no log data`); + return []; + } + + try { + const log = JSON.parse(job.log); + if (!Array.isArray(log)) { + console.warn(`Job ${job.id} log is not an array`); + return []; + } + + return log.map((entry: any) => ({ + id: `${job.id}-${entry.timestamp}`, + message: entry.message, + timestamp: new Date(entry.timestamp), + status: entry.level, + details: entry.details, + repositoryName: entry.repositoryName, + })); + } catch (parseError) { + console.error(`Failed to parse log for job ${job.id}:`, parseError); + return []; + } + }).slice(0, 20); +} catch (error) { + console.error('Error fetching activity:', error); + // Fallback to empty array if database access fails + activityData = []; +} + +// Client-side function to handle refresh +const handleRefresh = () => { + console.log('Refreshing activity log'); + // In a real implementation, this would call the API to refresh the activity log +}; +--- + + + + + + + + Activity Log - Gitea Mirror + + + + + + diff --git a/src/pages/api/activities/index.ts b/src/pages/api/activities/index.ts new file mode 100644 index 0000000..e56cce3 --- /dev/null +++ b/src/pages/api/activities/index.ts @@ -0,0 +1,58 @@ +import type { APIRoute } from "astro"; +import { db, mirrorJobs, configs } from "@/lib/db"; +import { eq, sql } from "drizzle-orm"; +import type { MirrorJob } from "@/lib/db/schema"; +import { repoStatusEnum } from "@/types/Repository"; + +export const GET: APIRoute = async ({ url }) => { + try { + const searchParams = new URL(url).searchParams; + const userId = searchParams.get("userId"); + + if (!userId) { + return new Response( + JSON.stringify({ error: "Missing 'userId' in query parameters." }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch mirror jobs associated with the user + const jobs = await db + .select() + .from(mirrorJobs) + .where(eq(mirrorJobs.userId, userId)) + .orderBy(sql`${mirrorJobs.timestamp} DESC`); + + const activities: MirrorJob[] = jobs.map((job) => ({ + id: job.id, + userId: job.userId, + repositoryId: job.repositoryId ?? undefined, + repositoryName: job.repositoryName ?? undefined, + organizationId: job.organizationId ?? undefined, + organizationName: job.organizationName ?? undefined, + status: repoStatusEnum.parse(job.status), + details: job.details ?? undefined, + message: job.message, + timestamp: job.timestamp, + })); + + return new Response( + JSON.stringify({ + success: true, + message: "Mirror job activities retrieved successfully.", + activities, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } catch (error) { + console.error("Error fetching mirror job activities:", error); + return new Response( + JSON.stringify({ + success: false, + error: + error instanceof Error ? error.message : "An unknown error occurred.", + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/auth/index.ts b/src/pages/api/auth/index.ts new file mode 100644 index 0000000..1c2f936 --- /dev/null +++ b/src/pages/api/auth/index.ts @@ -0,0 +1,83 @@ +import type { APIRoute } from "astro"; +import { db, users, configs, client } from "@/lib/db"; +import { eq, and } from "drizzle-orm"; +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +export const GET: APIRoute = async ({ request, cookies }) => { + const authHeader = request.headers.get("Authorization"); + const token = authHeader?.split(" ")[1] || cookies.get("token")?.value; + + if (!token) { + const userCountResult = await client.execute( + `SELECT COUNT(*) as count FROM users` + ); + const userCount = userCountResult.rows[0].count; + + if (userCount === 0) { + return new Response(JSON.stringify({ error: "No users found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as { id: string }; + + const userResult = await db + .select() + .from(users) + .where(eq(users.id, decoded.id)) + .limit(1); + + if (!userResult.length) { + return new Response(JSON.stringify({ error: "User not found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + const { password, ...userWithoutPassword } = userResult[0]; + + const configResult = await db + .select({ + scheduleConfig: configs.scheduleConfig, + }) + .from(configs) + .where(and(eq(configs.userId, decoded.id), eq(configs.isActive, true))) + .limit(1); + + const scheduleConfig = configResult[0]?.scheduleConfig; + + const syncEnabled = scheduleConfig?.enabled ?? false; + const syncInterval = scheduleConfig?.interval ?? 3600; + const lastSync = scheduleConfig?.lastRun ?? null; + const nextSync = scheduleConfig?.nextRun ?? null; + + return new Response( + JSON.stringify({ + ...userWithoutPassword, + syncEnabled, + syncInterval, + lastSync, + nextSync, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + return new Response(JSON.stringify({ error: "Invalid token" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } +}; diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts new file mode 100644 index 0000000..6f6786b --- /dev/null +++ b/src/pages/api/auth/login.ts @@ -0,0 +1,62 @@ +import type { APIRoute } from "astro"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import { db, users } from "@/lib/db"; +import { eq } from "drizzle-orm"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +export const POST: APIRoute = async ({ request }) => { + const { username, password } = await request.json(); + + if (!username || !password) { + return new Response( + JSON.stringify({ error: "Username and password are required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const user = await db + .select() + .from(users) + .where(eq(users.username, username)) + .limit(1); + + if (!user.length) { + return new Response( + JSON.stringify({ error: "Invalid username or password" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const isPasswordValid = await bcrypt.compare(password, user[0].password); + + if (!isPasswordValid) { + return new Response( + JSON.stringify({ error: "Invalid username or password" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const { password: _, ...userWithoutPassword } = user[0]; + const token = jwt.sign({ id: user[0].id }, JWT_SECRET, { expiresIn: "7d" }); + + return new Response(JSON.stringify({ token, user: userWithoutPassword }), { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${ + 60 * 60 * 24 * 7 + }`, + }, + }); +}; diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/logout.ts new file mode 100644 index 0000000..e1e7014 --- /dev/null +++ b/src/pages/api/auth/logout.ts @@ -0,0 +1,11 @@ +import type { APIRoute } from "astro"; + +export const POST: APIRoute = async () => { + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": "token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0", + }, + }); +}; diff --git a/src/pages/api/auth/register.ts b/src/pages/api/auth/register.ts new file mode 100644 index 0000000..b561f1f --- /dev/null +++ b/src/pages/api/auth/register.ts @@ -0,0 +1,72 @@ +import type { APIRoute } from "astro"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import { db, users } from "@/lib/db"; +import { eq, or } from "drizzle-orm"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +export const POST: APIRoute = async ({ request }) => { + const { username, email, password } = await request.json(); + + if (!username || !email || !password) { + return new Response( + JSON.stringify({ error: "Username, email, and password are required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Check if username or email already exists + const existingUser = await db + .select() + .from(users) + .where(or(eq(users.username, username), eq(users.email, email))) + .limit(1); + + if (existingUser.length) { + return new Response( + JSON.stringify({ error: "Username or email already exists" }), + { + status: 409, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Generate UUID + const id = crypto.randomUUID(); + + // Create user + const newUser = await db + .insert(users) + .values({ + id, + username, + email, + password: hashedPassword, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + const { password: _, ...userWithoutPassword } = newUser[0]; + const token = jwt.sign({ id: newUser[0].id }, JWT_SECRET, { + expiresIn: "7d", + }); + + return new Response(JSON.stringify({ token, user: userWithoutPassword }), { + status: 201, + headers: { + "Content-Type": "application/json", + "Set-Cookie": `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${ + 60 * 60 * 24 * 7 + }`, + }, + }); +}; diff --git a/src/pages/api/config/index.ts b/src/pages/api/config/index.ts new file mode 100644 index 0000000..3c4b231 --- /dev/null +++ b/src/pages/api/config/index.ts @@ -0,0 +1,225 @@ +import type { APIRoute } from "astro"; +import { db, configs, users } from "@/lib/db"; +import { v4 as uuidv4 } from "uuid"; +import { eq } from "drizzle-orm"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { userId, githubConfig, giteaConfig, scheduleConfig } = body; + + if (!userId || !githubConfig || !giteaConfig || !scheduleConfig) { + return new Response( + JSON.stringify({ + success: false, + message: + "userId, githubConfig, giteaConfig, and scheduleConfig are required.", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Fetch existing config + const existingConfigResult = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + const existingConfig = existingConfigResult[0]; + + // Preserve tokens if fields are empty + if (existingConfig) { + try { + const existingGithub = + typeof existingConfig.githubConfig === "string" + ? JSON.parse(existingConfig.githubConfig) + : existingConfig.githubConfig; + + const existingGitea = + typeof existingConfig.giteaConfig === "string" + ? JSON.parse(existingConfig.giteaConfig) + : existingConfig.giteaConfig; + + if (!githubConfig.token && existingGithub.token) { + githubConfig.token = existingGithub.token; + } + + if (!giteaConfig.token && existingGitea.token) { + giteaConfig.token = existingGitea.token; + } + } catch (tokenError) { + console.error("Failed to preserve tokens:", tokenError); + } + } + + if (existingConfig) { + // Update path + await db + .update(configs) + .set({ + githubConfig, + giteaConfig, + scheduleConfig, + updatedAt: new Date(), + }) + .where(eq(configs.id, existingConfig.id)); + + return new Response( + JSON.stringify({ + success: true, + message: "Configuration updated successfully", + configId: existingConfig.id, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Fallback user check (optional if you're always passing userId) + const userExists = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (userExists.length === 0) { + return new Response( + JSON.stringify({ + success: false, + message: "Invalid userId. No matching user found.", + }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Create new config + const configId = uuidv4(); + await db.insert(configs).values({ + id: configId, + userId, + name: "Default Configuration", + isActive: true, + githubConfig, + giteaConfig, + include: [], + exclude: [], + scheduleConfig, + createdAt: new Date(), + updatedAt: new Date(), + }); + + return new Response( + JSON.stringify({ + success: true, + message: "Configuration created successfully", + configId, + }), + { + status: 201, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Error saving configuration:", error); + return new Response( + JSON.stringify({ + success: false, + message: + "Error saving configuration: " + + (error instanceof Error ? error.message : "Unknown error"), + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); + } +}; + +export const GET: APIRoute = async ({ request }) => { + try { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return new Response(JSON.stringify({ error: "User ID is required" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + // Fetch the configuration for the user + const config = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + if (config.length === 0) { + // Return a default empty configuration instead of a 404 error + return new Response( + JSON.stringify({ + id: null, + userId: userId, + name: "Default Configuration", + isActive: true, + githubConfig: { + username: "", + token: "", + skipForks: false, + privateRepositories: false, + mirrorIssues: false, + mirrorStarred: true, + useSpecificUser: false, + preserveOrgStructure: true, + skipStarredIssues: false, + }, + giteaConfig: { + url: "", + token: "", + username: "", + organization: "github-mirrors", + visibility: "public", + starredReposOrg: "github", + }, + scheduleConfig: { + enabled: false, + interval: 3600, + lastRun: null, + nextRun: null, + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + return new Response(JSON.stringify(config[0]), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error fetching configuration:", error); + + return new Response( + JSON.stringify({ + error: error instanceof Error ? error.message : "Something went wrong", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); + } +}; diff --git a/src/pages/api/dashboard/index.ts b/src/pages/api/dashboard/index.ts new file mode 100644 index 0000000..3512d7a --- /dev/null +++ b/src/pages/api/dashboard/index.ts @@ -0,0 +1,122 @@ +import type { APIRoute } from "astro"; +import { db, repositories, organizations, mirrorJobs, configs } from "@/lib/db"; +import { eq, count, and, sql, or } from "drizzle-orm"; +import { jsonResponse } from "@/lib/utils"; +import type { DashboardApiResponse } from "@/types/dashboard"; +import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; +import { membershipRoleEnum } from "@/types/organizations"; + +export const GET: APIRoute = async ({ request }) => { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return jsonResponse({ + data: { + success: false, + error: "Missing userId", + }, + status: 400, + }); + } + + try { + const [ + userRepos, + userOrgs, + userLogs, + [userConfig], + [{ value: repoCount }], + [{ value: orgCount }], + [{ value: mirroredCount }], + ] = await Promise.all([ + db + .select() + .from(repositories) + .where(eq(repositories.userId, userId)) + .orderBy(sql`${repositories.updatedAt} DESC`) + .limit(10), + db + .select() + .from(organizations) + .where(eq(organizations.userId, userId)) + .orderBy(sql`${organizations.updatedAt} DESC`) + .limit(10), // not really needed in the frontend but just in case + db + .select() + .from(mirrorJobs) + .where(eq(mirrorJobs.userId, userId)) + .orderBy(sql`${mirrorJobs.timestamp} DESC`) + .limit(10), + db.select().from(configs).where(eq(configs.userId, userId)).limit(1), + db + .select({ value: count() }) + .from(repositories) + .where(eq(repositories.userId, userId)), + db + .select({ value: count() }) + .from(organizations) + .where(eq(organizations.userId, userId)), + db + .select({ value: count() }) + .from(repositories) + .where( + and( + eq(repositories.userId, userId), + or( + eq(repositories.status, "mirrored"), + eq(repositories.status, "synced") + ) + ) + ), + ]); + + const successResponse: DashboardApiResponse = { + success: true, + message: "Dashboard data loaded successfully", + repoCount: repoCount ?? 0, + orgCount: orgCount ?? 0, + mirroredCount: mirroredCount ?? 0, + repositories: userRepos.map((repo) => ({ + ...repo, + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + status: repoStatusEnum.parse(repo.status), + visibility: repositoryVisibilityEnum.parse(repo.visibility), + })), + organizations: userOrgs.map((org) => ({ + ...org, + status: repoStatusEnum.parse(org.status), + membershipRole: membershipRoleEnum.parse(org.membershipRole), + lastMirrored: org.lastMirrored ?? undefined, + errorMessage: org.errorMessage ?? undefined, + })), + activities: userLogs.map((job) => ({ + id: job.id, + userId: job.userId, + repositoryName: job.repositoryName ?? undefined, + organizationName: job.organizationName ?? undefined, + status: repoStatusEnum.parse(job.status), + details: job.details ?? undefined, + message: job.message, + timestamp: job.timestamp, + })), + lastSync: userConfig?.scheduleConfig.lastRun ?? null, + }; + + return jsonResponse({ data: successResponse }); + } catch (error) { + console.error("Error loading dashboard for user:", userId, error); + + return jsonResponse({ + data: { + success: false, + error: error instanceof Error ? error.message : "Internal server error", + message: "Failed to fetch dashboard data", + }, + status: 500, + }); + } +}; diff --git a/src/pages/api/gitea/test-connection.ts b/src/pages/api/gitea/test-connection.ts new file mode 100644 index 0000000..a322c05 --- /dev/null +++ b/src/pages/api/gitea/test-connection.ts @@ -0,0 +1,135 @@ +import type { APIRoute } from 'astro'; +import axios from 'axios'; + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { url, token, username } = body; + + if (!url || !token) { + return new Response( + JSON.stringify({ + success: false, + message: 'Gitea URL and token are required', + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // Normalize the URL (remove trailing slash if present) + const baseUrl = url.endsWith('/') ? url.slice(0, -1) : url; + + // Test the connection by fetching the authenticated user + const response = await axios.get(`${baseUrl}/api/v1/user`, { + headers: { + 'Authorization': `token ${token}`, + 'Accept': 'application/json', + }, + }); + + const data = response.data; + + // Verify that the authenticated user matches the provided username (if provided) + if (username && data.login !== username) { + return new Response( + JSON.stringify({ + success: false, + message: `Token belongs to ${data.login}, not ${username}`, + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // Return success response with user data + return new Response( + JSON.stringify({ + success: true, + message: `Successfully connected to Gitea as ${data.login}`, + user: { + login: data.login, + name: data.full_name, + avatar_url: data.avatar_url, + }, + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } catch (error) { + console.error('Gitea connection test failed:', error); + + // Handle specific error types + if (axios.isAxiosError(error) && error.response) { + if (error.response.status === 401) { + return new Response( + JSON.stringify({ + success: false, + message: 'Invalid Gitea token', + }), + { + status: 401, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } else if (error.response.status === 404) { + return new Response( + JSON.stringify({ + success: false, + message: 'Gitea API endpoint not found. Please check the URL.', + }), + { + status: 404, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + } + + // Handle connection errors + if (axios.isAxiosError(error) && (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND')) { + return new Response( + JSON.stringify({ + success: false, + message: 'Could not connect to Gitea server. Please check the URL.', + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // Generic error response + return new Response( + JSON.stringify({ + success: false, + message: `Gitea connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; diff --git a/src/pages/api/github/organizations.ts b/src/pages/api/github/organizations.ts new file mode 100644 index 0000000..2bbe198 --- /dev/null +++ b/src/pages/api/github/organizations.ts @@ -0,0 +1,60 @@ +import type { APIRoute } from "astro"; +import { db } from "@/lib/db"; +import { organizations } from "@/lib/db"; +import { eq, sql } from "drizzle-orm"; +import { + membershipRoleEnum, + type OrganizationsApiResponse, +} from "@/types/organizations"; +import type { Organization } from "@/lib/db/schema"; +import { repoStatusEnum } from "@/types/Repository"; +import { jsonResponse } from "@/lib/utils"; + +export const GET: APIRoute = async ({ request }) => { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return jsonResponse({ + data: { + success: false, + error: "Missing userId", + }, + status: 400, + }); + } + + try { + const rawOrgs = await db + .select() + .from(organizations) + .where(eq(organizations.userId, userId)) + .orderBy(sql`name COLLATE NOCASE`); + + const orgsWithIds: Organization[] = rawOrgs.map((org) => ({ + ...org, + status: repoStatusEnum.parse(org.status), + membershipRole: membershipRoleEnum.parse(org.membershipRole), + lastMirrored: org.lastMirrored ?? undefined, + errorMessage: org.errorMessage ?? undefined, + })); + + const resPayload: OrganizationsApiResponse = { + success: true, + message: "Organizations fetched successfully", + organizations: orgsWithIds, + }; + + return jsonResponse({ data: resPayload, status: 200 }); + } catch (error) { + console.error("Error fetching organizations:", error); + + return jsonResponse({ + data: { + success: false, + error: error instanceof Error ? error.message : "Something went wrong", + }, + status: 500, + }); + } +}; diff --git a/src/pages/api/github/repositories.ts b/src/pages/api/github/repositories.ts new file mode 100644 index 0000000..cce184a --- /dev/null +++ b/src/pages/api/github/repositories.ts @@ -0,0 +1,96 @@ +import type { APIRoute } from "astro"; +import { db, repositories, configs } from "@/lib/db"; +import { and, eq, sql } from "drizzle-orm"; +import { + repositoryVisibilityEnum, + repoStatusEnum, + type RepositoryApiResponse, +} from "@/types/Repository"; +import { jsonResponse } from "@/lib/utils"; + +export const GET: APIRoute = async ({ request }) => { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return jsonResponse({ + data: { success: false, error: "Missing userId" }, + status: 400, + }); + } + + try { + // Fetch the user's active configuration + const [config] = await db + .select() + .from(configs) + .where(and(eq(configs.userId, userId), eq(configs.isActive, true))); + + if (!config) { + return jsonResponse({ + data: { + success: false, + error: "No active configuration found for this user", + }, + status: 404, + }); + } + + const githubConfig = config.githubConfig as { + mirrorStarred: boolean; + skipForks: boolean; + privateRepositories: boolean; + }; + + // Build query conditions based on config + const conditions = [eq(repositories.userId, userId)]; + + if (!githubConfig.mirrorStarred) { + conditions.push(eq(repositories.isStarred, false)); + } + + if (githubConfig.skipForks) { + conditions.push(eq(repositories.isForked, false)); + } + + if (!githubConfig.privateRepositories) { + conditions.push(eq(repositories.isPrivate, false)); + } + + const rawRepositories = await db + .select() + .from(repositories) + .where(and(...conditions)) + .orderBy(sql`name COLLATE NOCASE`); + + const response: RepositoryApiResponse = { + success: true, + message: "Repositories fetched successfully", + repositories: rawRepositories.map((repo) => ({ + ...repo, + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + status: repoStatusEnum.parse(repo.status), + visibility: repositoryVisibilityEnum.parse(repo.visibility), + })), + }; + + return jsonResponse({ + data: response, + status: 200, + }); + } catch (error) { + console.error("Error fetching repositories:", error); + + return jsonResponse({ + data: { + success: false, + error: error instanceof Error ? error.message : "Something went wrong", + message: "An error occurred while fetching repositories.", + }, + status: 500, + }); + } +}; diff --git a/src/pages/api/github/test-connection.ts b/src/pages/api/github/test-connection.ts new file mode 100644 index 0000000..f7a8df1 --- /dev/null +++ b/src/pages/api/github/test-connection.ts @@ -0,0 +1,101 @@ +import type { APIRoute } from "astro"; +import { Octokit } from "@octokit/rest"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { token, username } = body; + + if (!token) { + return new Response( + JSON.stringify({ + success: false, + message: "GitHub token is required", + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + // Create an Octokit instance with the provided token + const octokit = new Octokit({ + auth: token, + }); + + // Test the connection by fetching the authenticated user + const { data } = await octokit.users.getAuthenticated(); + + // Verify that the authenticated user matches the provided username (if provided) + if (username && data.login !== username) { + return new Response( + JSON.stringify({ + success: false, + message: `Token belongs to ${data.login}, not ${username}`, + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + // Return success response with user data + return new Response( + JSON.stringify({ + success: true, + message: `Successfully connected to GitHub as ${data.login}`, + user: { + login: data.login, + name: data.name, + avatar_url: data.avatar_url, + }, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); + } catch (error) { + console.error("GitHub connection test failed:", error); + + // Handle specific error types + if (error instanceof Error && (error as any).status === 401) { + return new Response( + JSON.stringify({ + success: false, + message: "Invalid GitHub token", + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + // Generic error response + return new Response( + JSON.stringify({ + success: false, + message: `GitHub connection test failed: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); + } +}; diff --git a/src/pages/api/job/mirror-org.ts b/src/pages/api/job/mirror-org.ts new file mode 100644 index 0000000..4f95d80 --- /dev/null +++ b/src/pages/api/job/mirror-org.ts @@ -0,0 +1,118 @@ +import type { APIRoute } from "astro"; +import type { MirrorOrgRequest, MirrorOrgResponse } from "@/types/mirror"; +import { db, configs, organizations } from "@/lib/db"; +import { eq, inArray } from "drizzle-orm"; +import { createGitHubClient } from "@/lib/github"; +import { mirrorGitHubOrgToGitea } from "@/lib/gitea"; +import { repoStatusEnum } from "@/types/Repository"; +import { type MembershipRole } from "@/types/organizations"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body: MirrorOrgRequest = await request.json(); + const { userId, organizationIds } = body; + + if (!userId || !organizationIds || !Array.isArray(organizationIds)) { + return new Response( + JSON.stringify({ + success: false, + message: "userId and organizationIds are required.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + if (organizationIds.length === 0) { + return new Response( + JSON.stringify({ + success: false, + message: "No organization IDs provided.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch config + const configResult = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + const config = configResult[0]; + + if (!config || !config.githubConfig.token) { + return new Response( + JSON.stringify({ error: "Config missing for the user or token." }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch organizations + const orgs = await db + .select() + .from(organizations) + .where(inArray(organizations.id, organizationIds)); + + if (!orgs.length) { + return new Response( + JSON.stringify({ error: "No organizations found for the given IDs." }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Fire async mirroring without blocking response + setTimeout(async () => { + for (const org of orgs) { + if (!config.githubConfig.token) { + throw new Error("GitHub token is missing in config."); + } + + const octokit = createGitHubClient(config.githubConfig.token); + + try { + await mirrorGitHubOrgToGitea({ + config, + octokit, + organization: { + ...org, + status: repoStatusEnum.parse("imported"), + membershipRole: org.membershipRole as MembershipRole, + lastMirrored: org.lastMirrored ?? undefined, + errorMessage: org.errorMessage ?? undefined, + }, + }); + } catch (error) { + console.error(`Mirror failed for organization ${org.name}:`, error); + } + } + }, 0); + + const responsePayload: MirrorOrgResponse = { + success: true, + message: "Mirror job started.", + organizations: orgs.map((org) => ({ + ...org, + status: repoStatusEnum.parse(org.status), + membershipRole: org.membershipRole as MembershipRole, + lastMirrored: org.lastMirrored ?? undefined, + errorMessage: org.errorMessage ?? undefined, + })), + }; + + // Immediate response + return new Response(JSON.stringify(responsePayload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error in mirroring organization:", error); + return new Response( + JSON.stringify({ + error: + error instanceof Error ? error.message : "An unknown error occurred.", + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/job/mirror-repo.ts b/src/pages/api/job/mirror-repo.ts new file mode 100644 index 0000000..2fe1654 --- /dev/null +++ b/src/pages/api/job/mirror-repo.ts @@ -0,0 +1,144 @@ +import type { APIRoute } from "astro"; +import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror"; +import { db, configs, repositories } from "@/lib/db"; +import { eq, inArray } from "drizzle-orm"; +import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; +import { + mirrorGithubRepoToGitea, + mirrorGitHubOrgRepoToGiteaOrg, +} from "@/lib/gitea"; +import { createGitHubClient } from "@/lib/github"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body: MirrorRepoRequest = await request.json(); + const { userId, repositoryIds } = body; + + if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) { + return new Response( + JSON.stringify({ + success: false, + message: "userId and repositoryIds are required.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + if (repositoryIds.length === 0) { + return new Response( + JSON.stringify({ + success: false, + message: "No repository IDs provided.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch config + const configResult = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + const config = configResult[0]; + + if (!config || !config.githubConfig.token) { + return new Response( + JSON.stringify({ error: "Config missing for the user or token." }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch repos + const repos = await db + .select() + .from(repositories) + .where(inArray(repositories.id, repositoryIds)); + + if (!repos.length) { + return new Response( + JSON.stringify({ error: "No repositories found for the given IDs." }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Start async mirroring in background + setTimeout(async () => { + for (const repo of repos) { + if (!config.githubConfig.token) { + throw new Error("GitHub token is missing."); + } + + const octokit = createGitHubClient(config.githubConfig.token); + + try { + if (repo.organization && config.githubConfig.preserveOrgStructure) { + await mirrorGitHubOrgRepoToGiteaOrg({ + config, + octokit, + orgName: repo.organization, + repository: { + ...repo, + status: repoStatusEnum.parse("imported"), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + mirroredLocation: repo.mirroredLocation || "", + }, + }); + } else { + await mirrorGithubRepoToGitea({ + octokit, + repository: { + ...repo, + status: repoStatusEnum.parse("imported"), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + mirroredLocation: repo.mirroredLocation || "", + }, + config, + }); + } + } catch (error) { + console.error(`Mirror failed for repo ${repo.name}:`, error); + } + } + }, 0); + + const responsePayload: MirrorRepoResponse = { + success: true, + message: "Mirror job started.", + repositories: repos.map((repo) => ({ + ...repo, + status: repoStatusEnum.parse(repo.status), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + mirroredLocation: repo.mirroredLocation || "", + })), + }; + + // Return the updated repo list to the user + return new Response(JSON.stringify(responsePayload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error mirroring repositories:", error); + return new Response( + JSON.stringify({ + error: + error instanceof Error ? error.message : "An unknown error occurred", + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/job/retry-repo.ts b/src/pages/api/job/retry-repo.ts new file mode 100644 index 0000000..d761184 --- /dev/null +++ b/src/pages/api/job/retry-repo.ts @@ -0,0 +1,160 @@ +import type { APIRoute } from "astro"; +import { db, configs, repositories } from "@/lib/db"; +import { eq, inArray } from "drizzle-orm"; +import { getGiteaRepoOwner, isRepoPresentInGitea } from "@/lib/gitea"; +import { + mirrorGithubRepoToGitea, + mirrorGitHubOrgRepoToGiteaOrg, + syncGiteaRepo, +} from "@/lib/gitea"; +import { createGitHubClient } from "@/lib/github"; +import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository"; +import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body: RetryRepoRequest = await request.json(); + const { userId, repositoryIds } = body; + + if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) { + return new Response( + JSON.stringify({ + success: false, + message: "userId and repositoryIds are required.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + if (repositoryIds.length === 0) { + return new Response( + JSON.stringify({ + success: false, + message: "No repository IDs provided.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch user config + const configResult = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + const config = configResult[0]; + + if (!config || !config.githubConfig.token || !config.giteaConfig?.token) { + return new Response( + JSON.stringify({ error: "Missing GitHub or Gitea configuration." }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch repositories + const repos = await db + .select() + .from(repositories) + .where(inArray(repositories.id, repositoryIds)); + + if (!repos.length) { + return new Response( + JSON.stringify({ error: "No repositories found for the given IDs." }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Start background retry + setTimeout(async () => { + for (const repo of repos) { + try { + const visibility = repositoryVisibilityEnum.parse(repo.visibility); + const status = repoStatusEnum.parse(repo.status); + const repoData = { + ...repo, + visibility, + status, + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + }; + + let owner = getGiteaRepoOwner({ + config, + repository: repoData, + }); + + const present = await isRepoPresentInGitea({ + config, + owner, + repoName: repo.name, + }); + + if (present) { + await syncGiteaRepo({ config, repository: repoData }); + console.log(`Synced existing repo: ${repo.name}`); + } else { + if (!config.githubConfig.token) { + throw new Error("GitHub token is missing."); + } + + console.log(`Importing repo: ${repo.name} ${owner}`); + + const octokit = createGitHubClient(config.githubConfig.token); + if (repo.organization && config.githubConfig.preserveOrgStructure) { + await mirrorGitHubOrgRepoToGiteaOrg({ + config, + octokit, + orgName: repo.organization, + repository: { + ...repoData, + status: repoStatusEnum.parse("imported"), + }, + }); + } else { + await mirrorGithubRepoToGitea({ + config, + octokit, + repository: { + ...repoData, + status: repoStatusEnum.parse("imported"), + }, + }); + } + } + } catch (err) { + console.error(`Failed to retry repo ${repo.name}:`, err); + } + } + }, 0); + + const responsePayload: RetryRepoResponse = { + success: true, + message: "Retry job (sync/mirror) started.", + repositories: repos.map((repo) => ({ + ...repo, + status: repoStatusEnum.parse(repo.status), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + })), + }; + + return new Response(JSON.stringify(responsePayload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (err) { + console.error("Error retrying repo:", err); + return new Response( + JSON.stringify({ + error: err instanceof Error ? err.message : "An unknown error occurred", + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/job/schedule-sync-repo.ts b/src/pages/api/job/schedule-sync-repo.ts new file mode 100644 index 0000000..2152a4d --- /dev/null +++ b/src/pages/api/job/schedule-sync-repo.ts @@ -0,0 +1,154 @@ +import type { APIRoute } from "astro"; +import { db, configs, repositories } from "@/lib/db"; +import { eq, or } from "drizzle-orm"; +import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository"; +import { isRepoPresentInGitea, syncGiteaRepo } from "@/lib/gitea"; +import type { + ScheduleSyncRepoRequest, + ScheduleSyncRepoResponse, +} from "@/types/sync"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body: ScheduleSyncRepoRequest = await request.json(); + const { userId } = body; + + if (!userId) { + return new Response( + JSON.stringify({ + success: false, + error: "Missing userId in request body.", + repositories: [], + } satisfies ScheduleSyncRepoResponse), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch config for the user + const configResult = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + const config = configResult[0]; + + if (!config || !config.githubConfig.token) { + return new Response( + JSON.stringify({ + success: false, + error: "Config missing for the user or GitHub token not found.", + repositories: [], + } satisfies ScheduleSyncRepoResponse), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch repositories with status 'mirrored' or 'synced' + const repos = await db + .select() + .from(repositories) + .where( + eq(repositories.userId, userId) && + or( + eq(repositories.status, "mirrored"), + eq(repositories.status, "synced"), + eq(repositories.status, "failed") + ) + ); + + if (!repos.length) { + return new Response( + JSON.stringify({ + success: false, + error: + "No repositories found with status mirrored, synced or failed.", + repositories: [], + } satisfies ScheduleSyncRepoResponse), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Calculate nextRun and update lastRun and nextRun in the config + const currentTime = new Date(); + const interval = config.scheduleConfig?.interval ?? 3600; + const nextRun = new Date(currentTime.getTime() + interval * 1000); + + // Update the full giteaConfig object + await db + .update(configs) + .set({ + scheduleConfig: { + ...config.scheduleConfig, + lastRun: currentTime, + nextRun: nextRun, + }, + }) + .where(eq(configs.userId, userId)); + + // Start async sync in background + setTimeout(async () => { + for (const repo of repos) { + try { + // Only check Gitea presence if the repo failed previously + if (repo.status === "failed") { + const isPresent = await isRepoPresentInGitea({ + config, + owner: repo.owner, + repoName: repo.name, + }); + + if (!isPresent) { + continue; //silently skip if repo is not present in Gitea + } + } + + await syncGiteaRepo({ + config, + repository: { + ...repo, + status: repoStatusEnum.parse(repo.status), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + }, + }); + } catch (error) { + console.error(`Sync failed for repo ${repo.name}:`, error); + } + } + }, 0); + + const resPayload: ScheduleSyncRepoResponse = { + success: true, + message: "Sync job scheduled for eligible repositories.", + repositories: repos.map((repo) => ({ + ...repo, + status: repoStatusEnum.parse(repo.status), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + })), + }; + + return new Response(JSON.stringify(resPayload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error in scheduling sync:", error); + return new Response( + JSON.stringify({ + success: false, + error: + error instanceof Error ? error.message : "An unknown error occurred", + repositories: [], + } satisfies ScheduleSyncRepoResponse), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/job/sync-repo.ts b/src/pages/api/job/sync-repo.ts new file mode 100644 index 0000000..ceb039f --- /dev/null +++ b/src/pages/api/job/sync-repo.ts @@ -0,0 +1,114 @@ +import type { APIRoute } from "astro"; +import type { MirrorRepoRequest } from "@/types/mirror"; +import { db, configs, repositories } from "@/lib/db"; +import { eq, inArray } from "drizzle-orm"; +import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; +import { syncGiteaRepo } from "@/lib/gitea"; +import type { SyncRepoResponse } from "@/types/sync"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body: MirrorRepoRequest = await request.json(); + const { userId, repositoryIds } = body; + + if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) { + return new Response( + JSON.stringify({ + success: false, + message: "userId and repositoryIds are required.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + if (repositoryIds.length === 0) { + return new Response( + JSON.stringify({ + success: false, + message: "No repository IDs provided.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch config + const configResult = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + const config = configResult[0]; + + if (!config || !config.githubConfig.token) { + return new Response( + JSON.stringify({ error: "Config missing for the user or token." }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Fetch repos + const repos = await db + .select() + .from(repositories) + .where(inArray(repositories.id, repositoryIds)); + + if (!repos.length) { + return new Response( + JSON.stringify({ error: "No repositories found for the given IDs." }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Start async mirroring in background + setTimeout(async () => { + for (const repo of repos) { + try { + await syncGiteaRepo({ + config, + repository: { + ...repo, + status: repoStatusEnum.parse(repo.status), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + }, + }); + } catch (error) { + console.error(`Sync failed for repo ${repo.name}:`, error); + } + } + }, 0); + + const responsePayload: SyncRepoResponse = { + success: true, + message: "Sync job started.", + repositories: repos.map((repo) => ({ + ...repo, + status: repoStatusEnum.parse(repo.status), + organization: repo.organization ?? undefined, + lastMirrored: repo.lastMirrored ?? undefined, + errorMessage: repo.errorMessage ?? undefined, + forkedFrom: repo.forkedFrom ?? undefined, + visibility: repositoryVisibilityEnum.parse(repo.visibility), + })), + }; + + // Return the updated repo list to the user + return new Response(JSON.stringify(responsePayload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error in syncing repositories:", error); + return new Response( + JSON.stringify({ + error: + error instanceof Error ? error.message : "An unknown error occurred", + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/rough.ts b/src/pages/api/rough.ts new file mode 100644 index 0000000..818f7ca --- /dev/null +++ b/src/pages/api/rough.ts @@ -0,0 +1,75 @@ +//this is a rough api route that will delete all the orgs and repos in gitea. can be used for some other testing purposes as well + +import { configs, db } from "@/lib/db"; +import { deleteAllOrgs, deleteAllReposInGitea } from "@/lib/rough"; +import type { APIRoute } from "astro"; +import { eq } from "drizzle-orm"; + +export const GET: APIRoute = async ({ request }) => { + try { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return new Response(JSON.stringify({ error: "Missing userId" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + // Fetch user configuration + const userConfig = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + if (userConfig.length === 0) { + return new Response( + JSON.stringify({ error: "No configuration found for this user" }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const config = userConfig[0]; + + if (!config.githubConfig || !config.githubConfig.token) { + return new Response( + JSON.stringify({ error: "GitHub token is missing in config" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + //adjust this based on the orgs you you want to delete + await deleteAllOrgs({ + config, + orgs: [ + "Neucruit", + "initify", + "BitBustersx719", + "uiastra", + "conductor-oss", + "HackForge-JUSL", + "vercel", + ], + }); + + await deleteAllReposInGitea({ + config, + }); + + return new Response(JSON.stringify({ message: "Process completed." }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error in long-running process:", error); + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}; diff --git a/src/pages/api/sse/index.ts b/src/pages/api/sse/index.ts new file mode 100644 index 0000000..cf4f109 --- /dev/null +++ b/src/pages/api/sse/index.ts @@ -0,0 +1,68 @@ +import type { APIRoute } from "astro"; +import { redisSubscriber } from "@/lib/redis"; + +export const GET: APIRoute = async ({ request }) => { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return new Response("Missing userId", { status: 400 }); + } + + const channel = `mirror-status:${userId}`; + let isClosed = false; + + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + + const handleMessage = (ch: string, message: string) => { + if (isClosed || ch !== channel) return; + try { + controller.enqueue(encoder.encode(`data: ${message}\n\n`)); + } catch (err) { + console.error("Stream enqueue error:", err); + } + }; + + redisSubscriber.subscribe(channel, (err) => { + if (err) { + isClosed = true; + controller.error(err); + } + }); + + redisSubscriber.on("message", handleMessage); + + try { + controller.enqueue(encoder.encode(": connected\n\n")); + } catch (err) { + console.error("Initial enqueue error:", err); + } + + request.signal?.addEventListener("abort", () => { + if (!isClosed) { + isClosed = true; + redisSubscriber.off("message", handleMessage); + redisSubscriber.unsubscribe(channel); + controller.close(); + } + }); + }, + cancel() { + // extra safety in case cancel is triggered + if (!isClosed) { + isClosed = true; + redisSubscriber.unsubscribe(channel); + } + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +}; diff --git a/src/pages/api/sync/index.ts b/src/pages/api/sync/index.ts new file mode 100644 index 0000000..b823eb7 --- /dev/null +++ b/src/pages/api/sync/index.ts @@ -0,0 +1,177 @@ +import type { APIRoute } from "astro"; +import { db, organizations, repositories, configs } from "@/lib/db"; +import { eq } from "drizzle-orm"; +import { v4 as uuidv4 } from "uuid"; +import { createMirrorJob } from "@/lib/helpers"; +import { + createGitHubClient, + getGithubOrganizations, + getGithubRepositories, + getGithubStarredRepositories, +} from "@/lib/github"; +import { jsonResponse } from "@/lib/utils"; + +export const POST: APIRoute = async ({ request }) => { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return jsonResponse({ data: { error: "Missing userId" }, status: 400 }); + } + + try { + const [config] = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + if (!config) { + return jsonResponse({ + data: { error: "No configuration found for this user" }, + status: 404, + }); + } + + const token = config.githubConfig?.token; + + if (!token) { + return jsonResponse({ + data: { error: "GitHub token is missing in config" }, + status: 400, + }); + } + + const octokit = createGitHubClient(token); + + // Fetch GitHub data in parallel + const [basicAndForkedRepos, starredRepos, gitOrgs] = await Promise.all([ + getGithubRepositories({ octokit, config }), + config.githubConfig?.mirrorStarred + ? getGithubStarredRepositories({ octokit, config }) + : Promise.resolve([]), + getGithubOrganizations({ octokit, config }), + ]); + + const allGithubRepos = [...basicAndForkedRepos, ...starredRepos]; + + // Prepare full list of repos and orgs + const newRepos = allGithubRepos.map((repo) => ({ + id: uuidv4(), + userId, + configId: config.id, + name: repo.name, + fullName: repo.fullName, + url: repo.url, + cloneUrl: repo.cloneUrl, + owner: repo.owner, + organization: repo.organization, + isPrivate: repo.isPrivate, + isForked: repo.isForked, + forkedFrom: repo.forkedFrom, + hasIssues: repo.hasIssues, + isStarred: repo.isStarred, + isArchived: repo.isArchived, + size: repo.size, + hasLFS: repo.hasLFS, + hasSubmodules: repo.hasSubmodules, + defaultBranch: repo.defaultBranch, + visibility: repo.visibility, + status: repo.status, + lastMirrored: repo.lastMirrored, + errorMessage: repo.errorMessage, + createdAt: repo.createdAt, + updatedAt: repo.updatedAt, + })); + + const newOrgs = gitOrgs.map((org) => ({ + id: uuidv4(), + userId, + configId: config.id, + name: org.name, + avatarUrl: org.avatarUrl, + membershipRole: org.membershipRole, + isIncluded: false, + status: org.status, + repositoryCount: org.repositoryCount, + createdAt: new Date(), + updatedAt: new Date(), + })); + + let insertedRepos: typeof newRepos = []; + let insertedOrgs: typeof newOrgs = []; + + // Transaction to insert only new items + await db.transaction(async (tx) => { + const [existingRepos, existingOrgs] = await Promise.all([ + tx + .select({ fullName: repositories.fullName }) + .from(repositories) + .where(eq(repositories.userId, userId)), + tx + .select({ name: organizations.name }) + .from(organizations) + .where(eq(organizations.userId, userId)), + ]); + + const existingRepoNames = new Set(existingRepos.map((r) => r.fullName)); + const existingOrgNames = new Set(existingOrgs.map((o) => o.name)); + + insertedRepos = newRepos.filter( + (r) => !existingRepoNames.has(r.fullName) + ); + insertedOrgs = newOrgs.filter((o) => !existingOrgNames.has(o.name)); + + if (insertedRepos.length > 0) { + await tx.insert(repositories).values(insertedRepos); + } + + if (insertedOrgs.length > 0) { + await tx.insert(organizations).values(insertedOrgs); + } + }); + + // Create mirror jobs only for newly inserted items + const mirrorJobPromises = [ + ...insertedRepos.map((repo) => + createMirrorJob({ + userId, + repositoryId: repo.id, + repositoryName: repo.name, + status: "imported", + message: `Repository ${repo.name} fetched successfully`, + details: `Repository ${repo.name} was fetched from GitHub`, + }) + ), + ...insertedOrgs.map((org) => + createMirrorJob({ + userId, + organizationId: org.id, + organizationName: org.name, + status: "imported", + message: `Organization ${org.name} fetched successfully`, + details: `Organization ${org.name} was fetched from GitHub`, + }) + ), + ]; + + await Promise.all(mirrorJobPromises); + + return jsonResponse({ + data: { + success: true, + message: "Repositories and organizations synced successfully", + newRepositories: insertedRepos.length, + newOrganizations: insertedOrgs.length, + }, + }); + } catch (error) { + console.error("Error syncing GitHub data for user:", userId, error); + return jsonResponse({ + data: { + error: error instanceof Error ? error.message : "Something went wrong", + }, + status: 500, + }); + } +}; diff --git a/src/pages/api/sync/organization.ts b/src/pages/api/sync/organization.ts new file mode 100644 index 0000000..325ae0b --- /dev/null +++ b/src/pages/api/sync/organization.ts @@ -0,0 +1,136 @@ +import type { APIRoute } from "astro"; +import { Octokit } from "@octokit/rest"; +import { configs, db, organizations, repositories } from "@/lib/db"; +import { and, eq } from "drizzle-orm"; +import { jsonResponse } from "@/lib/utils"; +import type { + AddOrganizationApiRequest, + AddOrganizationApiResponse, +} from "@/types/organizations"; +import type { RepositoryVisibility, RepoStatus } from "@/types/Repository"; +import { v4 as uuidv4 } from "uuid"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body: AddOrganizationApiRequest = await request.json(); + const { role, org, userId } = body; + + if (!org || !userId || !role) { + return jsonResponse({ + data: { success: false, error: "Missing org, role or userId" }, + status: 400, + }); + } + + // Check if org already exists + const existingOrg = await db + .select() + .from(organizations) + .where( + and(eq(organizations.name, org), eq(organizations.userId, userId)) + ); + + if (existingOrg.length > 0) { + return jsonResponse({ + data: { + success: false, + error: "Organization already exists for this user", + }, + status: 400, + }); + } + + // Get user's config + const [config] = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + if (!config) { + return jsonResponse({ + data: { error: "No configuration found for this user" }, + status: 404, + }); + } + + const configId = config.id; + + const octokit = new Octokit(); + + // Fetch org metadata + const { data: orgData } = await octokit.orgs.get({ org }); + + // Fetch public repos using Octokit paginator + const publicRepos = await octokit.paginate(octokit.repos.listForOrg, { + org, + type: "public", + per_page: 100, + }); + + // Insert repositories + const repoRecords = publicRepos.map((repo) => ({ + id: uuidv4(), + userId, + configId, + name: repo.name, + fullName: repo.full_name, + url: repo.html_url, + cloneUrl: repo.clone_url ?? "", + owner: repo.owner.login, + organization: + repo.owner.type === "Organization" ? repo.owner.login : null, + isPrivate: repo.private, + isForked: repo.fork, + forkedFrom: undefined, + hasIssues: repo.has_issues, + isStarred: false, + isArchived: repo.archived, + size: repo.size, + hasLFS: false, + hasSubmodules: false, + defaultBranch: repo.default_branch ?? "main", + visibility: (repo.visibility ?? "public") as RepositoryVisibility, + status: "imported" as RepoStatus, + lastMirrored: undefined, + errorMessage: undefined, + createdAt: repo.created_at ? new Date(repo.created_at) : new Date(), + updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(), + })); + + await db.insert(repositories).values(repoRecords); + + // Insert organization metadata + const organizationRecord = { + id: uuidv4(), + userId, + configId, + name: orgData.login, + avatarUrl: orgData.avatar_url, + membershipRole: role, + isIncluded: false, + status: "imported" as RepoStatus, + repositoryCount: publicRepos.length, + createdAt: orgData.created_at ? new Date(orgData.created_at) : new Date(), + updatedAt: orgData.updated_at ? new Date(orgData.updated_at) : new Date(), + }; + + await db.insert(organizations).values(organizationRecord); + + const resPayload: AddOrganizationApiResponse = { + success: true, + organization: organizationRecord, + message: "Organization and repositories imported successfully", + }; + + return jsonResponse({ data: resPayload, status: 200 }); + } catch (error) { + console.error("Error inserting organization/repositories:", error); + return jsonResponse({ + data: { + error: error instanceof Error ? error.message : "Something went wrong", + }, + status: 500, + }); + } +}; diff --git a/src/pages/api/sync/repository.ts b/src/pages/api/sync/repository.ts new file mode 100644 index 0000000..332e641 --- /dev/null +++ b/src/pages/api/sync/repository.ts @@ -0,0 +1,137 @@ +import type { APIRoute } from "astro"; +import { Octokit } from "@octokit/rest"; +import { configs, db, repositories } from "@/lib/db"; +import { v4 as uuidv4 } from "uuid"; +import { and, eq } from "drizzle-orm"; +import { type Repository } from "@/lib/db/schema"; +import { jsonResponse } from "@/lib/utils"; +import type { + AddRepositoriesApiRequest, + AddRepositoriesApiResponse, + RepositoryVisibility, +} from "@/types/Repository"; +import { createMirrorJob } from "@/lib/helpers"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body: AddRepositoriesApiRequest = await request.json(); + const { owner, repo, userId } = body; + + if (!owner || !repo || !userId) { + return new Response( + JSON.stringify({ + success: false, + error: "Missing owner, repo, or userId", + }), + { status: 400 } + ); + } + + // Check if repository with the same owner, name, and userId already exists + const existingRepo = await db + .select() + .from(repositories) + .where( + and( + eq(repositories.owner, owner), + eq(repositories.name, repo), + eq(repositories.userId, userId) + ) + ); + + if (existingRepo.length > 0) { + return jsonResponse({ + data: { + success: false, + error: + "Repository with this name and owner already exists for this user", + }, + status: 400, + }); + } + + // Get user's active config + const [config] = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + if (!config) { + return jsonResponse({ + data: { error: "No configuration found for this user" }, + status: 404, + }); + } + + const configId = config.id; + + const octokit = new Octokit(); // No auth for public repos + + const { data: repoData } = await octokit.rest.repos.get({ owner, repo }); + + const metadata = { + id: uuidv4(), + userId, + configId, + name: repoData.name, + fullName: repoData.full_name, + url: repoData.html_url, + cloneUrl: repoData.clone_url, + owner: repoData.owner.login, + organization: + repoData.owner.type === "Organization" + ? repoData.owner.login + : undefined, + isPrivate: repoData.private, + isForked: repoData.fork, + forkedFrom: undefined, + hasIssues: repoData.has_issues, + isStarred: false, + isArchived: repoData.archived, + size: repoData.size, + hasLFS: false, + hasSubmodules: false, + defaultBranch: repoData.default_branch, + visibility: (repoData.visibility ?? "public") as RepositoryVisibility, + status: "imported" as Repository["status"], + lastMirrored: undefined, + errorMessage: undefined, + createdAt: repoData.created_at + ? new Date(repoData.created_at) + : new Date(), + updatedAt: repoData.updated_at + ? new Date(repoData.updated_at) + : new Date(), + }; + + await db.insert(repositories).values(metadata); + + createMirrorJob({ + userId, + organizationId: metadata.organization, + organizationName: metadata.organization, + repositoryId: metadata.id, + repositoryName: metadata.name, + status: "imported", + message: `Repository ${metadata.name} fetched successfully`, + details: `Repository ${metadata.name} was fetched from GitHub`, + }); + + const resPayload: AddRepositoriesApiResponse = { + success: true, + repository: metadata, + message: "Repository added successfully", + }; + + return jsonResponse({ data: resPayload, status: 200 }); + } catch (error) { + console.error("Error inserting repository:", error); + return jsonResponse({ + data: { + error: error instanceof Error ? error.message : "Something went wrong", + }, + status: 500, + }); + } +}; diff --git a/src/pages/config.astro b/src/pages/config.astro new file mode 100644 index 0000000..59fda44 --- /dev/null +++ b/src/pages/config.astro @@ -0,0 +1,24 @@ +--- +import '../styles/global.css'; +import App, { MainLayout } from '@/components/layout/MainLayout'; +import { ConfigTabs } from '@/components/config/ConfigTabs'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { db, configs } from '@/lib/db'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; +import { Button } from '@/components/ui/button'; +import type { SaveConfigApiRequest,SaveConfigApiResponse } from '@/types/config'; +--- + + + + + + + + Configuration - Gitea Mirror + + + + + + diff --git a/src/pages/docs/[slug].astro b/src/pages/docs/[slug].astro new file mode 100644 index 0000000..1c886f0 --- /dev/null +++ b/src/pages/docs/[slug].astro @@ -0,0 +1,63 @@ +--- +import { getCollection } from 'astro:content'; +import MainLayout from '../../layouts/main.astro'; + +// Enable prerendering for this dynamic route +export const prerender = true; + +// Generate static paths for all documentation pages +export async function getStaticPaths() { + const docs = await getCollection('docs'); + return docs.map(entry => ({ + params: { slug: entry.slug }, + props: { entry }, + })); +} + +// Get the documentation entry from props +const { entry } = Astro.props; +const { Content } = await entry.render(); +--- + + +
+ +
+
+ +
+
+ +
+
diff --git a/src/pages/docs/index.astro b/src/pages/docs/index.astro new file mode 100644 index 0000000..3777cb2 --- /dev/null +++ b/src/pages/docs/index.astro @@ -0,0 +1,44 @@ +--- +import { getCollection } from 'astro:content'; +import MainLayout from '../../layouts/main.astro'; +import { LuSettings, LuRocket, LuBookOpen } from 'react-icons/lu'; + +// Helper to pick an icon based on doc.slug +// We'll use inline conditional rendering instead of this function + +// Get all documentation entries, sorted by order +const docs = await getCollection('docs'); +const sortedDocs = docs.sort((a, b) => { + const orderA = a.data.order || 999; + const orderB = b.data.order || 999; + return orderA - orderB; +}); +--- + + +
+

Gitea Mirror Documentation

+

Browse guides and technical docs for Gitea Mirror.

+ + +
+
diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..854fb9f --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,70 @@ +--- +import '../styles/global.css'; +import App from '@/components/layout/MainLayout'; +import { db, repositories, mirrorJobs, client } from '@/lib/db'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; + +// Check if any users exist in the database +const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`); +const userCount = userCountResult.rows[0].count; + +// Redirect to signup if no users exist +if (userCount === 0) { + return Astro.redirect('/signup'); +} + +// Fetch data from the database +let repoData:any[] = []; +let activityData = []; + +try { + // Fetch repositories from database + const dbRepos = await db.select().from(repositories).limit(10); + repoData = dbRepos; + + // Fetch recent activity from mirror jobs + const jobs = await db.select().from(mirrorJobs).limit(10); + activityData = jobs.flatMap((job: any) => { + // Check if log exists before parsing + if (!job.log) { + console.warn(`Job ${job.id} has no log data`); + return []; + } + try { + const log = JSON.parse(job.log); + if (!Array.isArray(log)) { + console.warn(`Job ${job.id} log is not an array`); + return []; + } + return log.map((entry: any) => ({ + id: `${job.id}-${entry.timestamp}`, + message: entry.message, + timestamp: new Date(entry.timestamp), + status: entry.level, + })); + } catch (parseError) { + console.error(`Failed to parse log for job ${job.id}:`, parseError); + return []; + } + }).slice(0, 10); +} catch (error) { + console.error('Error fetching data:', error); + // Fallback to empty arrays if database access fails + repoData = []; + activityData = []; +} +--- + + + + + + + + Dashboard - Gitea Mirror + + + + + + \ No newline at end of file diff --git a/src/pages/login.astro b/src/pages/login.astro new file mode 100644 index 0000000..3cf82a6 --- /dev/null +++ b/src/pages/login.astro @@ -0,0 +1,33 @@ +--- +import '../styles/global.css'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; +import { LoginForm } from '@/components/auth/LoginForm'; +import { client } from '../lib/db'; + +// Check if any users exist in the database +const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`); +const userCount = userCountResult.rows[0].count; + +// Redirect to signup if no users exist +if (userCount === 0) { + return Astro.redirect('/signup'); +} + +const generator = Astro.generator; +--- + + + + + + + + Login - Gitea Mirror + + + +
+ +
+ + diff --git a/src/pages/markdown-page.md b/src/pages/markdown-page.md new file mode 100644 index 0000000..de11d38 --- /dev/null +++ b/src/pages/markdown-page.md @@ -0,0 +1,16 @@ +--- +title: 'Markdown + Tailwind' +layout: ../layouts/main.astro +--- + +
+
+ Tailwind classes also work in Markdown! +
+ + Go home + +
diff --git a/src/pages/organizations.astro b/src/pages/organizations.astro new file mode 100644 index 0000000..de2b450 --- /dev/null +++ b/src/pages/organizations.astro @@ -0,0 +1,22 @@ +--- +import '../styles/global.css'; +import App from '@/components/layout/MainLayout'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; + +--- + + + + + + + + Organizations - Gitea Mirror + + + + + + + + diff --git a/src/pages/repositories.astro b/src/pages/repositories.astro new file mode 100644 index 0000000..1888473 --- /dev/null +++ b/src/pages/repositories.astro @@ -0,0 +1,21 @@ +--- +import '../styles/global.css'; +import App from '@/components/layout/MainLayout'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; +--- + + + + + + + + Repositories - Gitea Mirror + + + + + + + + diff --git a/src/pages/signup.astro b/src/pages/signup.astro new file mode 100644 index 0000000..d7f09d3 --- /dev/null +++ b/src/pages/signup.astro @@ -0,0 +1,37 @@ +--- +import '../styles/global.css'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; +import { SignupForm } from '@/components/auth/SignupForm'; +import { client } from '../lib/db'; + +// Check if any users exist in the database +const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`); +const userCount = userCountResult.rows[0]?.count; + +// Redirect to login if users already exist +if (userCount !== null && Number(userCount) > 0) { + return Astro.redirect('/login'); +} + +const generator = Astro.generator; +--- + + + + + + + + Setup Admin Account - Gitea Mirror + + + +
+
+

Welcome to Gitea Mirror

+

Let's set up your administrator account to get started.

+
+ +
+ + diff --git a/src/styles/docs.css b/src/styles/docs.css new file mode 100644 index 0000000..d246629 --- /dev/null +++ b/src/styles/docs.css @@ -0,0 +1,63 @@ +/* Enhanced Markdown/Docs styling for Tailwind Typography */ +.prose { + --tw-prose-body: #e5e7eb; + --tw-prose-headings: #fff; + --tw-prose-links: #60a5fa; + --tw-prose-bold: #fff; + --tw-prose-codes: #fbbf24; + --tw-prose-pre-bg: #18181b; + --tw-prose-pre-color: #f3f4f6; + --tw-prose-hr: #374151; + --tw-prose-quotes: #a3e635; + font-size: 1.1rem; + line-height: 1.8; +} + +.prose h1, .prose h2, .prose h3 { + font-weight: 700; + margin-top: 2.5rem; + margin-bottom: 1rem; +} + +.prose pre { + background: #18181b; + color: #f3f4f6; + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; +} + +.prose code { + background: #23272e; + color: #fbbf24; + border-radius: 0.3rem; + padding: 0.2em 0.4em; + font-size: 0.95em; +} + +.prose table { + width: 100%; + border-collapse: collapse; + margin: 1.5rem 0; +} + +.prose th, .prose td { + border: 1px solid #374151; + padding: 0.5rem 1rem; +} + +.prose blockquote { + border-left: 4px solid #60a5fa; + background: #1e293b; + color: #a3e635; + padding: 1rem 1.5rem; + margin: 1.5rem 0; + font-style: italic; +} + +/* Mermaid diagrams should be responsive */ +.prose .mermaid svg { + width: 100% !important; + height: auto !important; + background: transparent; +} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..b95ba51 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,148 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* ===== Scrollbar Styles ===== */ + +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: oklch(0.8 0 0); /* light grayish */ + border-radius: 9999px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: oklch(0.8 0 0); +} + +/* Dark Mode Scrollbar */ +.dark ::-webkit-scrollbar-thumb { + background-color: oklch(0.4 0 0); /* dark thumb */ +} + +.dark ::-webkit-scrollbar-thumb:hover { + background-color: oklch(0.6 0 0); +} diff --git a/src/tests/example.test.ts b/src/tests/example.test.ts new file mode 100644 index 0000000..9b3fd5e --- /dev/null +++ b/src/tests/example.test.ts @@ -0,0 +1,8 @@ +// example.test.ts +import { describe, it, expect } from "vitest"; + +describe("Example Test", () => { + it("should pass", () => { + expect(true).toBe(true); + }); +}); diff --git a/src/tests/setup.ts b/src/tests/setup.ts new file mode 100644 index 0000000..c980f9c --- /dev/null +++ b/src/tests/setup.ts @@ -0,0 +1,8 @@ +import "@testing-library/jest-dom"; +import { expect, afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; + +// Run cleanup after each test case (e.g. clearing jsdom) +afterEach(() => { + cleanup(); +}); diff --git a/src/types/Repository.ts b/src/types/Repository.ts new file mode 100644 index 0000000..387a4af --- /dev/null +++ b/src/types/Repository.ts @@ -0,0 +1,82 @@ +import type { Repository } from "@/lib/db/schema"; +import { z } from "zod"; + +export const repoStatusEnum = z.enum([ + "imported", + "mirroring", + "mirrored", + "failed", + "syncing", + "synced", +]); + +export type RepoStatus = z.infer; + +export const repositoryVisibilityEnum = z.enum([ + "public", + "private", + "internal", +]); + +export type RepositoryVisibility = z.infer; + +export interface RepositoryApiSuccessResponse { + success: true; + message: string; + repositories: Repository[]; +} + +export interface RepositoryApiErrorResponse { + success: false; + error: string; + message?: string; +} + +export type RepositoryApiResponse = + | RepositoryApiSuccessResponse + | RepositoryApiErrorResponse; + +export interface GitRepo { + name: string; + fullName: string; + url: string; + cloneUrl: string; + + owner: string; + organization?: string; + + isPrivate: boolean; + isForked: boolean; + forkedFrom?: string; + + hasIssues: boolean; + isStarred: boolean; + isArchived: boolean; + + size: number; + hasLFS: boolean; + hasSubmodules: boolean; + + defaultBranch: string; + visibility: RepositoryVisibility; + + status: RepoStatus; + lastMirrored?: Date; + errorMessage?: string; + + createdAt: Date; + updatedAt: Date; +} + +export interface AddRepositoriesApiRequest { + userId: string; + repo: string; + owner: string; +} + +export interface AddRepositoriesApiResponse { + success: boolean; + message: string; + repository: Repository; + error?: string; +} diff --git a/src/types/Sidebar.ts b/src/types/Sidebar.ts new file mode 100644 index 0000000..bb31ffd --- /dev/null +++ b/src/types/Sidebar.ts @@ -0,0 +1,14 @@ +import * as React from "react"; + +export type Paths = + | "/" + | "/repositories" + | "/organizations" + | "/config" + | "/activity"; + +export interface SidebarItem { + href: Paths; + label: string; + icon: React.ElementType; +} diff --git a/src/types/activities.ts b/src/types/activities.ts new file mode 100644 index 0000000..abbe3c4 --- /dev/null +++ b/src/types/activities.ts @@ -0,0 +1,10 @@ +import type { MirrorJob } from "@/lib/db/schema"; +import { z } from "zod"; + +export const activityLogLevelEnum = z.enum(["info", "warning", "error", ""]); + +export interface ActivityApiResponse { + success: boolean; + message: string; + activities: MirrorJob[]; +} diff --git a/src/types/config.ts b/src/types/config.ts new file mode 100644 index 0000000..8d35247 --- /dev/null +++ b/src/types/config.ts @@ -0,0 +1,63 @@ +import { type Config as ConfigType } from "@/lib/db/schema"; + +export type GiteaOrgVisibility = "public" | "private" | "limited"; + +export interface GiteaConfig { + url: string; + username: string; + token: string; + organization: string; + visibility: GiteaOrgVisibility; + starredReposOrg: string; +} + +export interface ScheduleConfig { + enabled: boolean; + interval: number; + lastRun?: Date; + nextRun?: Date; +} + +export interface GitHubConfig { + username: string; + token: string; + skipForks: boolean; + privateRepositories: boolean; + mirrorIssues: boolean; + mirrorStarred: boolean; + preserveOrgStructure: boolean; + skipStarredIssues: boolean; +} + +export interface SaveConfigApiRequest { + userId: string; + githubConfig: GitHubConfig; + giteaConfig: GiteaConfig; + scheduleConfig: ScheduleConfig; +} + +export interface SaveConfigApiResponse { + success: boolean; + message: string; +} + +export interface Config extends ConfigType {} + +export interface ConfigApiRequest { + userId: string; +} + +export interface ConfigApiResponse { + id: string; + userId: string; + name: string; + isActive: boolean; + githubConfig: GitHubConfig; + giteaConfig: GiteaConfig; + scheduleConfig: ScheduleConfig; + include: string[]; + exclude: string[]; + createdAt: Date; + updatedAt: Date; + error?: string; +} diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts new file mode 100644 index 0000000..81d7cac --- /dev/null +++ b/src/types/dashboard.ts @@ -0,0 +1,25 @@ +import type { Repository } from "@/lib/db/schema"; +import type { Organization } from "@/lib/db/schema"; +import type { MirrorJob } from "@/lib/db/schema"; + +export interface DashboardApiSuccessResponse { + success: true; + message: string; + repoCount: number; + orgCount: number; + mirroredCount: number; + repositories: Repository[]; + organizations: Organization[]; + activities: MirrorJob[]; + lastSync: Date | null; +} + +export interface DashboardApiErrorResponse { + success: false; + error: string; + message?: string; +} + +export type DashboardApiResponse = + | DashboardApiSuccessResponse + | DashboardApiErrorResponse; diff --git a/src/types/filter.ts b/src/types/filter.ts new file mode 100644 index 0000000..641de55 --- /dev/null +++ b/src/types/filter.ts @@ -0,0 +1,12 @@ +import type { MembershipRole } from "./organizations"; +import type { RepoStatus } from "./Repository"; + +export interface FilterParams { + searchTerm?: string; + status?: RepoStatus | ""; // repos, activity and orgs status + membershipRole?: MembershipRole | ""; //membership role in orgs + owner?: string; // owner of the repos + organization?: string; // organization of the repos + type?: string; //types in activity log + name?: string; // name in activity log +} diff --git a/src/types/mirror.ts b/src/types/mirror.ts new file mode 100644 index 0000000..a687a01 --- /dev/null +++ b/src/types/mirror.ts @@ -0,0 +1,30 @@ +import type { Organization, Repository } from "@/lib/db/schema"; + +export interface MirrorRepoRequest { + userId: string; + repositoryIds: string[]; +} + +export interface MirrorRepoResponse { + success: boolean; + error?: string; + message?: string; + repositories: Repository[]; +} + +export interface MirrorOrgRequest { + userId: string; + organizationIds: string[]; +} + +export interface MirrorOrgRequest { + userId: string; + organizationIds: string[]; +} + +export interface MirrorOrgResponse { + success: boolean; + error?: string; + message?: string; + organizations: Organization[]; +} diff --git a/src/types/organizations.ts b/src/types/organizations.ts new file mode 100644 index 0000000..c829870 --- /dev/null +++ b/src/types/organizations.ts @@ -0,0 +1,51 @@ +import type { Organization } from "@/lib/db/schema"; +import { z } from "zod"; +import type { RepoStatus } from "./Repository"; + +export const membershipRoleEnum = z.enum([ + "member", + "admin", + "billing_manager", +]); + +export type MembershipRole = z.infer; + +export interface OrganizationsApiSuccessResponse { + success: true; + message: string; + organizations: Organization[]; +} + +export interface OrganizationsApiErrorResponse { + success: false; + error: string; + message?: string; +} + +export type OrganizationsApiResponse = + | OrganizationsApiSuccessResponse + | OrganizationsApiErrorResponse; + +export interface GitOrg { + name: string; + avatarUrl: string; + membershipRole: MembershipRole; + isIncluded: boolean; + status: RepoStatus; + repositoryCount: number; + createdAt: Date; + updatedAt: Date; +} + +export interface AddOrganizationApiRequest { + userId: string; + org: string; + role: MembershipRole; +} + +export interface AddOrganizationApiResponse { + success: boolean; + message: string; + organization: Organization; + error?: string; +} diff --git a/src/types/retry.ts b/src/types/retry.ts new file mode 100644 index 0000000..63631d7 --- /dev/null +++ b/src/types/retry.ts @@ -0,0 +1,13 @@ +import type { Repository } from "@/lib/db/schema"; + +export interface RetryRepoRequest { + userId: string; + repositoryIds: string[]; +} + +export interface RetryRepoResponse { + success: boolean; + error?: string; + message?: string; + repositories: Repository[]; +} diff --git a/src/types/sync.ts b/src/types/sync.ts new file mode 100644 index 0000000..24c14b2 --- /dev/null +++ b/src/types/sync.ts @@ -0,0 +1,24 @@ +import type { Repository } from "@/lib/db/schema"; + +export interface SyncRepoRequest { + userId: string; + repositoryIds: string[]; +} + +export interface SyncRepoResponse { + success: boolean; + error?: string; + message?: string; + repositories: Repository[]; +} + +export interface ScheduleSyncRepoRequest { + userId: string; +} + +export interface ScheduleSyncRepoResponse { + success: boolean; + error?: string; + message?: string; + repositories: Repository[]; +} diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..7fb994a --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,8 @@ +import type { User } from "@/lib/db/schema"; + +export interface ExtendedUser extends User { + syncEnabled: boolean; + syncInterval: number; + lastSync: Date | null; + nextSync: Date | null; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..90604a7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [ + ".astro/types.d.ts", + "**/*" + ], + "exclude": [ + "dist" + ], + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + } + } +} \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..40977ad --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import { fileURLToPath } from 'url'; +import path from 'path'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/tests/setup.ts'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +});