diff --git a/.bashmainrpc b/.bashmainrpc new file mode 100644 index 0000000..b7353bb --- /dev/null +++ b/.bashmainrpc @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +MODEL_FILE="model.json" +RPC_LOG="/tmp/bashrpc.log" + +# Load JSON model +model=$(jq '.' "$MODEL_FILE") + +# Dynamically handle calls +handle_rpc() { + local service="$1" + local method="$2" + local data="$3" + + # Check if service/method exist in model + if ! jq -e ".services.\"$service\".methods.\"$method\"" <<< "$model" >/dev/null; then + echo "{\"error\":\"Unknown method $service.$method\"}" + return + fi + + # Execute service + case "$service.$method" in + ai.ask) + curl -s https://api.openai.com/v1/chat/completions \ + -H "Authorization: Bearer $GPT_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"$(jq -r ".services.ai.model" <<< "$model")\", + \"messages\": [{\"role\":\"user\",\"content\":\"$data\"}] + }" | jq -r '.choices[0].message.content' + ;; + sys.info) + echo "{\"hostname\":\"$(hostname)\",\"uptime\":\"$(uptime -p)\"}" + ;; + *) + echo "{\"error\":\"Service not implemented yet\"}" + ;; + esac +} + +# Example local call +# ./bashmainrpc ai ask "Explain MDSE" +handle_rpc "$1" "$2" "$3" diff --git a/.bashrpc b/.bashrpc new file mode 100644 index 0000000..aaabb97 --- /dev/null +++ b/.bashrpc @@ -0,0 +1,29 @@ +docker build --build-arg CONFIGCAT_AUTH_KEY=yourkeyhere --build-arg JOB_ID=1234 -t myreactapp . +docker run -p 3000:3000 -e JOB_ID=1234 myreactapp +redis-server +uvicorn enclov_ai_backend:app --reload +celery -A enclov_ai_backend.celery_app worker --loglevel=info +pip install fastapi uvicorn celery redis pydantic mlflow optuna shap +pip install -r requirements.txt +base64 -w 0 kubeconfig.yaml +docker run --net=host -it -e NGROK_AUTHTOKEN=2y96VwzmhS1YV6vgll2Ua411oGc_755Qcpps22pznftiuGZWr ngrok/ngrok:latest http --url=above-feasible-lobster.ngrok-free.app 80 +docker run --net=host -it -e NGROK_AUTHTOKEN=2y96VwzmhS1YV6vgll2Ua411oGc_755Qcpps22pznftiuGZWr ngrok/ngrok:latest http 80 +docker pull ngrok/ngrok +ngrok config add-authtoken 2y96VwzmhS1YV6vgll2Ua411oGc_755Qcpps22pznftiuGZWr +npm install -r requirements.txt +cd enclov-AI +mkdir frontend +cd frontend +npx create-next-app@latest . + +cat << 'EOF' | git apply +diff --git a/.github/workflows/gen-man.yml b/.github/workflows/gen-man.yml +--- a/.github/workflows/gen-man.yml ++++ b/.github/workflows/gen-man.yml +@@ -2,2 +2,5 @@ + ++permissions: ++ contents: read ++ + on: +EOF diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..09e4418 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,33 @@ +version: 2.1 + +orbs: + docker: circleci/docker@2.1.1 + +jobs: + build_and_push: + docker: + - image: cimg/python:3.11 + steps: + - checkout + + - setup_remote_docker: + docker_layer_caching: true + + - run: + name: Install dependencies + command: pip install -r requirements.txt + + - docker/build: + image: web4application.github.io/enclov-AI + tag: latest + path: . + extra_build_args: --compress + + - docker/push: + image: web4application.github.io/enclov-AI + tag: latest + +workflows: + build_and_push_workflow: + jobs: + - build_and_push diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..619b893 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/devcontainers/python:3.11 + +# Install Node.js and npm +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y nodejs + +# Install global tools if needed (optional) +RUN npm install -g serve + +# Set up Python packages early to cache them in Docker layer +COPY requirements.txt /tmp/requirements.txt +RUN pip install --upgrade pip && pip install -r /tmp/requirements.txt diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3ef51df --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "name": "Enclov-AI Dev Container", + "dockerFile": "Dockerfile", + + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/local/bin/python", + "editor.formatOnSave": true + }, + + "extensions": [ + "ms-python.python", + "ms-toolsai.jupyter", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "formulahendry.auto-close-tag", + "formulahendry.auto-rename-tag" + ], + + "forwardPorts": [3000, 8000, 8501], // React / FastAPI / Streamlit or similar + + "postCreateCommand": "pip install -r requirements.txt && npm install", + + "remoteUser": "vscode" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c46a422 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +# Python cache files +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Virtual environments +env/ +venv/ + +# Git files (don’t send your git repo to Docker build context) +.git +.gitignore + +# Dockerfile itself (optional, but usually you want to exclude it from context) +Dockerfile + +# Markdown docs (usually not needed inside container) +*.md + +# Test folders/files (not needed in production image) +tests/ diff --git a/.env b/.env new file mode 100644 index 0000000..667fb62 --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +GITHUB_WEBHOOK_SECRET=84298ed9f2470f159b48e36e76eb90eeac061941 +GITHUB_APP_ID=1325944 +GITHUB_PRIVATE_KEY_PATH=./keys/private-key.pem +google_API_KEY=AIzaSyAvrxOyAVzPVcnzxuD0mjKVDyS2bNWfC10 +GOOGLE_API_KEY=AlzaSyCHjfdo3w160Dd5yTVJD409pWmigOJEg +GITHUB_REPOSITORY=web4application/enclove-ai +PR_NUMBER=123 +GEMINI_API_KEY= + OPENAI_Api_KEY=gG1uZhj50x1lYFKrrB5kT3BlbkFJXP3R63ExWT9lkcHI0pRq +GITHUB_TOKEN=ghp_5ea33wQ9MnPEuszphuVv5X5jjiYRWT3uXtQ +GITHUB_TOKEN=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 +SENDGRID_API_KEY=your_sendgrid_api_key +EMAIL_TO=kubulee@gmail.com +WEBHOOK_PROXY_URL=https://smee.io/pMGkyDT3VfoxjdTK diff --git a/.env.local b/.env.local deleted file mode 100644 index 9f24580..0000000 --- a/.env.local +++ /dev/null @@ -1,5 +0,0 @@ -GITHUB_TOKEN=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 -GITHUB_REPOSITORY=owner/repo -PR_NUMBER=123 -GITHUB_TOKEN=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 -GITHUB_TOKEN=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 diff --git a/.github/workflows/_cd_ai_analysis.yml b/.github/workflows/_cd_ai_analysis.yml new file mode 100644 index 0000000..9fd9c04 --- /dev/null +++ b/.github/workflows/_cd_ai_analysis.yml @@ -0,0 +1,42 @@ +name: Deploy enclov-AI to Kubernetes + +on: + push: + branches: [main] + +permissions: + missions: + contents: read + pull-requests: write + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ enclovai }} + password: ${{ 84298ed9f2470f159b48e36e76eb90eeac061941 }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + push: true + tags: ${{ web4application.github.io/enclov-ai:latest}} + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Deploy to Kubernetes + run: kubectl apply -f k8s/ + env: + KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.github/workflows/ai-code-review.yml b/.github/workflows/ai-code-review.yml new file mode 100644 index 0000000..0e4abcd --- /dev/null +++ b/.github/workflows/ai-code-review.yml @@ -0,0 +1,22 @@ +name: AI Code Review + +on: + pull_request: + types: [opened, synchronize, reopened] + permission: + contents: read + pull-requests: write + +jobs: + ai_review: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Run AI Code Reviewer + uses: ./ai-code-reviewer + with: + _sk-svcacct-har5GKODfk8-tj201s33ZljryjcWD4W7-6H2Ub5yEBSvP8axegYfd69_j3WKhLwYwkQW0VRv0OT3BlbkFJBbshA3_NYfZSR6l77qSgPrj9Crgtr8KZPPoFJrZPTlUSYu1FfM-rKu70ETKAMctd9eu0G8RJgA", + GITHUB_TOKEN=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 diff --git a/.github/workflows/ai_pr_review.yml b/.github/workflows/ai_pr_review.yml deleted file mode 100644 index 7fd2726..0000000 --- a/.github/workflows/ai_pr_review.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: AI PR Code Review - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - ai_review: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - - name: Install dependencies - run: pip install PyGithub - - - name: Run AI Review - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: python ai_code_review.py diff --git a/.github/workflows/ci_cd_ai_analysis.yml b/.github/workflows/ci_cd_ai_analysis.yml deleted file mode 100644 index 1eeba90..0000000 --- a/.github/workflows/ci_cd_ai_analysis.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: AI Project Analysis Pipeline - -on: - pull_request: - branches: - - main - - develop - -jobs: - ai_analysis: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Run AI task extraction and repo analysis - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AI_MODEL: "github" # or "openai" - run: | - python -c " -import os -from project_pilot_ai.github_models import ModelClient -from project_pilot_ai.config import get_model_config - -base_url, model_name = get_model_config(os.getenv('AI_MODEL', 'github')) -token = os.getenv('GITHUB_TOKEN') -client = ModelClient(base_url=base_url, model=model_name, token=token) - -# Example: analyze a dummy transcript (replace with real data or files) -transcript = 'Meeting started at 10am. Task A assigned to John by next Friday.' -prompt = f'Extract tasks from meeting transcript: {transcript}' - -response = client.ask([ - {'role': 'system', 'content': 'You are a helpful assistant.'}, - {'role': 'user', 'content': prompt} -]) -print('Extracted Tasks:', response) - -# Example repo analysis prompt (replace with actual file tree) -file_tree = 'README.md\nsrc/\ntests/\n.gitignore' -goals = 'Make project scalable, maintainable, and secure.' -prompt_repo = f'Analyze project structure:\\n{file_tree}\\nGoals:\\n{goals}' - -response_repo = client.ask([ - {'role': 'system', 'content': 'You are an expert project architect.'}, - {'role': 'user', 'content': prompt_repo} -]) -print('Repo Analysis:', response_repo) -" diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 0000000..2077feb --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,51 @@ +name: Docker CI for enclov-AI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Docker login + uses: docker/login-action@v2 + with: + username: ${{ secrets.enclov-AI }} + password: ${{ secrets.84298ed9f2470f159b48e36e76eb90eeac061941 }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: ./wd + file: ./wd/Dockerfile + push: true + tags: my-registry/my-image:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + + - name: Move cache for next build + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..3f53646 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..ec0f253 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,98 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + schedule: + - cron: '26 8 * * *' + push: + branches: [ "main" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "main" ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/.github/workflows/jekyll-docker.yml b/.github/workflows/jekyll-docker.yml new file mode 100644 index 0000000..3d0eedb --- /dev/null +++ b/.github/workflows/jekyll-docker.yml @@ -0,0 +1,20 @@ +name: Jekyll site CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build the site in the jekyll/builder container + run: | + docker run \ + -v ${{ github.workspace }}:/srv/jekyll -v ${{ github.workspace }}/_site:/srv/jekyll/_site \ + jekyll/builder:latest /bin/bash -c "chmod -R 777 /srv/jekyll && jekyll build --future" diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml new file mode 100644 index 0000000..ed74736 --- /dev/null +++ b/.github/workflows/nextjs.yml @@ -0,0 +1,93 @@ +# Sample workflow for building and deploying a Next.js site to GitHub Pages +# +# To get started with Next.js see: https://nextjs.org/docs/getting-started +# +name: Deploy Next.js site to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Detect package manager + id: detect-package-manager + run: | + if [ -f "${{ github.workspace }}/yarn.lock" ]; then + echo "manager=yarn" >> $GITHUB_OUTPUT + echo "command=install" >> $GITHUB_OUTPUT + echo "runner=yarn" >> $GITHUB_OUTPUT + exit 0 + elif [ -f "${{ github.workspace }}/package.json" ]; then + echo "manager=npm" >> $GITHUB_OUTPUT + echo "command=ci" >> $GITHUB_OUTPUT + echo "runner=npx --no-install" >> $GITHUB_OUTPUT + exit 0 + else + echo "Unable to determine package manager" + exit 1 + fi + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: ${{ steps.detect-package-manager.outputs.manager }} + - name: Setup Pages + uses: actions/configure-pages@v5 + with: + # Automatically inject basePath in your Next.js configuration file and disable + # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). + # + # You may remove this line if you want to manage the configuration yourself. + static_site_generator: next + - name: Restore cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- + - name: Install dependencies + run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} + - name: Build with Next.js + run: ${{ steps.detect-package-manager.outputs.runner }} next build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./out + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..f2c9e97 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: '.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..975a7cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +env/ +venv/ +ENV/ +*.env.local +*.env + +# Node.js +node_modules/ +build/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Docker +*.log +docker-compose.override.yml +.env + +# VSCode +.vscode/ + +# MacOS +.DS_Store + +# Editors and IDEs +*.swp +*.swo +*.idea/ +*.iml + +# Logs +*.log + +# Coverage reports +htmlcov/ +.coverage +.coverage.* + +# pytest cache +.cache/ +.pytest_cache/ + +# mypy +.mypy_cache/ diff --git a/githubmodelclient.py b/Cli/main.js similarity index 86% rename from githubmodelclient.py rename to Cli/main.js index a90a358..a34cd9f 100644 --- a/githubmodelclient.py +++ b/Cli/main.js @@ -2,7 +2,7 @@ class ModelClient: def __init__(self, base_url: str, model: str, token: str): - self.client = OpenAI(base_url=base_url, api_key=token) + self.client = OpenAI(base_url=https://models.github.ai/inference, api_key=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) self.model = model def ask(self, messages, retries=3, delay=2): diff --git a/Deploy.sh b/Deploy.sh new file mode 100644 index 0000000..e0816ee --- /dev/null +++ b/Deploy.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +APP_NAME="enclov-AI" +REPO_URL="https://github.com/Web4application/enclov-AI.git" +FRONTEND_PATH="./index.html" +BACKEND_PATH="./app" +VERCEL_PROJECT_NAME="enclov-ai-frontend" + +echo "πŸš€ Deploying $APP_NAME..." + +# Step 1: Pull latest +if [ ! -d "$APP_NAME" ]; then + git clone $https://github.com/Web4application/enclov-AI.git +else + echo "πŸ”„ Repo already exists. Pulling latest..." + cd $APP_NAME && git pull && cd .. +fi + +cd $enclove-AI + +# Step 2: Check Docker Compose +echo "🐳 Starting Docker Compose stack..." +docker-compose up -d --build + +# Step 3: Vercel Frontend Deploy (if installed) +if command -v vercel &> /dev/null; then + echo "🌐 Deploying static frontend to Vercel..." + cd frontend || mkdir frontend && cp $FRONTEND_PATH frontend/index.html && cd frontend + vercel --prod --confirm --name "$VERCEL_PROJECT_NAME" + cd .. +else + echo "⚠️ Vercel CLI not found. Skipping frontend deploy." + echo "Install via: npm i -g vercel" +fi + +echo "βœ… Deployment complete." diff --git a/Dist/index.html b/Dist/index.html new file mode 100644 index 0000000..e5798c0 --- /dev/null +++ b/Dist/index.html @@ -0,0 +1,38 @@ + + +Enclov CLI Manual - TOC + + + +

Enclov CLI Manual

+
    +
  • enclov start command
  • + + + diff --git a/README.md b/README.md index 319a58c..9dffbf6 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,177 @@ -# AI-Powered PR Code Reviewer +[![Build and Deploy Enclov CLI Docs](https://github.com/Web4application/enclov-AI/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/Web4application/enclov-AI/actions/workflows/deploy-docs.yml) +# enclov-AI πŸ§ πŸ€– -This repository hosts an AI-powered GitHub pull request code review system that analyzes diffs and posts review comments automatically. +**AI-Powered GitHub Pull Request Reviewer** +_Automate code reviews with GPT-powered intelligence. FastAPI + Celery + Redis. Dockerized & DevOps-ready._ -## Features +--- + +## ✨ Overview + +`enclov-AI` is your AI sidekick for PR reviews. It listens to GitHub webhook events, analyzes code diffs using OpenAI models, and posts smart, context-aware comments right into your pull requests. + +Whether you're tired of nitpicks, want consistent reviews, or just love dev automation β€” `enclov-AI` ships your code with confidence. + +--- + +## πŸš€ Features + +- βœ… **GPT-powered code review suggestions** +- πŸ”„ **GitHub webhook integration** +- ⏱️ **Async processing with Celery & Redis** +- 🐳 **Full Docker support for dev/prod** +- πŸ›‘οΈ **Scalable FastAPI backend** +- πŸ§ͺ Unit-tested core logic (soonβ„’) + +--- + +## πŸ› οΈ Architecture + +``` + +GitHub Webhook β†’ FastAPI Web Server β†’ Celery Task Queue β†’ OpenAI API +↓ +Redis (Broker) + +```` + +--- + +## πŸ“¦ Tech Stack + +| Layer | Tech | +|---------------|-------------------------| +| Backend | FastAPI | +| AI Review | OpenAI GPT (via API) | +| Async Queue | Celery + Redis | +| Deployment | Docker / Vercel | +| Frontend | Static HTML (Dark UI) | + +--- + +## βš™οΈ Installation + +Clone the repo: + +```bash +git clone https://github.com/Web4application/enclov-AI.git +cd enclov-AI +```` -- Analyzes PR diffs using AI to identify bugs, security issues, and style problems. -- Posts AI-generated review comments directly on the PR. -- Runs automatically on PR open, update, or reopen events. -- Easy to extend with new languages, prompts, and integrations. +### 🐳 Docker (Full stack) -## Setup +```bash +docker-compose up --build +``` -1. **Create a GitHub repository** and push this code. +By default, this starts: -2. **Add the GitHub Actions workflow** in `.github/workflows/ai_pr_review.yml`. +* `api`: FastAPI backend on `http://localhost:8000` +* `worker`: Celery task processor +* `redis`: Message broker -3. **Ensure your repo has a GitHub token** set as a secret (usually `GITHUB_TOKEN` is provided automatically). +--- + +## πŸ” Environment Setup + +Create a `.env` file in the root: + +```env +OPENAI_API_KEY=your_openai_key_here +GITHUB_APP_SECRET=your_github_webhook_secret +``` + +You can generate GitHub secrets under your App configuration. + +--- + +## πŸ§ͺ Webhook Testing + +* Use [smee.io](https://smee.io/) or ngrok to tunnel `http://localhost:8000/webhook/github` +* Push a PR to your GitHub repo +* Watch the logs for the AI-generated feedback -4. **Configure environment variables** as needed if running locally (see `.env.example`). +--- + +## πŸ“€ Deployment -5. **Push a PR** to trigger the workflow and see AI comments on your PR. +### βœ… Local Dev -## Development +Use Docker Compose (see `docker-compose.yml`): -- The main logic is in `ai_code_review.py`. -- Modify prompts or add parsing logic to improve AI feedback precision. -- Add support for inline comments by mapping AI feedback to diff positions. +```bash +docker-compose up +``` -## Contribution +### 🌐 Vercel Frontend + Backend -Contributions, issues, and feature requests are welcome. Please open an issue or pull request. +You can deploy the static HTML to [Vercel](https://vercel.com/) and the FastAPI backend separately (Render, Fly.io, Railway). --- -**Keep your code clean. Let AI handle the hard part.** +## 🧠 Want to Extend? + +* Add support for GitHub Checks API or GitHub Comments +* Integrate Claude/Sonar/CodeQL for hybrid analysis +* Add CI/CD with GitHub Actions to auto-deploy containers + +--- + +## πŸ‘€ Author + +**Kubu Lee** +[GitHub](https://github.com/Web4application) β€’ `enclov-AI` Maintainer β€’ Builder of AI, Web4, & Insight Engines + +--- + +## πŸ›‘οΈ License + +MIT β€” Use it, fork it, AI-ify your stack. + +```` + +--- +## βš™οΈ `deploy.sh` β€” Dev/Prod Deployment Script + +```bash +#!/bin/bash +set -e + +APP_NAME="enclov-AI" +REPO_URL="https://github.com/Web4application/enclov-AI.git" +FRONTEND_PATH="./index.html" +BACKEND_PATH="./app" +VERCEL_PROJECT_NAME="enclov-ai-frontend" + +echo "πŸš€ Deploying $APP_NAME..." + +# Step 1: Pull latest +if [ ! -d "$APP_NAME" ]; then + git clone $REPO_URL +else + echo "πŸ”„ Repo already exists. Pulling latest..." + cd $APP_NAME && git pull && cd .. +fi + +cd $APP_NAME + +# Step 2: Check Docker Compose +echo "🐳 Starting Docker Compose stack..." +docker-compose up -d --build + +# Step 3: Vercel Frontend Deploy (if installed) +if command -v vercel &> /dev/null; then + echo "🌐 Deploying static frontend to Vercel..." + cd frontend || mkdir frontend && cp $FRONTEND_PATH frontend/index.html && cd frontend + vercel --prod --confirm --name "$VERCEL_PROJECT_NAME" + cd .. +else + echo "⚠️ Vercel CLI not found. Skipping frontend deploy." + echo "Install via: npm i -g vercel" +fi + +echo "βœ… Deployment complete." +```` + +> πŸ’‘ Optional: Add GitHub Actions for CI/CD. Want me to scaffold it? diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/Server/package-lock.json b/Server/package-lock.json new file mode 100644 index 0000000..490aa15 --- /dev/null +++ b/Server/package-lock.json @@ -0,0 +1,2105 @@ +{ + "name": "@github/copilot-xcode", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@github/copilot-xcode", + "version": "0.0.1", + "dependencies": { + "@github/copilot-language-server": "^1.335.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "monaco-editor": "0.52.2" + }, + "devDependencies": { + "@types/node": "^22.15.17", + "copy-webpack-plugin": "^13.0.0", + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "terser-webpack-plugin": "^5.3.14", + "ts-loader": "^9.5.2", + "typescript": "^5.8.3", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@github/copilot-language-server": { + "version": "1.335.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.335.0.tgz", + "integrity": "sha512-uX5t6kOlWau4WtpL/WQLL8qADE4iHSfbDojYRVq8kTIjg1u5w6Ty7wqddnfyPUIpTltifsBVoHjHpW5vdhf55g==", + "license": "https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features", + "dependencies": { + "vscode-languageserver-protocol": "^3.17.5" + }, + "bin": { + "copilot-language-server": "dist/language-server.js" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz", + "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.142", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz", + "integrity": "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/Server/package.json b/Server/package.json new file mode 100644 index 0000000..92ed578 --- /dev/null +++ b/Server/package.json @@ -0,0 +1,26 @@ +{ + "name": "@github/copilot-xcode", + "version": "0.0.1", + "description": "Package for downloading @github/copilot-language-server", + "private": true, + "scripts": { + "build": "webpack" + }, + "dependencies": { + "@github/copilot-language-server": "^1.335.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "monaco-editor": "0.52.2" + }, + "devDependencies": { + "@types/node": "^22.15.17", + "copy-webpack-plugin": "^13.0.0", + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "terser-webpack-plugin": "^5.3.14", + "ts-loader": "^9.5.2", + "typescript": "^5.8.3", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1" + } +} diff --git a/Server/src/diffView/css/style.css b/Server/src/diffView/css/style.css new file mode 100644 index 0000000..2e43014 --- /dev/null +++ b/Server/src/diffView/css/style.css @@ -0,0 +1,192 @@ +/* Diff Viewer Styles */ +:root { + /* Light theme variables */ + --bg-color: #ffffff; + --text-color: #333333; + --border-color: #dddddd; + --button-bg: #007acc; + --button-text: white; + --secondary-button-bg: #f0f0f0; + --secondary-button-text: #333333; + --secondary-button-border: #dddddd; + --secondary-button-hover: #e0e0e0; + --additions-foreground-color: #2EA043; + --deletions-foreground-color: #F85149; +} + +@media (prefers-color-scheme: dark) { + :root { + /* Dark theme variables */ + --bg-color: #1e1e1e; + --text-color: #cccccc; + --border-color: #444444; + --button-bg: #0e639c; + --button-text: white; + --secondary-button-bg: #6E6D70; + --secondary-button-text: #DFDEDF; + --secondary-button-border: #555555; + --secondary-button-hover: #505050; + --additions-foreground-color: #2EA043; + --deletions-foreground-color: #F85149; + } +} + +html, body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + overflow: hidden; + background-color: var(--bg-color); + color: var(--text-color); +} + +#container { + width: calc(100% - 40px); /* 20px padding on each side */ + height: calc(100vh - 84px); /* 40px header + 4px top padding + 40px bottom padding */ + border: 1px solid var(--border-color); + margin: 0 20px 40px 20px; + padding: 0; + margin-top: 44px; /* 40px header + 4px top padding */ + box-sizing: border-box; +} + +.loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + color: var(--text-color); +} + +.header { + position: absolute; + top: 4px; + left: 10px; + right: 10px; + height: 40px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10px; + background-color: var(--bg-color); + box-sizing: border-box; +} + +.action-button { + margin-left: 2px; + padding: 4px 14px; + background-color: var(--button-bg); + color: var(--button-text); + border: none; + border-radius: 4px; + cursor: pointer; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 14px; + font-weight: 500; +} + +.action-button:hover { + background-color: #0062a3; +} + +.action-button.secondary { + background-color: var(--secondary-button-bg); + color: var(--secondary-button-text); + border: 1px solid var(--secondary-button-border); +} + +.action-button.secondary:hover { + background-color: var(--secondary-button-hover); +} + +.hidden { + display: none; +} + +.header-left { + display: flex; + align-items: center; + overflow: hidden; + gap: 4px; +} + +/* file path */ +.file-path { + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 14px; + font-weight: 600; + color: var(--text-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Diff stats */ +.diff-stats { + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 12px; + font-weight: 500; + display: flex; + gap: 4px; +} + +.additions-count { + color: var(--additions-foreground-color); + font-weight: 600; +} + +.deletions-count { + color: var(--deletions-foreground-color); + font-weight: 600; +} + +/* Style for gutter indicators using data attributes */ +.monaco-editor .codicon.codicon-diff-insert:before { + content: "+" !important; + font-family: inherit !important; + font-size: inherit !important; + font-weight: bold; + color: var(--additions-foreground-color) !important; + padding: 0 2px; +} + +.monaco-editor .codicon.codicon-diff-remove:before { + content: "-" !important; + font-family: inherit !important; + font-size: inherit !important; + font-weight: bold; + color: var(--deletions-foreground-color) !important; + padding: 0 2px; +} + +/* Force show for Monaco Editor 0.52.2 */ +.monaco-editor .diff-side-insert .margin-view-zone .codicon, +.monaco-editor .diff-side-delete .margin-view-zone .codicon { + display: inline-block !important; + visibility: visible !important; + opacity: 1 !important; +} + +/* Hide the diff overview bar completely */ +.monaco-diff-editor .diffOverview { + display: none !important; +} + +/* Hide all lightbulb icons (Copy Changed Line buttons) */ +.monaco-editor .codicon-lightbulb, +.monaco-editor .codicon-lightbulb-autofix, +.monaco-editor .lightbulb-glyph { + display: none !important; + visibility: hidden !important; + pointer-events: none !important; +} + +/* Unfold icon */ +.monaco-editor .codicon.codicon-unfold:before { + content:"Β·Β·Β·" !important; + font-family: inherit !important; + font-size: inherit !important; + font-weight: bold; +} \ No newline at end of file diff --git a/Server/src/diffView/diffView.html b/Server/src/diffView/diffView.html new file mode 100644 index 0000000..de32b01 --- /dev/null +++ b/Server/src/diffView/diffView.html @@ -0,0 +1,31 @@ + + + + + + Diff Viewer + + + +
    Loading diff viewer...
    + +
    +
    +
    +
    + +0 + -0 +
    +
    + +
    + + +
    +
    + +
    + + + + diff --git a/Server/src/diffView/index.ts b/Server/src/diffView/index.ts new file mode 100644 index 0000000..05eb6fd --- /dev/null +++ b/Server/src/diffView/index.ts @@ -0,0 +1,38 @@ +// index.ts - Main entry point for the Monaco Editor diff view +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { initDiffEditor } from './js/monaco-diff-editor'; +import { setupUI } from './js/ui-controller'; +import DiffViewer from './js/api'; + +// Initialize everything when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + // Hide loading indicator as Monaco is directly imported + const loadingElement = document.getElementById('loading'); + if (loadingElement) { + loadingElement.style.display = 'none'; + } + + // Set up UI elements and event handlers + setupUI(); + + // Make sure the editor follows the system theme + DiffViewer.followSystemTheme(); + + // Handle window resize events + window.addEventListener('resize', () => { + DiffViewer.handleResize(); + }); +}); + +// Define DiffViewer on the window object +declare global { + interface Window { + DiffViewer: typeof DiffViewer; + } +} + +// Expose the MonacoDiffViewer API to the global scope +window.DiffViewer = DiffViewer; + +// Export the MonacoDiffViewer for webpack +export default DiffViewer; diff --git a/Server/src/diffView/js/api.ts b/Server/src/diffView/js/api.ts new file mode 100644 index 0000000..2774e0c --- /dev/null +++ b/Server/src/diffView/js/api.ts @@ -0,0 +1,121 @@ +// api.ts - Public API for external use +import { initDiffEditor, updateDiffContent, getEditor, setEditorTheme, updateDiffStats } from './monaco-diff-editor'; +import { updateFileMetadata } from './ui-controller'; +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +/** + * Interface for the DiffViewer API + */ +interface DiffViewerAPI { + init: ( + originalContent: string, + modifiedContent: string, + path: string | null, + status: string | null, + options?: monaco.editor.IDiffEditorConstructionOptions + ) => void; + update: ( + originalContent: string, + modifiedContent: string, + path: string | null, + status: string | null + ) => void; + handleResize: () => void; + setTheme: (theme: 'light' | 'dark') => void; + followSystemTheme: () => void; +} + +/** + * The public API that will be exposed to the global scope + */ +const DiffViewer: DiffViewerAPI = { + /** + * Initialize the diff editor with content + * @param {string} originalContent - Content for the original side + * @param {string} modifiedContent - Content for the modified side + * @param {string} path - File path + * @param {string} status - File edit status + * @param {Object} options - Optional configuration for the diff editor + */ + init: function( + originalContent: string, + modifiedContent: string, + path: string | null, + status: string | null, + options?: monaco.editor.IDiffEditorConstructionOptions + ): void { + // Initialize editor + initDiffEditor(originalContent, modifiedContent, options || {}); + + // Update file metadata and UI + updateFileMetadata(path, status); + }, + + /** + * Update the diff editor with new content + * @param {string} originalContent - Content for the original side + * @param {string} modifiedContent - Content for the modified side + * @param {string} path - File path + * @param {string} status - File edit status + */ + update: function( + originalContent: string, + modifiedContent: string, + path: string | null, + status: string | null + ): void { + // Update editor content + updateDiffContent(originalContent, modifiedContent); + + // Update file metadata and UI + updateFileMetadata(path, status); + + // Update diff stats + updateDiffStats(); + }, + + /** + * Handle resize events + */ + handleResize: function(): void { + const editor = getEditor(); + if (editor) { + const container = document.getElementById('container'); + if (container) { + const headerHeight = 40; + const topPadding = 4; + const bottomPadding = 40; + + const availableHeight = window.innerHeight - headerHeight - topPadding - bottomPadding; + container.style.height = `${availableHeight}px`; + } + + editor.layout(); + } + }, + + /** + * Set the theme for the editor + */ + setTheme: function(theme: 'light' | 'dark'): void { + setEditorTheme(theme); + }, + + /** + * Follow the system theme + */ + followSystemTheme: function(): void { + // Set initial theme based on system preference + const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + setEditorTheme(isDarkMode ? 'dark' : 'light'); + + // Add listener for theme changes + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { + setEditorTheme(event.matches ? 'dark' : 'light'); + }); + } + } +}; + +export default DiffViewer; diff --git a/Server/src/diffView/js/monaco-diff-editor.ts b/Server/src/diffView/js/monaco-diff-editor.ts new file mode 100644 index 0000000..0a87ac4 --- /dev/null +++ b/Server/src/diffView/js/monaco-diff-editor.ts @@ -0,0 +1,346 @@ +// monaco-diff-editor.ts - Monaco Editor diff view core functionality +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +// Editor state +let diffEditor: monaco.editor.IStandaloneDiffEditor | null = null; +let originalModel: monaco.editor.ITextModel | null = null; +let modifiedModel: monaco.editor.ITextModel | null = null; +let resizeObserver: ResizeObserver | null = null; +const DEFAULT_EDITOR_OPTIONS: monaco.editor.IDiffEditorConstructionOptions = { + renderSideBySide: false, + readOnly: true, + // Enable automatic layout adjustments + automaticLayout: true, + glyphMargin: false, + // Collapse unchanged regions + folding: true, + hideUnchangedRegions: { + enabled: true, + revealLineCount: 20, + minimumLineCount: 2, + contextLineCount: 2 + + }, + // Disable overview ruler and related features + renderOverviewRuler: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + scrollBeyondLastLine: false, + scrollbar: { + vertical: 'auto', + horizontal: 'auto', + useShadows: false, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false, + }, + lineHeight: 24, +} + +/** + * Initialize the Monaco diff editor + * @param {string} originalContent - Content for the original side + * @param {string} modifiedContent - Content for the modified side + * @param {Object} options - Optional configuration for the diff editor + * @returns {Object} The diff editor instance + */ +function initDiffEditor( + originalContent: string, + modifiedContent: string, + options: monaco.editor.IDiffEditorConstructionOptions = {} +): monaco.editor.IStandaloneDiffEditor | null { + try { + // Default options + const editorOptions: monaco.editor.IDiffEditorConstructionOptions = { + ...DEFAULT_EDITOR_OPTIONS, + lineNumbersMinChars: calculateLineNumbersMinChars(originalContent, modifiedContent), + ...options + }; + + // Create the diff editor if it doesn't exist yet + if (!diffEditor) { + const container = document.getElementById("container"); + if (!container) { + throw new Error("Container element not found"); + } + + // Set initial container size to viewport height + // const headerHeight = 40; + // container.style.height = `${window.innerHeight - headerHeight}px`; + // Set initial container size to viewport height with precise calculations + const visibleHeight = window.innerHeight; + const headerHeight = 40; + const topPadding = 4; + const bottomPadding = 40; + const availableHeight = visibleHeight - headerHeight - topPadding - bottomPadding; + container.style.height = `${Math.floor(availableHeight)}px`; + container.style.overflow = "hidden"; // Ensure container doesn't have scrollbars + + diffEditor = monaco.editor.createDiffEditor( + container, + editorOptions + ); + + // Add resize handling + setupResizeHandling(); + + // Initialize theme + initializeTheme(); + } else { + // Apply any new options + diffEditor.updateOptions(editorOptions); + } + + // Create and set models + updateModels(originalContent, modifiedContent); + + return diffEditor; + } catch (error) { + console.error("Error initializing diff editor:", error); + return null; + } +} + +/** + * Setup proper resize handling for the editor + */ +function setupResizeHandling(): void { + window.addEventListener('resize', () => { + if (diffEditor) { + diffEditor.layout(); + } + }); + + if (window.ResizeObserver && !resizeObserver) { + const container = document.getElementById('container'); + + if (container) { + resizeObserver = new ResizeObserver(() => { + if (diffEditor) { + diffEditor.layout() + } + }); + resizeObserver.observe(container); + } + } +} + +/** + * Create or update the models for the diff editor + * @param {string} originalContent - Content for the original side + * @param {string} modifiedContent - Content for the modified side + */ +function updateModels(originalContent: string, modifiedContent: string): void { + try { + // Clean up existing models if they exist + if (originalModel) { + originalModel.dispose(); + } + if (modifiedModel) { + modifiedModel.dispose(); + } + + // Create new models with the content + originalModel = monaco.editor.createModel(originalContent || "", "plaintext"); + modifiedModel = monaco.editor.createModel(modifiedContent || "", "plaintext"); + + // Set the models to show the diff + if (diffEditor) { + diffEditor.setModel({ + original: originalModel, + modified: modifiedModel, + }); + + // Add timeout to give Monaco time to calculate diffs + setTimeout(() => { + updateDiffStats(); + adjustContainerHeight(); + }, 100); // 100ms delay allows diff calculation to complete + } + } catch (error) { + console.error("Error updating models:", error); + } +} + +/** + * Update the diff view with new content + * @param {string} originalContent - Content for the original side + * @param {string} modifiedContent - Content for the modified side + */ +function updateDiffContent(originalContent: string, modifiedContent: string): void { + // If editor exists, update it + if (diffEditor && diffEditor.getModel()) { + const model = diffEditor.getModel(); + + // Update model values + if (model) { + model.original.setValue(originalContent || ""); + model.modified.setValue(modifiedContent || ""); + } + } else { + // Initialize if not already done + initDiffEditor(originalContent, modifiedContent); + } +} + +/** + * Get the current diff editor instance + * @returns {Object|null} The diff editor instance or null + */ +function getEditor(): monaco.editor.IStandaloneDiffEditor | null { + return diffEditor; +} + +/** + * Calculate the number of line differences + * @returns {Object} The number of additions and deletions + */ +function calculateLineDifferences(): { additions: number, deletions: number } { + if (!diffEditor || !diffEditor.getModel()) { + return { additions: 0, deletions: 0 }; + } + + let additions = 0; + let deletions = 0; + const lineChanges = diffEditor.getLineChanges(); + console.log(">>> Line Changes:", lineChanges); + if (lineChanges) { + for (const change of lineChanges) { + console.log(change); + if (change.originalEndLineNumber >= change.originalStartLineNumber) { + deletions += change.originalEndLineNumber - change.originalStartLineNumber + 1; + } + if (change.modifiedEndLineNumber >= change.modifiedStartLineNumber) { + additions += change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1; + } + } + } + + return { additions, deletions }; +} + +/** + * Update the diff statistics displayed in the UI + */ +function updateDiffStats(): void { + const { additions, deletions } = calculateLineDifferences(); + + const additionsElement = document.getElementById('additions-count'); + const deletionsElement = document.getElementById('deletions-count'); + + if (additionsElement) { + additionsElement.textContent = `+${additions}`; + } + + if (deletionsElement) { + deletionsElement.textContent = `-${deletions}`; + } +} + +/** + * Dynamically adjust container height based on content + */ +function adjustContainerHeight(): void { + const container = document.getElementById('container'); + if (!container || !diffEditor) return; + + // Always use the full viewport height + const visibleHeight = window.innerHeight; + const headerHeight = 40; // Height of the header + const topPadding = 4; // Top padding + const bottomPadding = 40; // Bottom padding + const availableHeight = visibleHeight - headerHeight - topPadding - bottomPadding; + + container.style.height = `${Math.floor(availableHeight)}px`; + + diffEditor.layout(); +} + +/** + * Set the editor theme + * @param {string} theme - The theme to set ('light' or 'dark') + */ +function setEditorTheme(theme: 'light' | 'dark'): void { + if (!diffEditor) return; + + monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs'); +} + +/** + * Detect the system theme preference + * @returns {string} The detected theme ('light' or 'dark') + */ +function detectSystemTheme(): 'light' | 'dark' { + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +/** + * Initialize the theme based on system preference + * and set up a listener for changes + */ +function initializeTheme(): void { + const theme = detectSystemTheme(); + setEditorTheme(theme); + + // Listen for changes in system theme preference + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { + setEditorTheme(event.matches ? 'dark' : 'light'); + }); + } +} + +/** + * Calculate the optimal number of characters for line numbers + * @param {string} originalContent - Content for the original side + * @param {string} modifiedContent - Content for the modified side + * @returns {number} The minimum number of characters needed for line numbers + */ +function calculateLineNumbersMinChars(originalContent: string, modifiedContent: string): number { + // Count the number of lines in both contents + const originalLineCount = originalContent ? originalContent.split('\n').length : 0; + const modifiedLineCount = modifiedContent ? modifiedContent.split('\n').length : 0; + + // Get the maximum line count + const maxLineCount = Math.max(originalLineCount, modifiedLineCount); + + // Calculate the number of digits in the max line count + // Use Math.log10 and Math.ceil to get the number of digits + // Add 1 to ensure some extra padding + const digits = maxLineCount > 0 ? Math.floor(Math.log10(maxLineCount) + 1) + 1 : 2; + + // Return a minimum of 2 characters, maximum of 5 + return Math.min(Math.max(digits, 2), 5); +} + +/** + * Dispose of the editor and models to clean up resources + */ +function dispose(): void { + if (resizeObserver) { + resizeObserver.disconnect(); + resizeObserver = null; + } + + if (originalModel) { + originalModel.dispose(); + originalModel = null; + } + if (modifiedModel) { + modifiedModel.dispose(); + modifiedModel = null; + } + if (diffEditor) { + diffEditor.dispose(); + diffEditor = null; + } +} + +export { + initDiffEditor, + updateDiffContent, + getEditor, + dispose, + setEditorTheme, + updateDiffStats +}; diff --git a/Server/src/diffView/js/ui-controller.ts b/Server/src/diffView/js/ui-controller.ts new file mode 100644 index 0000000..6e8579e --- /dev/null +++ b/Server/src/diffView/js/ui-controller.ts @@ -0,0 +1,162 @@ +// ui-controller.ts - UI event handlers and state management +import { DiffViewMessageHandler } from '../../shared/webkit'; +/** + * UI state and file metadata + */ +let filePath: string | null = null; +let fileEditStatus: string | null = null; + +/** + * Interface for messages sent to Swift handlers + */ +interface SwiftMessage { + event: string; + data: { + filePath: string | null; + [key: string]: any; + }; +} + +/** + * Initialize and set up UI elements and their event handlers + * @param {string} initialPath - The initial file path + * @param {string} initialStatus - The initial file edit status + */ +function setupUI(initialPath: string | null = null, initialStatus: string | null = null): void { + filePath = initialPath; + fileEditStatus = initialStatus; + + if (filePath) { + showFilePath(filePath); + } + + const keepButton = document.getElementById('keep-button'); + const undoButton = document.getElementById('undo-button'); + const choiceButtons = document.getElementById('choice-buttons'); + + if (!keepButton || !undoButton || !choiceButtons) { + console.error("Could not find UI elements"); + return; + } + + // Set initial UI state + updateUIStatus(initialStatus); + + // Setup event listeners + keepButton.addEventListener('click', handleKeepButtonClick); + undoButton.addEventListener('click', handleUndoButtonClick); +} + +/** + * Update the UI based on file edit status + * @param {string} status - The current file edit status + */ +function updateUIStatus(status: string | null): void { + fileEditStatus = status; + const choiceButtons = document.getElementById('choice-buttons'); + + if (!choiceButtons) return; + + // Hide buttons if file has been modified + if (status && status !== "none") { + choiceButtons.classList.add('hidden'); + } else { + choiceButtons.classList.remove('hidden'); + } +} + +/** + * Update the file metadata + * @param {string} path - The file path + * @param {string} status - The file edit status + */ +function updateFileMetadata(path: string | null, status: string | null): void { + filePath = path; + updateUIStatus(status); + if (filePath) { + showFilePath(filePath) + } +} + +/** + * Handle the "Keep" button click + */ +function handleKeepButtonClick(): void { + // Send message to Swift handler + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.swiftHandler) { + const message: SwiftMessage = { + event: 'keepButtonClicked', + data: { + filePath: filePath + } + }; + window.webkit.messageHandlers.swiftHandler.postMessage(message); + } else { + console.log('Keep button clicked, but no message handler found'); + } + + // Hide the choice buttons + const choiceButtons = document.getElementById('choice-buttons'); + if (choiceButtons) { + choiceButtons.classList.add('hidden'); + } +} + +/** + * Handle the "Undo" button click + */ +function handleUndoButtonClick(): void { + // Send message to Swift handler + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.swiftHandler) { + const message: SwiftMessage = { + event: 'undoButtonClicked', + data: { + filePath: filePath + } + }; + window.webkit.messageHandlers.swiftHandler.postMessage(message); + } else { + console.log('Undo button clicked, but no message handler found'); + } + + // Hide the choice buttons + const choiceButtons = document.getElementById('choice-buttons'); + if (choiceButtons) { + choiceButtons.classList.add('hidden'); + } +} + +/** + * Get the current file path + * @returns {string} The current file path + */ +function getFilePath(): string | null { + return filePath; +} + +/** + * Show the current file path + */ +function showFilePath(path: string): void { + const filePathElement = document.getElementById('file-path'); + const fileName = path.split('/').pop() ?? ''; + if (filePathElement) { + filePathElement.textContent = fileName + } +} + +/** + * Get the current file edit status + * @returns {string} The current file edit status + */ +function getFileEditStatus(): string | null { + return fileEditStatus; +} + +export { + setupUI, + updateUIStatus, + updateFileMetadata, + getFilePath, + getFileEditStatus +}; \ No newline at end of file diff --git a/Server/src/shared/webkit.ts b/Server/src/shared/webkit.ts new file mode 100644 index 0000000..3b6948f --- /dev/null +++ b/Server/src/shared/webkit.ts @@ -0,0 +1,49 @@ +/** + * Type definitions for WebKit message handlers used in WebView communication + */ + +/** + * Base WebKit message handler interface + */ +export interface WebkitMessageHandler { + postMessage(message: any): void; +} + +/** + * Terminal-specific message handler + */ +export interface TerminalMessageHandler extends WebkitMessageHandler { + postMessage(message: string): void; +} + +/** + * DiffView-specific message handler + */ +export interface DiffViewMessageHandler extends WebkitMessageHandler { + postMessage(message: object): void; +} + +/** + * WebKit message handlers container interface + */ +export interface WebkitMessageHandlers { + terminalInput: TerminalMessageHandler; + swiftHandler: DiffViewMessageHandler; + [key: string]: WebkitMessageHandler | undefined; +} + +/** + * Main WebKit interface exposed by WebViews + */ +export interface WebkitHandler { + messageHandlers: WebkitMessageHandlers; +} + +/** + * Add webkit to the global Window interface + */ +declare global { + interface Window { + webkit: WebkitHandler; + } +} \ No newline at end of file diff --git a/Server/src/terminal/index.ts b/Server/src/terminal/index.ts new file mode 100644 index 0000000..e97ee33 --- /dev/null +++ b/Server/src/terminal/index.ts @@ -0,0 +1,52 @@ +import '@xterm/xterm/css/xterm.css'; +import { Terminal } from '@xterm/xterm'; +import { TerminalAddon } from './terminalAddon'; + +declare global { + interface Window { + initializeTerminal: () => Terminal; + writeToTerminal: (text: string) => void; + clearTerminal: () => void; + } +} + +window.initializeTerminal = function (): Terminal { + const term = new Terminal({ + cursorBlink: true, + theme: { + background: '#1e1e1e', + foreground: '#cccccc', + cursor: '#ffffff', + selectionBackground: 'rgba(128, 128, 128, 0.4)' + }, + fontFamily: 'Menlo, Monaco, "Courier New", monospace', + fontSize: 13 + }); + + const terminalAddon = new TerminalAddon(); + term.loadAddon(terminalAddon); + + const terminalElement = document.getElementById('terminal'); + if (!terminalElement) { + throw new Error('Terminal element not found'); + } + term.open(terminalElement); + terminalAddon.fit(); + + // Handle window resize + window.addEventListener('resize', () => { + terminalAddon.fit(); + }); + + // Expose terminal API methods + window.writeToTerminal = function (text: string): void { + term.write(text); + terminalAddon.processTerminalOutput(text); + }; + + window.clearTerminal = function (): void { + term.clear(); + }; + + return term; +} diff --git a/Server/src/terminal/terminal.html b/Server/src/terminal/terminal.html new file mode 100644 index 0000000..a35ac6f --- /dev/null +++ b/Server/src/terminal/terminal.html @@ -0,0 +1,27 @@ + + + + + + + + +
    + + + + diff --git a/Server/src/terminal/terminalAddon.ts b/Server/src/terminal/terminalAddon.ts new file mode 100644 index 0000000..bf78dfe --- /dev/null +++ b/Server/src/terminal/terminalAddon.ts @@ -0,0 +1,326 @@ +import { FitAddon } from '@xterm/addon-fit'; +import { Terminal, ITerminalAddon } from '@xterm/xterm'; +import { TerminalMessageHandler } from '../shared/webkit'; + +interface TermSize { + cols: number; + rows: number; +} + +interface TerminalPosition { + row: number; + col: number; +} + +// https://xtermjs.org/docs/api/vtfeatures/ +// https://en.wikipedia.org/wiki/ANSI_escape_code +const VT = { + ESC: '\x1b', + CSI: '\x1b[', + UP_ARROW: '\x1b[A', + DOWN_ARROW: '\x1b[B', + RIGHT_ARROW: '\x1b[C', + LEFT_ARROW: '\x1b[D', + HOME_KEY: ['\x1b[H', '\x1bOH'], + END_KEY: ['\x1b[F', '\x1bOF'], + DELETE_REST_OF_LINE: '\x1b[K', + CursorUp: (n = 1) => `\x1b[${n}A`, + CursorDown: (n = 1) => `\x1b[${n}B`, + CursorForward: (n = 1) => `\x1b[${n}C`, + CursorBack: (n = 1) => `\x1b[${n}D` +}; + +/** + * Key code constants + */ +const KeyCodes = { + CONTROL_C: 3, + CONTROL_D: 4, + ENTER: 13, + BACKSPACE: 8, + DELETE: 127 +}; + +export class TerminalAddon implements ITerminalAddon { + private term: Terminal | null; + private fitAddon: FitAddon; + private inputBuffer: string; + private cursor: number; + private promptInLastLine: string; + private termSize: TermSize; + + constructor() { + this.term = null; + this.fitAddon = new FitAddon(); + this.inputBuffer = ''; + this.cursor = 0; + this.promptInLastLine = ''; + this.termSize = { + cols: 0, + rows: 0, + }; + } + + dispose(): void { + this.fitAddon.dispose(); + } + + activate(terminal: Terminal): void { + this.term = terminal; + this.termSize = { + cols: terminal.cols, + rows: terminal.rows, + }; + this.fitAddon.activate(terminal); + this.term.onData(this.handleData.bind(this)); + this.term.onResize(this.handleResize.bind(this)); + } + + fit(): void { + this.fitAddon.fit(); + } + + private handleData(data: string): void { + // If the input is a longer string (e.g., from paste), and it contains newlines + if (data.length > 1 && !data.startsWith(VT.ESC)) { + const lines = data.split(/(\r\n|\n|\r)/g); + + let lineIndex = 0; + const processLine = () => { + if (lineIndex >= lines.length) return; + + const line = lines[lineIndex]; + if (line === '\n' || line === '\r' || line === '\r\n') { + if (this.cursor > 0) { + this.clearInputLine(); + this.cursor = 0; + this.renderInputLine(this.inputBuffer); + } + window.webkit.messageHandlers.terminalInput.postMessage(this.inputBuffer + '\n'); + this.inputBuffer = ''; + this.cursor = 0; + lineIndex++; + setTimeout(processLine, 100); + return; + } + + this.handleSingleLine(line); + lineIndex++; + processLine(); + }; + + processLine(); + return; + } + + // Handle escape sequences for special keys + if (data.startsWith(VT.ESC)) { + this.handleEscSequences(data); + return; + } + + this.handleSingleLine(data); + } + + private handleSingleLine(data: string): void { + if (data.length === 0) return; + + const char = data.charCodeAt(0); + // Handle control characters + if (char < 32 || char === 127) { + // Handle Enter key (carriage return) + if (char === KeyCodes.ENTER) { + if (this.cursor > 0) { + this.clearInputLine(); + this.cursor = 0; + this.renderInputLine(this.inputBuffer); + } + window.webkit.messageHandlers.terminalInput.postMessage(this.inputBuffer + '\n'); + this.inputBuffer = ''; + this.cursor = 0; + } + else if (char === KeyCodes.CONTROL_C || char === KeyCodes.CONTROL_D) { + if (this.cursor > 0) { + this.clearInputLine(); + this.cursor = 0; + this.renderInputLine(this.inputBuffer); + } + window.webkit.messageHandlers.terminalInput.postMessage(this.inputBuffer + data); + this.inputBuffer = ''; + this.cursor = 0; + } + // Handle backspace or delete + else if (char === KeyCodes.BACKSPACE || char === KeyCodes.DELETE) { + if (this.cursor > 0) { + this.clearInputLine(); + + // Delete character at cursor position - 1 + const beforeCursor = this.inputBuffer.substring(0, this.cursor - 1); + const afterCursor = this.inputBuffer.substring(this.cursor); + const newInput = beforeCursor + afterCursor; + this.cursor--; + this.renderInputLine(newInput); + } + } + return; + } + + this.clearInputLine(); + + // Insert character at cursor position + const beforeCursor = this.inputBuffer.substring(0, this.cursor); + const afterCursor = this.inputBuffer.substring(this.cursor); + const newInput = beforeCursor + data + afterCursor; + this.cursor += data.length; + this.renderInputLine(newInput); + } + + private handleResize(data: { cols: number; rows: number }): void { + this.clearInputLine(); + this.termSize = { + cols: data.cols, + rows: data.rows, + }; + this.renderInputLine(this.inputBuffer); + } + + private clearInputLine(): void { + if (!this.term) return; + // Move to beginning of the current line + this.term.write('\r'); + const cursorPosition = this.calcCursorPosition(); + const inputEndPosition = this.calcLineWrapPosition(this.promptInLastLine.length + this.inputBuffer.length); + // If cursor is not at the end of input, move to the end + if (cursorPosition.row < inputEndPosition.row) { + this.term.write(VT.CursorDown(inputEndPosition.row - cursorPosition.row)); + } else if (cursorPosition.row > inputEndPosition.row) { + this.term.write(VT.CursorUp(cursorPosition.row - inputEndPosition.row)); + } + + // Clear from the last line upwards + this.term.write('\r' + VT.DELETE_REST_OF_LINE); + for (let i = inputEndPosition.row - 1; i >= 0; i--) { + this.term.write(VT.CursorUp(1)); + this.term.write('\r' + VT.DELETE_REST_OF_LINE); + } + }; + + // Function to render the input line considering line wrapping + private renderInputLine(newInput: string): void { + if (!this.term) return; + this.inputBuffer = newInput; + // Write prompt and input + this.term.write(this.promptInLastLine + this.inputBuffer); + const cursorPosition = this.calcCursorPosition(); + const inputEndPosition = this.calcLineWrapPosition(this.promptInLastLine.length + this.inputBuffer.length); + // If the last input char is at the end of the terminal width, + // need to print an extra empty line to display the cursor. + if (inputEndPosition.col == 0) { + this.term.write(' '); + this.term.write(VT.CursorBack(1)); + this.term.write(VT.DELETE_REST_OF_LINE); + } + + if (this.inputBuffer.length === this.cursor) { + return; + } + + // Move the cursor from the input end to the expected cursor row + if (cursorPosition.row < inputEndPosition.row) { + this.term.write(VT.CursorUp(inputEndPosition.row - cursorPosition.row)); + } + this.term.write('\r'); + if (cursorPosition.col > 0) { + this.term.write(VT.CursorForward(cursorPosition.col)); + } + }; + + private calcCursorPosition(): TerminalPosition { + return this.calcLineWrapPosition(this.promptInLastLine.length + this.cursor); + } + + private calcLineWrapPosition(textLength: number): TerminalPosition { + if (!this.term) { + return { row: 0, col: 0 }; + } + const row = Math.floor(textLength / this.termSize.cols); + const col = textLength % this.termSize.cols; + + return { row, col }; + } + + /** + * Handle ESC sequences + */ + private handleEscSequences(data: string): void { + if (!this.term) return; + switch (data) { + case VT.UP_ARROW: + // TODO: Could implement command history here + break; + + case VT.DOWN_ARROW: + // TODO: Could implement command history here + break; + + case VT.RIGHT_ARROW: + if (this.cursor < this.inputBuffer.length) { + this.clearInputLine(); + this.cursor++; + this.renderInputLine(this.inputBuffer); + } + break; + + case VT.LEFT_ARROW: + if (this.cursor > 0) { + this.clearInputLine(); + this.cursor--; + this.renderInputLine(this.inputBuffer); + } + break; + } + + // Handle Home key variations + if (VT.HOME_KEY.includes(data)) { + this.clearInputLine(); + this.cursor = 0; + this.renderInputLine(this.inputBuffer); + } + + // Handle End key variations + if (VT.END_KEY.includes(data)) { + this.clearInputLine(); + this.cursor = this.inputBuffer.length; + this.renderInputLine(this.inputBuffer); + } + }; + + /** + * Remove OSC escape sequences from text + */ + private removeOscSequences(text: string): string { + // Remove basic OSC sequences + let filteredText = text.replace(/\u001b\]\d+;[^\u0007\u001b]*[\u0007\u001b\\]/g, ''); + + // More comprehensive approach for nested sequences + return filteredText.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, ''); + }; + + /** + * Process terminal output and update prompt tracking + */ + processTerminalOutput(text: string): void { + if (typeof text !== 'string') return; + + const lastNewline = text.lastIndexOf('\n'); + const lastCarriageReturn = text.lastIndexOf('\r'); + const lastControlChar = Math.max(lastNewline, lastCarriageReturn); + let newPromptText = lastControlChar !== -1 ? text.substring(lastControlChar + 1) : text; + + // Filter out OSC sequences + newPromptText = this.removeOscSequences(newPromptText); + + this.promptInLastLine = lastControlChar !== -1 ? + newPromptText : this.promptInLastLine + newPromptText; + }; +} diff --git a/Server/tsconfig.json b/Server/tsconfig.json new file mode 100644 index 0000000..71eb52f --- /dev/null +++ b/Server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "sourceMap": true, + "allowJs": true, + "checkJs": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/Server/webpack.config.js b/Server/webpack.config.js new file mode 100644 index 0000000..2ace244 --- /dev/null +++ b/Server/webpack.config.js @@ -0,0 +1,77 @@ +const path = require('path'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const webpack = require('webpack'); +const TerserPlugin = require('terser-webpack-plugin'); + +/* + * The folder structure of `dist` would be: + * dist/ + * β”œβ”€β”€ terminal/ + * β”‚ β”œβ”€β”€ terminal.js + * β”‚ └── terminal.html + * └── diffView/ + * β”œβ”€β”€ diffView.js + * β”œβ”€β”€ diffView.html + * └── css/ + * └── style.css +*/ +module.exports = { + mode: 'production', + entry: { + // Add more entry points here + terminal: './src/terminal/index.ts', + diffView: './src/diffView/index.ts' + }, + resolve: { + extensions: ['.ts', '.js'] + }, + output: { + filename: '[name]/[name].js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'] + } + ] + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + /// MARK: - Terminal component files + { + from: 'src/terminal/terminal.html', + to: 'terminal/terminal.html' + }, + + /// MARK: - DiffView component files + { + from: 'src/diffView/diffView.html', + to: 'diffView/diffView.html' + }, + { + from: 'src/diffView/css', + to: 'diffView/css' + } + ] + }), + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1 + }) + ], + optimization: { + minimizer: [ + new TerserPlugin({ + // Prevent extracting license comments to a separate file + extractComments: false + }) + ] + } +}; diff --git a/ServerNotificationHandler.swift b/ServerNotificationHandler.swift new file mode 100644 index 0000000..1381747 --- /dev/null +++ b/ServerNotificationHandler.swift @@ -0,0 +1,49 @@ +import Combine +import Foundation +import JSONRPC +import LanguageServerProtocol + +protocol ServerNotificationHandler { + var protocolProgressSubject: PassthroughSubject { get } + func handleNotification(_ notification: AnyJSONRPCNotification) +} + +class ServerNotificationHandlerImpl: ServerNotificationHandler { + public static let shared = ServerNotificationHandlerImpl() + var protocolProgressSubject: PassthroughSubject + var conversationProgressHandler: ConversationProgressHandler = ConversationProgressHandlerImpl.shared + var featureFlagNotifier: FeatureFlagNotifier = FeatureFlagNotifierImpl.shared + + init() { + self.protocolProgressSubject = PassthroughSubject() + } + + func handleNotification(_ notification: AnyJSONRPCNotification) { + let methodName = notification.method + + if let method = ServerNotification.Method(rawValue: methodName) { + switch method { + case .windowLogMessage: + break + case .protocolProgress: + if let data = try? JSONEncoder().encode(notification.params), + let progress = try? JSONDecoder().decode(ProgressParams.self, from: data) { + conversationProgressHandler.handleConversationProgress(progress) + } + default: + break + } + } else { + switch methodName { + case "featureFlagsNotification": + if let data = try? JSONEncoder().encode(notification.params), + let featureFlags = try? JSONDecoder().decode(FeatureFlags.self, from: data) { + featureFlagNotifier.handleFeatureFlagNotification(featureFlags) + } + break + default: + break + } + } + } +} diff --git a/actions/Deploy.sh b/actions/Deploy.sh new file mode 100644 index 0000000..09913a0 --- /dev/null +++ b/actions/Deploy.sh @@ -0,0 +1,115 @@ +#!/bin/bash +set -e + +APP_NAME="enclov-AI" +REPO_URL="https://github.com/Web4application/enclov-AI.git" +FRONTEND_PATH="./index.html" +VERCEL_PROJECT_NAME="enclov-ai-frontend" +ENV_FILE=".env" + +# Load environment variables (including secrets) +load_env() { + if [ -f "$ENV_FILE" ]; then + echo "πŸ“¦ Loading environment variables from $ENV_FILE" + set -o allexport + source "$ENV_FILE" + set +o allexport + fi +} + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +send_slack_notification() { + if [ -n "$SLACK_WEBHOOK_URL" ]; then + local message="$1" + curl -s -X POST -H 'Content-type: application/json' \ + --data "{\"channel\": \"$SLACK_CHANNEL\", \"text\": \"$message\"}" \ + "$SLACK_WEBHOOK_URL" > /dev/null 2>&1 + fi +} + +send_email_notification() { + if [ -n "$SENDGRID_API_KEY" ] && [ -n "$EMAIL_TO" ]; then + local subject="$1" + local content="$2" + + curl -s --request POST \ + --url https://api.sendgrid.com/v3/mail/send \ + --header "Authorization: Bearer $SENDGRID_API_KEY" \ + --header 'Content-Type: application/json' \ + --data "{ + \"personalizations\": [{ \"to\": [{ \"email\": \"$EMAIL_TO\" }] }], + \"from\": { \"email\": \"$EMAIL_FROM\" }, + \"subject\": \"$subject\", + \"content\": [{ \"type\": \"text/plain\", \"value\": \"$content\" }] + }" > /dev/null + fi +} + +redeploy_on_change() { + log "πŸ› οΈ Watching for file changes in $APP_NAME..." + inotifywait -r -e modify,create,delete ./frontend ./app | + while read path _ file; do + log "πŸ” Change detected: $file. Redeploying..." + ./deploy.sh + done +} + +# Main deploy function +deploy() { + log "πŸš€ Deploying $APP_NAME..." + + # Clone or pull latest + if [ ! -d "$APP_NAME" ]; then + git clone "$REPO_URL" + else + log "πŸ”„ Repo exists. Pulling latest..." + pushd "$APP_NAME" + git pull + LAST_COMMIT=$(git log -1 --pretty=format:"%h - %s by %an") + popd + fi + + # Slack notify with commit info + send_slack_notification ":rocket: *Deploying $APP_NAME started...*\nLast commit: \`$LAST_COMMIT\`" + + cd "$APP_NAME" + + # Docker Compose deploy + log "🐳 Starting Docker Compose stack..." + if [ -f "../$ENV_FILE" ]; then + docker-compose --env-file "../$ENV_FILE" up -d --build + else + docker-compose up -d --build + fi + + # Vercel deploy frontend + if command -v vercel &> /dev/null; then + log "🌐 Deploying static frontend to Vercel..." + mkdir -p frontend + cp "$FRONTEND_PATH" frontend/index.html + pushd frontend + vercel --prod --confirm --name "$VERCEL_PROJECT_NAME" + popd + log "🌍 Visit: https://$VERCEL_PROJECT_NAME.vercel.app" + else + log "⚠️ Vercel CLI not found. Skipping frontend deploy." + log "Install via: npm i -g vercel" + fi + + send_slack_notification ":white_check_mark: *$APP_NAME deployed successfully!*" + send_email_notification "$APP_NAME Deployment" "$APP_NAME was deployed successfully at $(date).\nCommit: $LAST_COMMIT" + log "βœ… Deployment complete." +} + +# Load env variables at the start +load_env + +if [[ "$1" == "--watch" ]]; then + deploy + redeploy_on_change +else + deploy +fi diff --git a/actions/ai-code-reviewer.yml b/actions/ai-code-reviewer.yml new file mode 100644 index 0000000..974aa0a --- /dev/null +++ b/actions/ai-code-reviewer.yml @@ -0,0 +1,26 @@ +name: "AI Code Reviewer" +description: "Reviews PRs with OpenAI and assigns labels + reviewers" + +inputs: + OPENAI_API_KEY: + description: "OpenAI API Key" + required: true + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run AI Review + run: python main.py + env: + GITHUB_TOKEN=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 + OPENAI_Api_KEY=gG1uZhj50x1lYFKrrB5kT3BlbkFJXP3R63ExWT9lkcHI0pRq diff --git a/actions/ai_code_reviewer.yml b/actions/ai_code_reviewer.yml new file mode 100644 index 0000000..62624a8 --- /dev/null +++ b/actions/ai_code_reviewer.yml @@ -0,0 +1,26 @@ +name: "AI Code Reviewer" +description: "Reviews PRs with OpenAI and assigns labels + reviewers" + +inputs: + OPENAI_API_KEY: + description: "OpenAI API Key" + required: true + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run AI Review + run: python main.py + env: + GITHUB_TOKEN: ${{ gG1uZhj50x1lYFKrrB5kT3BlbkFJXP3R63ExWT9lkcHI0pRq }} + OPENAI_API_KEY: ${{ gG1uZhj50x1lYFKrrB5kT3BlbkFJXP3R63ExWT9lkcHI0pRq }} diff --git a/actions/ci-cd.yml b/actions/ci-cd.yml new file mode 100644 index 0000000..b543237 --- /dev/null +++ b/actions/ci-cd.yml @@ -0,0 +1,80 @@ +name: CI/CD - Build, Test, Lint, and Deploy + +on: + push: + branches: + - main + pull_request: + +env: + DOCKER_IMAGE_NAME: ${{ enclov-ai }}/fastapi-ai-pr-reviewer + +jobs: + lint: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - name: Install python and lint + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - run: pip install flake8 + - run: flake8 . + + test: + runs-on: ubuntu-latest + needs: lint + permissions: + contents: read + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - run: pip install -r requirements.txt + - name: Run Tests + run: | + # Add real test commands here + echo "No tests yet β€” add pytest or unittest!" + + build_and_push: + runs-on: ubuntu-latest + needs: test + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + + - name: Log in to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.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 }} + password: ${{ secrets.84298ed9f2470f159b48e36e76eb90eeac061941 }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + push: true + tags: ${{ env.web4application.github.io/enclov-ai:latest }}enclov-ai:latest + + deploy: + runs-on: ubuntu-latest + needs: build_and_push + permissions: + contents: read + steps: + - name: Deploy to production server + uses: appleboy/ssh-action@v0.1.7 + with: + host: ${{ secrets.PROD_SERVER_HOST }} + username: ${{ secrets.PROD_SERVER_USER }} + key: ${{ secrets.PROD_SERVER_SSH_KEY }} + script: | + docker pull ${{ env.DOCKER_IMAGE_NAME }}:latest + docker stop fastapi-ai-pr-reviewer || true + docker rm fastapi-ai-pr-reviewer || true + docker run -d --restart always -p 8000:8000 --name fastapi-ai-pr-reviewer ${{ env.DOCKER_IMAGE_NAME }}:latest diff --git a/actions/deploy-docs.yml b/actions/deploy-docs.yml new file mode 100644 index 0000000..d3b2c7f --- /dev/null +++ b/actions/deploy-docs.yml @@ -0,0 +1,35 @@ +name: Build and Deploy Enclov CLI Docs + +on: + push: + branches: + - main + +permissions: + contents: write + pages: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: pip install --upgrade pip + + - name: Generate docs + run: python generate_docs.py + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ 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 }} + publish_dir: ./docs diff --git a/actions/deploy.yml b/actions/deploy.yml new file mode 100644 index 0000000..3993cf8 --- /dev/null +++ b/actions/deploy.yml @@ -0,0 +1,44 @@ +name: Deploy enclov-AI to Kubernetes + +on: + push: + branches: [main] + +permissions: + contents: read + packages: write + deployments: write + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.enclov-ai }} + password: ${{ secrets.84298ed9f2470f159b48e36e76eb90eeac061941 }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + push: true + tags: web4application.github.io/enclov-ai:latest + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Decode KUBECONFIG and set up + run: | + echo "${{ secrets.KUBECONFIG }}" | base64 --decode > $HOME/.kube/config + + - name: Deploy to Kubernetes + run: kubectl apply -f k8s/ diff --git a/actions/generate-man.yml b/actions/generate-man.yml new file mode 100644 index 0000000..df2e973 --- /dev/null +++ b/actions/generate-man.yml @@ -0,0 +1,32 @@ +name: Generate Man Page + +on: + push: + branches: [main] + +jobs: + gen-man: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install enclov CLI (if needed) + run: | + # Adapt this based on how enclov is built or installed + npm install -g ./ # if it's a local Node.js CLI tool + + - name: Generate man page + run: python .github/scripts/generate_man.py + + - name: Upload man page artifact + uses: actions/upload-artifact@v3 + with: + name: enclov-man-page + path: man/enclov.1 diff --git a/actions/generate_man_pages.py b/actions/generate_man_pages.py new file mode 100644 index 0000000..2563b3e --- /dev/null +++ b/actions/generate_man_pages.py @@ -0,0 +1,37 @@ +name: Generate and Publish Man Page + +on: + push: + branches: [main] + +permissions: read + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install groff and python-qrcode + run: | + sudo apt-get update + sudo apt-get install -y groff + pip install qrcode[pil] + + - name: Generate man pages HTML and TOC + run: python scripts/generate_man.py + + - name: Commit and push docs + run: | + git config --global user.name "enclov-actions" + git config --global user.email "actions@github.com" + git add docs/ + git commit -m "Update man page HTMLs and TOC [skip ci]" || echo "No changes to commit" + git push origin main diff --git a/actions/generates_man_pages.py b/actions/generates_man_pages.py new file mode 100644 index 0000000..862102b --- /dev/null +++ b/actions/generates_man_pages.py @@ -0,0 +1,128 @@ +import os + +# Define all commands with their descriptions and options +commands = { + "enclov": { + "name": "enclov", + "section": "1", + "version": "v0.1.0", + "date": "May 2025", + "description": "privacy-first AI assistant CLI interface", + "synopsis": "enclov [options] [commands]", + "options": [ + ("--version", "Show version information."), + ("--help", "Show this help message."), + ], + "commands": [ + ("start", "Start the Enclov AI container."), + ("stop", "Stop the Enclov AI container."), + ("status", "Show the current status of the container and services."), + ("config [path]", "View or set configuration file path."), + ("logs", "View real-time logs from the Enclov container."), + ("update", "Update Enclov AI to the latest version."), + ], + }, + "start": { + "name": "enclov-start", + "section": "1", + "version": "v0.1.0", + "date": "May 2025", + "description": "start Enclov AI container", + "synopsis": "enclov start [options]", + "options": [ + ("--help", "Show help for the start command."), + ("--detached, -d", "Run container in detached/background mode."), + ("--config ", "Specify path to configuration file."), + ], + }, + "stop": { + "name": "enclov-stop", + "section": "1", + "version": "v0.1.0", + "date": "May 2025", + "description": "stop Enclov AI container", + "synopsis": "enclov stop [options]", + "options": [ + ("--help", "Show help for the stop command."), + ("--force, -f", "Force stop immediately."), + ], + }, + "status": { + "name": "enclov-status", + "section": "1", + "version": "v0.1.0", + "date": "May 2025", + "description": "show Enclov AI container and services status", + "synopsis": "enclov status", + "options": [ + ("--help", "Show help for the status command."), + ], + }, + "config": { + "name": "enclov-config", + "section": "1", + "version": "v0.1.0", + "date": "May 2025", + "description": "manage Enclov configuration file", + "synopsis": "enclov config [path]", + "options": [ + ("--show", "Display the current config file path."), + ("--set ", "Set a new config file path."), + ("--help", "Show help for the config command."), + ], + }, + "logs": { + "name": "enclov-logs", + "section": "1", + "version": "v0.1.0", + "date": "May 2025", + "description": "view Enclov AI container logs", + "synopsis": "enclov logs [options]", + "options": [ + ("--follow, -f", "Follow logs in real time."), + ("--tail ", "Show only the last n lines of logs."), + ("--help", "Show help for the logs command."), + ], + }, + "update": { + "name": "enclov-update", + "section": "1", + "version": "v0.1.0", + "date": "May 2025", + "description": "update Enclov AI platform", + "synopsis": "enclov update [options]", + "options": [ + ("--check", "Check for updates without applying."), + ("--force", "Force update, ignoring warnings."), + ("--help", "Show help for the update command."), + ], + }, +} + +def generate_man_page(cmd): + """Generate man page text for given command dictionary.""" + lines = [] + lines.append(f'.TH {cmd["name"].upper()} {cmd["section"]} "{cmd["date"]}" "{cmd["version"]}" "Enclov AI CLI Manual"') + lines.append(f'.SH NAME\n{cmd["name"]} \\- {cmd["description"]}\n') + lines.append(f'.SH SYNOPSIS\n.B {cmd["synopsis"]}\n') + lines.append(f'.SH DESCRIPTION\n{cmd["description"]}\n') + if "options" in cmd and cmd["options"]: + lines.append('.SH OPTIONS') + for opt, desc in cmd["options"]: + lines.append(f'.TP\n.BR {opt}\n{desc}') + if "commands" in cmd and cmd["commands"]: + lines.append('.SH COMMANDS') + for c, desc in cmd["commands"]: + lines.append(f'.TP\n.B {c}\n{desc}') + return '\n'.join(lines) + +def save_man_pages(output_dir='man'): + os.makedirs(output_dir, exist_ok=True) + for key, cmd in commands.items(): + filename = os.path.join(output_dir, f'{cmd["name"]}.1') + with open(filename, 'w') as f: + f.write(generate_man_page(cmd)) + print(f'Generated man page: {filename}') + +if __name__ == "__main__": + save_man_pages() diff --git a/ai-code-reviewer/.github/Workflows/ai-code-reviewer.yml b/ai-code-reviewer/.github/Workflows/ai-code-reviewer.yml new file mode 100644 index 0000000..1ddd6e4 --- /dev/null +++ b/ai-code-reviewer/.github/Workflows/ai-code-reviewer.yml @@ -0,0 +1,26 @@ +name: "AI Code Reviewer" +description: "Reviews PRs with OpenAI and assigns labels + reviewers" + +inputs: + OPENAI_API_KEY: + description: "OpenAI API Key" + required: true + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run AI Review + run: python main.py + env: + GITHUB_TOKEN: ${{ github.token }} + OPENAI_API_KEY: ${{ inputs.OPENAI_API_KEY }} diff --git a/ai-code-reviewer/Dockerfile b/ai-code-reviewer/Dockerfile new file mode 100644 index 0000000..323fe21 --- /dev/null +++ b/ai-code-reviewer/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . /app + +RUN pip install --no-cache-dir -r requirements.txt + +ENTRYPOINT ["python", "main.py"] diff --git a/bin/W b/ai-code-reviewer/api/__init__.py similarity index 100% rename from bin/W rename to ai-code-reviewer/api/__init__.py diff --git a/ai-code-reviewer/api/views.py b/ai-code-reviewer/api/views.py new file mode 100644 index 0000000..ed87e72 --- /dev/null +++ b/ai-code-reviewer/api/views.py @@ -0,0 +1,43 @@ +import os +from fastapi import APIRouter, HTTPException, Query +from github import Github +from utils import github_inline, triage +import openai + +router = APIRouter() + +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") +REPO_NAME = os.getenv("GITHUB_REPOSITORY") + +if not OPENAI_API_KEY or not GITHUB_TOKEN or not REPO_NAME: + raise RuntimeError("Missing required environment variables.") + +openai.api_key = OPENAI_API_KEY +gh = Github(GITHUB_TOKEN) +repo = gh.get_repo(REPO_NAME) + + +@router.get("/review") +async def review_pull_request(pr_number: int = Query(..., description="Pull Request number")): + try: + pr = repo.get_pull(pr_number) + github_inline.add_inline_review_comments(pr, gh, OPENAI_API_KEY) + + files_changed = [f.filename for f in pr.get_files()] + ai_response = triage.analyze_pr_with_openai(pr.title, pr.body or "", files_changed) + summary, labels = triage.extract_labels_from_ai(ai_response) + + if summary: + pr.create_issue_comment(f"🧠 **AI Summary:**\n\n{summary}") + if labels: + pr.add_to_labels(*labels) + + reviewers = triage.match_reviewers_by_paths(files_changed) + if reviewers: + pr.create_review_request(reviewers=reviewers) + + return {"status": "success", "summary": summary, "labels": labels, "reviewers": reviewers} + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ai-code-reviewer/main.py b/ai-code-reviewer/main.py new file mode 100644 index 0000000..5ef496c --- /dev/null +++ b/ai-code-reviewer/main.py @@ -0,0 +1,38 @@ +import os +import json +from github import Github +from utils import github_inline, triage +import openai + +def main(): + github_token = os.getenv("GITHUB_TOKEN") + openai_key = os.getenv("OPENAI_API_KEY") + repo_name = os.getenv("GITHUB_REPOSITORY") + pr_number = int(os.getenv("GITHUB_REF").split("/")[-2]) + + gh = Github(github_token) + repo = gh.get_repo(repo_name) + pr = repo.get_pull(pr_number) + + openai.api_key = openai_key + + # Inline AI Review + github_inline.add_inline_review_comments(pr, gh, openai_key) + + # AI PR Triage (Summary + Labels + Reviewers) + files_changed = [f.filename for f in pr.get_files()] + ai_response = triage.analyze_pr_with_openai(pr.title, pr.body or "", files_changed) + summary, labels = triage.extract_labels_from_ai(ai_response) + + if summary: + pr.create_issue_comment(f"🧠 **AI Summary:**\n\n{summary}") + + if labels: + pr.add_to_labels(*labels) + + reviewers = triage.match_reviewers_by_paths(files_changed) + if reviewers: + pr.create_review_request(reviewers=reviewers) + +if __name__ == "__main__": + main() diff --git a/ai-code-reviewer/requirements.txt b/ai-code-reviewer/requirements.txt new file mode 100644 index 0000000..23572d6 --- /dev/null +++ b/ai-code-reviewer/requirements.txt @@ -0,0 +1,7 @@ +openai +requests +PyGithub +fastapi +uvicorn +openai +PyGithub diff --git a/ai-code-reviewer/utils/Helpers.py b/ai-code-reviewer/utils/Helpers.py new file mode 100644 index 0000000..20790f2 --- /dev/null +++ b/ai-code-reviewer/utils/Helpers.py @@ -0,0 +1,131 @@ +import os +import openai +import requests +import difflib +from github import Github, PullRequest + +# === Core Setup === + +def init_openai(api_key: str): + openai.api_key = api_key + + +def get_github_client(token: str) -> Github: + return Github(token) + + +def get_pr_number() -> int: + ref = os.getenv("GITHUB_REF") + if not ref or "pull" not in ref: + raise RuntimeError("GITHUB_REF does not indicate a pull request") + return int(ref.split("/")[-1]) + + +def get_repo_full_name() -> str: + repo = os.getenv("GITHUB_REPOSITORY") + if not repo: + raise RuntimeError("GITHUB_REPOSITORY is not set") + return repo + + +def get_pull_request(gh: Github) -> PullRequest.PullRequest: + return gh.get_repo(get_repo_full_name()).get_pull(get_pr_number()) + + +# === PR Diff Utilities === + +def fetch_pr_diff(pr: PullRequest.PullRequest) -> str: + diff_url = pr.diff_url + headers = {"Authorization": f"token {os.getenv('GITHUB_TOKEN')}"} + response = requests.get(diff_url, headers=headers) + response.raise_for_status() + return response.text + + +def parse_diff_by_file(diff_text: str) -> dict: + """Parse unified diff into {filename: diff_content}""" + files = {} + current_file = None + current_diff = [] + + for line in diff_text.splitlines(): + if line.startswith("diff --git"): + if current_file and current_diff: + files[current_file] = "\n".join(current_diff) + current_file = line.split(" b/")[-1] + current_diff = [line] + elif current_file: + current_diff.append(line) + + if current_file and current_diff: + files[current_file] = "\n".join(current_diff) + + return files + + +def filter_files_by_extension(files: dict, exts=(".py", ".js", ".ts", ".go", ".java")) -> dict: + return {fname: diff for fname, diff in files.items() if fname.endswith(exts)} + + +def split_into_chunks(text: str, max_chars: int = 3000) -> list: + """Split large diffs into smaller chunks for OpenAI API""" + lines = text.splitlines() + chunks = [] + chunk = [] + + current_len = 0 + for line in lines: + line_len = len(line) + 1 + if current_len + line_len > max_chars: + chunks.append("\n".join(chunk)) + chunk = [] + current_len = 0 + chunk.append(line) + current_len += line_len + + if chunk: + chunks.append("\n".join(chunk)) + + return chunks + + +# === OpenAI Review === + +def review_diff_with_openai(diff: str, model: str = "gpt-4o-mini") -> str: + prompt = f""" +You are a senior code reviewer. Review this GitHub PR diff. Give useful, concise feedback on code quality, bugs, design, and best practices. + +Diff: +{diff} + +Review: +""".strip() + + response = openai.ChatCompletion.create( + model=model, + messages=[{"role": "user", "content": prompt}], + max_tokens=500, + temperature=0.3, + ) + return response.choices[0].message.content.strip() + + +# === PR Commenting === + +def post_review_comment(pr: PullRequest.PullRequest, review: str): + pr.create_issue_comment(f"🧠 **AI Code Review**\n\n{review}") + + +def post_chunked_reviews(pr: PullRequest.PullRequest, diff_text: str): + files = parse_diff_by_file(diff_text) + files = filter_files_by_extension(files) + + for fname, file_diff in files.items(): + chunks = split_into_chunks(file_diff) + for i, chunk in enumerate(chunks): + try: + review = review_diff_with_openai(chunk) + heading = f"🧠 Review for `{fname}` (chunk {i+1})" + pr.create_issue_comment(f"{heading}:\n\n{review}") + except Exception as e: + pr.create_issue_comment(f"⚠️ Error reviewing `{fname}` chunk {i+1}: {e}") diff --git a/ai-code-reviewer/utils/github_inline.py b/ai-code-reviewer/utils/github_inline.py new file mode 100644 index 0000000..988f229 --- /dev/null +++ b/ai-code-reviewer/utils/github_inline.py @@ -0,0 +1,80 @@ +import os +import openai +import requests +from github import Github, PullRequest + +def get_github_client(token: str) -> Github: + return Github(token) + +def get_pr(pr_number: int, repo_name: str, gh: Github) -> PullRequest.PullRequest: + return gh.get_repo(repo_name).get_pull(pr_number) + +def fetch_pr_files(pr: PullRequest.PullRequest): + return pr.get_files() + +def fetch_patch_from_file(file) -> str: + return file.patch # GitHub provides diff patch per file + +def split_patch_into_hunks(patch: str) -> list: + """Naively split a patch into hunks by @@ marker""" + return patch.split("\n@@")[1:] if patch else [] + +def review_patch_hunk(hunk: str, filename: str) -> str: + prompt = f""" +You are a senior software engineer. Review the following code patch (diff hunk) in file `{filename}`. +Identify any bugs, bad practices, or improvements. Be specific and constructive. + +Patch: +{hunk} + +Review: +""" + response = openai.ChatCompletion.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + temperature=0.2, + max_tokens=400, + ) + return response.choices[0].message.content.strip() + +def add_inline_review_comments(pr: PullRequest.PullRequest, gh: Github, openai_key: str): + openai.api_key = openai_key + repo = pr.base.repo + files = fetch_pr_files(pr) + review_comments = [] + + for file in files: + if not file.filename.endswith(('.py', '.js', '.ts', '.go', '.java')): + continue + + patch = fetch_patch_from_file(file) + hunks = split_patch_into_hunks(patch) + + for hunk in hunks: + try: + review = review_patch_hunk(hunk, file.filename) + if not review.strip(): + continue + + # GitHub API needs exact position, fallback to 1st line of hunk + position = 1 + comment = { + "path": file.filename, + "body": f"🧠 **AI Review Suggestion:**\n\n{review}", + "position": position + } + review_comments.append(comment) + + except Exception as e: + pr.create_issue_comment(f"⚠️ Failed to review a hunk in `{file.filename}`: {e}") + + if review_comments: + repo._requester.requestJson( + "POST", + f"{repo.url}/pulls/{pr.number}/reviews", + input={ + "body": "πŸ€– AI Code Review: Inline suggestions for improvement", + "event": "COMMENT", + "comments": review_comments + } + ) diff --git a/ai-code-reviewer/utils/triage.py b/ai-code-reviewer/utils/triage.py new file mode 100644 index 0000000..f61c8f4 --- /dev/null +++ b/ai-code-reviewer/utils/triage.py @@ -0,0 +1,63 @@ +import openai + +LABEL_MAP = { + "bug": ["fix", "broken", "issue", "error", "fails", "unexpected"], + "refactor": ["refactor", "cleanup", "structure", "reorganize"], + "docs": ["readme", "documentation", "comment", "docs"], + "feature": ["add", "introduce", "implement", "new"], + "security": ["vulnerability", "exploit", "xss", "sql injection", "leak"] +} + +REVIEWER_MAP = { + "frontend": ["src/components", "web/", "ui/"], + "backend": ["api/", "server/", "routes/", "models/"], + "security": ["auth/", "encryption/", "security/"], + "docs": ["docs/", "README.md"], +} + + +def analyze_pr_with_openai(title: str, body: str, files: list, model="gpt-4o-mini"): + prompt = f""" +You are an AI code triage assistant. Given the following GitHub PR title, description, and modified filenames, +summarize the purpose of this PR and suggest appropriate labels from: {', '.join(LABEL_MAP.keys())}. + +PR Title: +{title} + +PR Description: +{body} + +Changed Files: +{files} + +Your Response (JSON): +{{"summary": "...", "labels": ["label1", "label2", ...]}} +""" + + response = openai.ChatCompletion.create( + model=model, + messages=[{"role": "user", "content": prompt}], + temperature=0.3, + max_tokens=300, + ) + + content = response.choices[0].message.content.strip() + return content + + +def extract_labels_from_ai(json_str: str) -> tuple: + import json + try: + parsed = json.loads(json_str) + return parsed.get("summary", ""), parsed.get("labels", []) + except Exception: + return "", [] + + +def match_reviewers_by_paths(file_paths: list) -> list: + reviewers = set() + for path in file_paths: + for category, dirs in REVIEWER_MAP.items(): + if any(path.startswith(d) for d in dirs): + reviewers.add(category) + return list(reviewers) diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/api-client.js b/api/api-client.js new file mode 100644 index 0000000..82c62db --- /dev/null +++ b/api/api-client.js @@ -0,0 +1,28 @@ +const core = require('@actions/core') + +// Load variables from Actions runtime +function getRequiredVars() { + return { + workflowRun: process.env.GITHUB_RUN_ID, + repositoryNwo: process.env.GITHUB_REPOSITORY, + buildVersion: process.env.GITHUB_SHA, + buildActor: process.env.GITHUB_ACTOR, + actionsId: process.env.GITHUB_ACTION, + githubToken: core.getInput('token'), + githubApiUrl: process.env.GITHUB_API_URL ?? 'https://api.github.com', + githubServerUrl: process.env.GITHUB_SERVER_URL ?? 'https://github.com', + artifactName: core.getInput('artifact_name') || 'github-pages', + isPreview: core.getInput('preview') === 'true' + } +} + +module.exports = function getContext() { + const requiredVars = getRequiredVars() + for (const variable in requiredVars) { + if (requiredVars[variable] === undefined) { + throw new Error(`${variable} is undefined. Cannot continue.`) + } + } + core.debug('all variables are set') + return requiredVars +} diff --git a/api/llm/Model_loader.py b/api/llm/Model_loader.py new file mode 100644 index 0000000..dbdc3ba --- /dev/null +++ b/api/llm/Model_loader.py @@ -0,0 +1,23 @@ +import os, yaml + +def load_model(): + with open("model_config.yaml") as f: + config = yaml.safe_load(f) + provider = config.get("provider", "openai") + model_name = config.get("model", "gpt-4") + + if provider == "openai": + import openai + openai.api_key = os.environ.get("OPENAI_API_KEY") + class OpenAIWrapper: + def chat(self, message): + response = openai.ChatCompletion.create( + model=model_name, + messages=[{"role": "user", "content": message}], + temperature=config.get("temperature", 0.7), + max_tokens=config.get("max_tokens", 2048) + ) + return response.choices[0].message.content + return OpenAIWrapper() + + raise ValueError(f"Unsupported provider: {provider}") diff --git a/api/llm/__init__.py b/api/llm/__init__.py new file mode 100644 index 0000000..d3f5a12 --- /dev/null +++ b/api/llm/__init__.py @@ -0,0 +1 @@ + diff --git a/api/llm/chat_engine.py b/api/llm/chat_engine.py new file mode 100644 index 0000000..566def6 --- /dev/null +++ b/api/llm/chat_engine.py @@ -0,0 +1,6 @@ +from api.llm.model_loader import load_model + +def run_chat(message: str): + model = load_model() + response = model.chat(message) # Mocked call + return {"response": response} diff --git a/api/llm/model_loader.py b/api/llm/model_loader.py new file mode 100644 index 0000000..5798b83 --- /dev/null +++ b/api/llm/model_loader.py @@ -0,0 +1,5 @@ +def load_model(): + class DummyModel: + def chat(self, message): + return f"Echo: {message}" + return DummyModel() diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..cf4f695 --- /dev/null +++ b/api/main.py @@ -0,0 +1,30 @@ +from fastapi import FastAPI +from api.routes.chat import router as chat_router +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from fastapi.responses import JSONResponse, FileResponse +from pydantic import BaseModel +import os +from app.ai_review import analyze_pr + +app = FastAPI() + +app.mount("/static", StaticFiles(directory="static"), name="static") + +@app.get("/") +async def serve_index(): + return FileResponse("static/index.html") + +class PRRequest(BaseModel): + repo_url: str + pr_number: int + +@app.post("/api/submit") +async def submit_review(pr: PRRequest): + try: + result = analyze_pr(pr.repo_url, pr.pr_number) + return JSONResponse(content={"success": True, "comments": result}) + except Exception as e: + return JSONResponse(content={"success": False, "error": str(e)}) +app = FastAPI() +app.include_router(chat_router) diff --git a/api/review b/api/review new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/review @@ -0,0 +1 @@ + diff --git a/api/routes/Jf b/api/routes/Jf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/routes/Jf @@ -0,0 +1 @@ + diff --git a/api/routes/__init__.py b/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/routes/chat.py b/api/routes/chat.py new file mode 100644 index 0000000..85e54b9 --- /dev/null +++ b/api/routes/chat.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter, Request +from api.llm.chat_engine import run_chat + +router = APIRouter() + +@router.post("/chat") +async def chat_endpoint(request: Request): + body = await request.json() + user_input = body.get("message", "") + return run_chat(user_input) diff --git a/api/routes/comments.py b/api/routes/comments.py new file mode 100644 index 0000000..ca1ab6d --- /dev/null +++ b/api/routes/comments.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, Request, Header, HTTPException +import json +from core.security import verify_signature +from core.github_auth import get_installation_access_token +from core.ai_review import generate_ai_review_comment +import httpx + +router = APIRouter() + +@router.post("/webhook") +async def github_webhook( + request: Request, + x_hub_signature_256: str = Header(None), + x_github_event: str = Header(None) +): + body_bytes = await request.body() + + if not verify_signature(body_bytes, x_hub_signature_256): + raise HTTPException(status_code=401, detail="Invalid signature") + + payload = json.loads(body_bytes) + + if x_github_event == "pull_request": + action = payload.get("action") + if action in ["opened", "synchronize", "reopened"]: + pr = payload["pull_request"] + pr_number = pr["number"] + repo = payload["repository"]["full_name"] + installation_id = payload["installation"]["id"] + code_diff_url = pr["diff_url"] + + token = await get_installation_access_token(installation_id) + + async with httpx.AsyncClient() as client: + diff_resp = await client.get(code_diff_url) + diff_resp.raise_for_status() + diff_text = diff_resp.text + + ai_review_comment = generate_ai_review_comment(diff_text) + + review_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews" + headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github+json" + } + data = { + "body": ai_review_comment, + "event": "COMMENT" + } + async with httpx.AsyncClient() as client: + r = await client.post(review_url, headers=headers, json=data) + r.raise_for_status() + + return {"msg": "AI review comment posted"} + + return {"msg": "Event ignored"} diff --git a/api/submit b/api/submit new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/submit @@ -0,0 +1 @@ + diff --git a/api/submit_pr.py b/api/submit_pr.py new file mode 100644 index 0000000..6027f75 --- /dev/null +++ b/api/submit_pr.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, Request +from pydantic import BaseModel, HttpUrl +from typing import Optional +from starlette.responses import JSONResponse +import uuid +import logging +from github import Github +from urllib.parse import urlparse + +# Initialize FastAPI router +router = APIRouter() +logger = logging.getLogger(__name__) + +# Replace with your GitHub Personal Access Token +GITHUB_TOKEN = "your_github_token_here" + +class PRSubmission(BaseModel): + repo_url: HttpUrl + pr_number: int + +@router.post("/api/submit") +async def submit_pr(data: PRSubmission, request: Request): + try: + job_id = str(uuid.uuid4()) + logger.info(f"Received PR submission: {data.repo_url} PR #{data.pr_number} [job_id={job_id}]") + + # Parse the repository URL to extract owner and repo name + parsed_url = urlparse(str(data.repo_url)) + path_parts = parsed_url.path.strip("/").split("/") + if len(path_parts) < 2: + raise ValueError("Invalid repository URL format.") + owner, repo_name = path_parts[0], path_parts[1] + + # Authenticate with GitHub + g = Github(GITHUB_TOKEN) + repo = g.get_repo(f"{owner}/{repo_name}") + pr = repo.get_pull(data.pr_number) + + # Example: Fetch pull request details + pr_title = pr.title + pr_body = pr.body + pr_files = pr.get_files() + pr_commits = pr.get_commits() + + # TODO: Implement AI review logic here + # For demonstration, we'll create a simple review comment + review_body = f"Automated review for PR #{data.pr_number}: Looks good overall." + + # Create a review on the pull request + pr.create_review(body=review_body, event="COMMENT") + + return JSONResponse(content={"success": True, "job_id": job_id, "message": "Review submitted."}) + + except Exception as e: + logger.error(f"Failed to submit PR: {e}") + return JSONResponse(content={"success": False, "error": str(e)}, status_code=500) diff --git a/api/tools/Py b/api/tools/Py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/tools/Py @@ -0,0 +1 @@ + diff --git a/api/tools/__init__.py b/api/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app.py b/app.py new file mode 100644 index 0000000..f568919 --- /dev/null +++ b/app.py @@ -0,0 +1,61 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from api.routes import comments +from models.schema import CommandRequest +from enclov_commands import allowed_funcs +from app.routes import submit_pr # adjust path as needed +from fastapi import FastAPI +from submit_pr import router as submit_pr_router +from providers.enclovai_provider import call_enclovai + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # or specific origin + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +def process_prompt(prompt, model="auto"): + return call_enclovai(prompt, model=model) + +if __name__ == "__main__": + prompt = input("Enter your prompt: ") + provider = input("Choose provider (openai/google/auto): ").strip().lower() + response = process_prompt(prompt, model=provider) + print("\n=== Response ===\n") + print(response) + +app = FastAPI( + title="Enclov-AI", + version="0.1.0", + description="Privacy-first AI assistant for CLI + PR automation" +) + +app = FastAPI() +app.include_router(submit_pr_router) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Caution: lock this down in prod + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(submit_pr.router) + +# CLI command endpoint +@app.post("/api/run") +async def run_command(req: CommandRequest): + cmd = req.command.strip() + func = allowed_funcs.get(cmd) + + if not func: + return {"output": f"❌ Unknown command: '{cmd}'\nType 'enclov help' for help."} + + try: + return {"output": func()} + except Exception as e: + return {"output": f"πŸ”₯ Error while executing '{cmd}': {str(e)}"} + +# GitHub Webhook endpoint +app.include_router(comments.router) diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..61c7968 --- /dev/null +++ b/app/.env @@ -0,0 +1,14 @@ +GITHUB_WEBHOOK_SECRET=84298ed9f2470f159b48e36e76eb90eeac061941 +GITHUB_APP_ID=1325944 +GITHUB_PRIVATE_KEY_PATH=/keys/private-key.pem +google_API_KEY=AIzaSyAvrxOyAVzPVcnzxuD0mjKVDyS2bNWfC10 +export_GOOGLE_API_KEY=AlzaSyCHjfdo3w160Dd5yTVJD409pWmigOJEg +GITHUB_TOKEN=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 +GITHUB_REPOSITORY=web4application/enclove-ai +PR_NUMBER=123 +GITHUB_TOKEN=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 +GITHUB_TOKEN=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 +export_GEMINI_API_KEY= +OPENAI_Api_KEY=gG1uZhj50x1lYFKrrB5kT3BlbkFJXP3R63ExWT9lkcHI0pRq +export GITHUB_TOKEN="" +GITHUB_TOKEN="" diff --git a/app/Docker-compose.yml b/app/Docker-compose.yml new file mode 100644 index 0000000..d236a1f --- /dev/null +++ b/app/Docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + web: + build: . + ports: + - "8000:8000" + depends_on: + - redis + environment: + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + + worker: + build: . + command: celery -A app.tasks worker --loglevel=info + depends_on: + - redis + environment: + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + + redis: + image: redis:6.2 + ports: + - "6379:6379" + +flower: + image: mher/flower + ports: + - "5555:5555" + environment: + - CELERY_BROKER_URL=redis://redis:6379/0 + command: flower --broker=redis://redis:6379/0 diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..63aa625 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/Index.html b/app/Index.html new file mode 100644 index 0000000..e51a268 --- /dev/null +++ b/app/Index.html @@ -0,0 +1,175 @@ + + + + +

    Enclov AI

    + + + + + + +

    Welcome to Enclov AI

    +

    Powered by Web4Application

    + +
    +
    ⏳ Initializing Enclov AI...
    + + + + Build and Deploy Enclov CLI Docs + + + + + + + diff --git a/app/ai_review.py b/app/ai_review.py new file mode 100644 index 0000000..c92befa --- /dev/null +++ b/app/ai_review.py @@ -0,0 +1,12 @@ +import random + +def analyze_pr(repo_url: str, pr_number: int): + # Real logic would clone repo, checkout PR diff, run LLM, etc. + mock_comments = [ + f"βœ… Efficient use of data structures in PR #{pr_number}.", + "🧹 Consider removing unused imports.", + "πŸ”’ Security tip: mask sensitive keys in logs.", + "πŸ“¦ Use semantic versioning in your package updates.", + "🧠 Consider adding docstrings to helper functions." + ] + return random.sample(mock_comments, k=3) diff --git a/app/app.jsx b/app/app.jsx new file mode 100644 index 0000000..9c3d9cf --- /dev/null +++ b/app/app.jsx @@ -0,0 +1,111 @@ +import React, { useState } from "react"; + +export default function App() { + const [code, setCode] = useState(""); + const [review, setReview] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function handleSubmit(e) { + e.preventDefault(); + setLoading(true); + setError(""); + setReview(""); + + try { + const res = await fetch("/analyze-code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code }), + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.detail || "Unknown error"); + } + + const data = await res.json(); + setReview(data.review_comment); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + + return ( +
    +

    enclov-AI Code Reviewer

    +
    +