diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c9c78e9d..fecaf6ea 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,27 @@ version: 2 updates: + # ─── Python (pip) ───────────────────────────── - package-ecosystem: "pip" directory: "/" - schedule: - interval: "daily" - time: "06:00" - timezone: "UTC" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "pip" + schedule: { interval: "weekly" } + labels: [ "dependencies", "pip" ] + groups: # Group patches & minors from dev-only tools + dev-py: + dependency-type: "development" + update-types: ["minor", "patch"] + + # ─── Node (npm) ─────────────────────────────── + - package-ecosystem: "npm" + directory: "/" + schedule: { interval: "weekly" } + labels: [ "dependencies", "npm" ] + cooldown: # wait before opening PRs + semver-major-days: 30 + semver-minor-days: 7 + semver-patch-days: 3 + + # ─── GitHub Actions ─────────────────────────── + - package-ecosystem: "github-actions" + directory: "/" + schedule: { interval: "weekly" } + labels: [ "dependencies", "gh-actions" ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 587b776d..ff03ec68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,26 +25,50 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Locate pip cache + id: pip-cache + shell: bash + run: echo "dir=$(python -m pip cache dir)" >> "$GITHUB_OUTPUT" - name: Cache pip uses: actions/cache@v4 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }} + restore-keys: ${{ runner.os }}-pip- - name: Install dependencies run: | - pip install --upgrade pip - pip install -r requirements-dev.txt + python -m pip install --upgrade pip + python -m pip install ".[dev]" - name: Run tests - run: | - pytest + run: pytest # Run pre-commit only on Python 3.13 + ubuntu. - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 if: ${{ matrix.python-version == '3.13' && matrix.os == 'ubuntu-latest' }} - run: | - pre-commit run --all-files + + frontend: + needs: test # Builds Tailwind CSS only if tests pass + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install Node deps + run: npm ci + + - name: Build CSS + run: npm run build:css # Creates src/static/css/site.css + + - name: Upload artefact + uses: actions/upload-artifact@v4 + with: + name: static-css + path: src/static/css/site.css diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b9403985..e5c96102 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,39 +1,88 @@ -name: "Publish to PyPI" +name: Publish to PyPI on: release: - types: [created] - workflow_dispatch: + types: [created] # Run when you click “Publish release” + workflow_dispatch: # ... or run it manually from the Actions tab permissions: contents: read +# ── Build the Tailwind CSS bundle ─────────────────────────────── jobs: + frontend-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: package-lock.json + + - name: Install deps + build Tailwind + run: | + npm ci + npm run build:css + + - name: Upload built CSS + uses: actions/upload-artifact@v4 + with: + name: frontend-assets + path: src/static/css/site.css + if-no-files-found: error + +# ── Build wheel/sdist (needs CSS) and upload “dist/” ──────────── release-build: + needs: frontend-build runs-on: ubuntu-latest + steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + + # Grab site.css produced above + - uses: actions/download-artifact@v4 + with: + name: frontend-assets + path: src/static/css/ + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 with: python-version: "3.13" - - name: Build package + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install build backend run: | - pip install build + python -m pip install --upgrade pip + python -m pip install build twine python -m build - - uses: actions/upload-artifact@v4 + twine check dist/* + - name: Upload dist artefact + uses: actions/upload-artifact@v4 with: name: dist path: dist/ +# ── Publish to PyPI (only if “dist/” succeeded) ───────────────── pypi-publish: - needs: [release-build] + needs: release-build runs-on: ubuntu-latest - environment: pypi + environment: pypi # Creates the “pypi” environment in repo-settings + permissions: - id-token: write + id-token: write # OIDC token for trusted publishing + steps: - uses: actions/download-artifact@v4 with: name: dist path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 88888267..08a4ebc4 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,52 +1,39 @@ name: OSSF Scorecard on: - # For Branch-Protection check. Only the default branch is supported. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: - # To guarantee Maintained check is occasionally updated. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - - cron: '33 11 * * 2' + - cron: '33 11 * * 2' # Every Tuesday at 11:33 AM UTC push: - branches: [ "main" ] + branches: [ main ] -# Declare default permissions as read only. -permissions: read-all +permissions: read-all # Default for the whole workflow + +concurrency: # (optional) avoid overlapping runs + group: scorecard-${{ github.ref }} + cancel-in-progress: true jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: - # Needed to upload the results to code-scanning dashboard. - security-events: write - # Needed to publish results and get a badge (see publish_results below). - id-token: write + security-events: write # upload SARIF to code-scanning + id-token: write # publish results for the badge steps: - - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 with: persist-credentials: false - - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + - name: Run Scorecard + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 with: results_file: results.sarif results_format: sarif + publish_results: true # enables the public badge - # Public repositories: - # - Publish results to OpenSSF REST API for easy access by consumers - # - Allows the repository to include the Scorecard badge. - # - See https://github.com/ossf/scorecard-action#publishing-results. - publish_results: true - - # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF - # format to the repository Actions tab. - - # Upload the results to GitHub's code scanning dashboard (optional). - # Commenting out will disable upload of results to your repo's Code Scanning dashboard - - name: "Upload to code-scanning" + - name: Upload to code-scanning uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 0dbb49bf..1463caf5 100644 --- a/.gitignore +++ b/.gitignore @@ -170,6 +170,9 @@ cython_debug/ # JavaScript tooling node_modules/ +# CSS +src/static/css/site.css + # Project specific history.txt cleanup.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9b7a2a8..0d8144f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,97 +1,116 @@ # Contributing to Gitingest -Thanks for your interest in contributing to Gitingest! 🚀 Gitingest aims to be friendly for first time contributors, with a simple Python and HTML codebase. We would love your help to make it even better. If you need any help while working with the code, please reach out to us on [Discord](https://discord.com/invite/zerRaGK9EC). +Thanks for your interest in contributing to **Gitingest** 🚀 Our goal is to keep the codebase friendly to first-time contributors. +If you ever get stuck, reach out on [Discord](https://discord.com/invite/zerRaGK9EC). + +--- ## How to Contribute (non-technical) -- **Create an Issue**: If you find a bug or have an idea for a new feature, please [create an issue](https://github.com/cyclotruc/gitingest/issues/new) on GitHub. This will help us track and prioritize your request. -- **Spread the Word**: If you like Gitingest, please share it with your friends, colleagues, and on social media. This will help us grow the community and make Gitingest even better. -- **Use Gitingest**: The best feedback comes from real-world usage! If you encounter any issues or have ideas for improvement, please let us know by [creating an issue](https://github.com/cyclotruc/gitingest/issues/new) on GitHub or by reaching out to us on [Discord](https://discord.com/invite/zerRaGK9EC). +- **Create an Issue** – found a bug or have a feature idea? + [Open an issue](https://github.com/cyclotruc/gitingest/issues/new). +- **Spread the Word** – tweet, blog, or tell a friend. +- **Use Gitingest** – real-world usage gives the best feedback. File issues or ping us on [Discord](https://discord.com/invite/zerRaGK9EC) with anything you notice. + +--- ## How to submit a Pull Request -1. Fork the repository. +> **Prerequisites**: The project uses **Python 3.9+** and `pre-commit` for development. +> If you plan to touch the frontend, you'll also need **Node ≥18** (for Tailwind). + +1. **Fork** the repository. -2. Clone the forked repository: +2. **Clone** your fork: ```bash git clone https://github.com/cyclotruc/gitingest.git cd gitingest ``` - **Note**: To contribute, ensure you have **Python 3.9 or newer** installed, as some of the `pre-commit` hooks (e.g. `pyupgrade`) require Python 3.9+. - -3. Set up the development environment and install dependencies: +3. **Set up the dev environment**: ```bash python -m venv .venv source .venv/bin/activate - pip install -r requirements-dev.txt + pip install -e ".[dev]" pre-commit install ``` -4. Create a new branch for your changes: +4. **Create a branch** for your changes: - ```bash - git checkout -S -b your-branch - ``` + ```bash + git checkout -b your-branch + ``` -5. Make your changes. Make sure to add corresponding tests for your changes. +5. **Make your changes** (and add tests when relevant). -6. Stage your changes: +6. **Stage** the changes: - ```bash - git add . - ``` + ```bash + git add . + ``` -7. Run the tests: +7. **Run the backend test suite**: ```bash pytest ``` -8. Run the local web server - - 1. Navigate to src folder +8. *(Optional)* **Run `pre-commit` on all files** to check hooks without committing: - ``` bash - cd src - ``` + ```bash + pre-commit run --all-files + ``` - 2. Run the local web server: +9. **If you edited templates or CSS** rebuild Tailwind: - ``` bash - uvicorn server.main:app - ``` + ```bash + # one-time install + npm ci - 3. Open your browser and navigate to `http://localhost:8000` to see the app running. + # build once + npm run build:css -9. Confirm that everything is working as expected. If you encounter any issues, fix them and repeat steps 6 to 8. + # or watch & rebuild on every save + npm run dev:css + ``` -10. Commit your changes (signed): + *Skip this step if your PR only touches Python code.* - All commits to Gitingest must be [GPG-signed](https://docs.github.com/en/authentication/managing-commit-signature-verification) so that the project can verify the authorship of every contribution. You can either configure Git globally with: +10. **Run the local server** to sanity-check: ```bash - git config --global commit.gpgSign true + cd src + uvicorn server.main:app ``` - or pass the `-S` flag as shown below. + Open [http://localhost:8000](http://localhost:8000) to confirm everything works. + +11. **Commit** (signed): ```bash git commit -S -m "Your commit message" ``` - If `pre-commit` raises any issues, fix them and repeat steps 6 to 9. + If *pre-commit* complains, fix the problems and repeat **6 – 10**. -11. Push your changes: +12. **Push** your branch: ```bash git push origin your-branch ``` -12. Open a pull request on GitHub. Make sure to include a detailed description of your changes. +13. **Open a pull request** on GitHub with a clear description. + +14. **Iterate** on any review feedback—update your branch and repeat **6 – 13** as needed. + +*(Optional) Invite a maintainer to your branch for easier collaboration.* + +--- + +## CSS & build artefacts -13. Wait for the maintainers to review your pull request. If there are any issues, fix them and repeat steps 6 to 12. +- **Do not commit `src/static/css/site.css`.** The CI pipeline runs `npm run build:css` during the container/image build, so the artefact is produced automatically. - *(Optional) Invite project maintainer to your branch for easier collaboration.* +- When developing locally you may run the build yourself (see step 9) so you can preview the styles. diff --git a/Dockerfile b/Dockerfile index 63577a98..c70ade75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,44 +1,62 @@ -# Build stage -FROM python:3.12-slim AS builder +# ---------- Stage 1: Build CSS with Node ------------------------- +FROM node:20-alpine AS css-builder +WORKDIR /frontend + +# Copy only files that affect the CSS build to leverage Docker cache +COPY package*.json ./ +RUN npm ci + +# Tailwind source --> final CSS +# (adjust the paths if you store Tailwind input elsewhere) +COPY tailwind.config.js ./ # Tailwind config +COPY src/static/css/ ./src/static/css/ # Tailwind input file(s) +RUN npm run build:css # writes ./src/static/css/site.css -WORKDIR /build -# Copy requirements first to leverage Docker cache -COPY requirements.txt . +# ---------- Stage 2: Install Python dependencies ----------------- +FROM python:3.12-slim AS python-builder +WORKDIR /build -# Install build dependencies and Python packages +# System build tools first (so later layers are cached if unchanged) RUN apt-get update \ && apt-get install -y --no-install-recommends gcc python3-dev \ - && pip install --no-cache-dir --upgrade pip \ - && pip install --no-cache-dir --timeout 1000 -r requirements.txt \ && rm -rf /var/lib/apt/lists/* -# Runtime stage -FROM python:3.12-slim +# Python dependencies +COPY pyproject.toml . +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir --timeout 1000 "." -# Set Python environment variables -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 -# Install Git +# ---------- Stage 3: Final runtime image ------------------------- +FROM python:3.12-slim +LABEL org.opencontainers.image.source="https://github.com/cyclotruc/gitingest" + +# Minimal runtime utilities RUN apt-get update \ - && apt-get install -y --no-install-recommends git curl\ + && apt-get install -y --no-install-recommends git curl \ && rm -rf /var/lib/apt/lists/* +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + WORKDIR /app -# Create a non-root user +# Create non-root user (uid 1000 == common default on Linux host) RUN useradd -m -u 1000 appuser -COPY --from=builder /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/ +# ── Copy Python site-packages & app code ─────────────────────────── +COPY --from=python-builder /usr/local/lib/python3.12/site-packages/ \ + /usr/local/lib/python3.12/site-packages/ COPY src/ ./ -# Change ownership of the application files -RUN chown -R appuser:appuser /app +# ── Copy the freshly-built CSS ──────────────────────────────────── +COPY --from=css-builder /frontend/src/static/css/site.css \ + src/static/css/site.css -# Switch to non-root user +# Fix permissions +RUN chown -R appuser:appuser /app USER appuser EXPOSE 8000 - CMD ["python", "-m", "uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index 8af7ceaf..84b80e31 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Gitingest -[![Screenshot of Gitingest front page](./docs/frontpage.png)](https://gitingest.com) +[![Screenshot of Gitingest front page](https://raw.githubusercontent.com/cyclotruc/gitingest/refs/heads/main/docs/frontpage.png)](https://gitingest.com) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4dd841e0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1691 @@ +{ + "name": "gitingest", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitingest", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "simple-icons": "^15.4.0", + "tailwindcss": "^3.4.17" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@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/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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "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.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "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/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/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/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.177", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.177.tgz", + "integrity": "sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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/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/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "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/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "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/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.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/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-icons": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-15.4.0.tgz", + "integrity": "sha512-m6+a5bENq4YjrTuiDVpailM+qybhFLlXjILabS10yoGbmyjCFckWhfprn+1dAf8HGUR4DBcJGrtgh9UIdOwnVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/simple-icons" + }, + { + "type": "github", + "url": "https://github.com/sponsors/simple-icons" + } + ], + "license": "CC0-1.0", + "engines": { + "node": ">=0.12.18" + } + }, + "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/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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/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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..3ae7a176 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "name": "gitingest-frontend", + "version": "0.0.0", + "scripts": { + "build:css": "tailwindcss -i ./src/static/css/tailwind.css -o ./src/static/css/site.css --minify", + "dev:css": "tailwindcss -i ./src/static/css/tailwind.css -o ./src/static/css/site.css --watch" + }, + "devDependencies": { + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "simple-icons": "^15.4.0", + "tailwindcss": "^3.4.17" + } +} diff --git a/pyproject.toml b/pyproject.toml index 5171396d..8123cf2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,10 @@ dependencies = [ ] license = {file = "LICENSE"} -authors = [{name = "Romain Courtois", email = "romain@coderamp.io"}] +authors = [ + { name = "Romain Courtois", email = "romain@coderamp.io" }, + { name = "Filip Christiansen"}, +] classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", @@ -31,6 +34,15 @@ classifiers=[ "Programming Language :: Python :: 3.13", ] +[project.optional-dependencies] +dev = [ + "eval-type-backport", + "pre-commit", + "pytest", + "pytest-asyncio", + "pytest-mock", +] + [project.scripts] gitingest = "gitingest.cli:main" @@ -45,6 +57,7 @@ build-backend = "setuptools.build_meta" [tool.setuptools] packages = {find = {where = ["src"]}} include-package-data = true +package-data = {"gitingest" = ["static/css/*.css"]} # Linting configuration [tool.pylint.format] @@ -80,7 +93,7 @@ ignore = [ # https://docs.astral.sh/ruff/rules/... "BLE001", # blind-except, TODO: replace with specific exceptions "FAST003", # fast-api-unused-path-parameter, TODO: fix ] -per-file-ignores = { "tests/**/*.py" = ["S101"] } # Skip the “assert used” warning +per-file-ignores = { "tests/**/*.py" = ["S101"] } # Skip the "assert used" warning [tool.ruff.lint.pylint] max-returns = 10 diff --git a/src/gitingest/clone.py b/src/gitingest/clone.py index 53c2f485..a65c3046 100644 --- a/src/gitingest/clone.py +++ b/src/gitingest/clone.py @@ -26,7 +26,7 @@ async def clone_repo(config: CloneConfig, *, token: str | None = None) -> None: """Clone a repository to a local path based on the provided configuration. This function handles the process of cloning a Git repository to the local file system. - It can clone a specific branch or commit if provided, and it raises exceptions if + It can clone a specific branch, tag, or commit if provided, and it raises exceptions if any errors occur during the cloning process. Parameters @@ -47,6 +47,7 @@ async def clone_repo(config: CloneConfig, *, token: str | None = None) -> None: local_path: str = config.local_path commit: str | None = config.commit branch: str | None = config.branch + tag: str | None = config.tag partial_clone: bool = config.subpath != "/" # Create parent directory if it doesn't exist @@ -67,9 +68,14 @@ async def clone_repo(config: CloneConfig, *, token: str | None = None) -> None: if partial_clone: clone_cmd += ["--filter=blob:none", "--sparse"] + # Shallow clone unless a specific commit is requested if not commit: clone_cmd += ["--depth=1"] - if branch and branch.lower() not in ("main", "master"): + + # Prefer tag over branch when both are provided + if tag: + clone_cmd += ["--branch", tag] + elif branch and branch.lower() not in ("main", "master"): clone_cmd += ["--branch", branch] clone_cmd += [url, local_path] diff --git a/src/gitingest/entrypoint.py b/src/gitingest/entrypoint.py index aca57672..9c04d65b 100644 --- a/src/gitingest/entrypoint.py +++ b/src/gitingest/entrypoint.py @@ -3,10 +3,12 @@ from __future__ import annotations import asyncio -import inspect import shutil import sys +import warnings +from contextlib import asynccontextmanager from pathlib import Path +from typing import AsyncGenerator from gitingest.clone import clone_repo from gitingest.config import MAX_FILE_SIZE @@ -19,10 +21,11 @@ async def ingest_async( source: str, *, - max_file_size: int = MAX_FILE_SIZE, # 10 MB + max_file_size: int = MAX_FILE_SIZE, include_patterns: str | set[str] | None = None, exclude_patterns: str | set[str] | None = None, branch: str | None = None, + tag: str | None = None, include_gitignored: bool = False, token: str | None = None, output: str | None = None, @@ -45,6 +48,8 @@ async def ingest_async( Pattern or set of patterns specifying which files to exclude. If ``None``, no files are excluded. branch : str | None The branch to clone and ingest (default: the default branch). + tag : str | None + The tag to clone and ingest. If ``None``, no tag is used. include_gitignored : bool If ``True``, include files ignored by ``.gitignore`` and ``.gitingestignore`` (default: ``False``). token : str | None @@ -78,22 +83,13 @@ async def ingest_async( if not include_gitignored: _apply_gitignores(query) - if branch: - query.branch = branch - - repo_cloned = False - try: - await _clone_if_remote(query, token=token) - repo_cloned = bool(query.url) + if query.url: + _override_branch_and_tag(query, branch=branch, tag=tag) + async with _clone_repo_if_remote(query, token=token): summary, tree, content = ingest_query(query) await _write_output(tree, content=content, target=output) - return summary, tree, content - finally: - # Clean up the temporary directory for the repository - if repo_cloned: - shutil.rmtree(query.local_path.parent) def ingest( @@ -103,6 +99,7 @@ def ingest( include_patterns: str | set[str] | None = None, exclude_patterns: str | set[str] | None = None, branch: str | None = None, + tag: str | None = None, include_gitignored: bool = False, token: str | None = None, output: str | None = None, @@ -125,6 +122,8 @@ def ingest( Pattern or set of patterns specifying which files to exclude. If ``None``, no files are excluded. branch : str | None The branch to clone and ingest (default: the default branch). + tag : str | None + The tag to clone and ingest. If ``None``, no tag is used. include_gitignored : bool If ``True``, include files ignored by ``.gitignore`` and ``.gitingestignore`` (default: ``False``). token : str | None @@ -155,6 +154,7 @@ def ingest( include_patterns=include_patterns, exclude_patterns=exclude_patterns, branch=branch, + tag=tag, include_gitignored=include_gitignored, token=token, output=output, @@ -162,6 +162,43 @@ def ingest( ) +def _override_branch_and_tag(query: IngestionQuery, branch: str | None, tag: str | None) -> None: + """Compare the caller-supplied ``branch`` and ``tag`` with the ones already in ``query``. + + If they differ, update ``query`` to the chosen values and issue a warning. + If both are specified, the tag wins over the branch. + + Parameters + ---------- + query : IngestionQuery + The query to update. + branch : str | None + The branch to use. + tag : str | None + The tag to use. + + """ + if tag and query.tag and tag != query.tag: + msg = f"Warning: The specified tag '{tag}' overrides the tag found in the URL '{query.tag}'." + warnings.warn(msg, RuntimeWarning, stacklevel=3) + + query.tag = tag or query.tag + + if branch and query.branch and branch != query.branch: + msg = f"Warning: The specified branch '{branch}' overrides the branch found in the URL '{query.branch}'." + warnings.warn(msg, RuntimeWarning, stacklevel=3) + + query.branch = branch or query.branch + + if tag and branch: + msg = "Warning: Both tag and branch are specified. The tag will be used." + warnings.warn(msg, RuntimeWarning, stacklevel=3) + + # Tag wins over branch if both supplied + if query.tag: + query.branch = None + + def _apply_gitignores(query: IngestionQuery) -> None: """Update ``query.ignore_patterns`` in-place. @@ -175,36 +212,30 @@ def _apply_gitignores(query: IngestionQuery) -> None: query.ignore_patterns.update(load_ignore_patterns(query.local_path, filename=fname)) -async def _clone_if_remote(query: IngestionQuery, token: str | None) -> None: - """Clone the repo if *query* points to a remote URL. +@asynccontextmanager +async def _clone_repo_if_remote(query: IngestionQuery, *, token: str | None) -> AsyncGenerator[None]: + """Async context-manager that clones ``query.url`` if present. + + If ``query.url`` is set, the repo is cloned, control is yielded, and the temp directory is removed on exit. + If no URL is given, the function simply yields immediately. Parameters ---------- query : IngestionQuery - The query to clone. + Parsed query describing the source to ingest. token : str | None GitHub personal access token (PAT) for accessing private repositories. - Raises - ------ - TypeError - If ``clone_repo`` does not return a coroutine. - """ - if not query.url: # local path ingestion - return - - # let CLI arg override, else keep the parsed branch - clone_cfg = query.extract_clone_config() - clone_coroutine = clone_repo(clone_cfg, token=token) - if not inspect.iscoroutine(clone_coroutine): - msg = "clone_repo did not return a coroutine as expected." - raise TypeError(msg) - - if asyncio.get_event_loop().is_running(): - await clone_coroutine - else: # running under sync context (unit-test, etc.) - asyncio.run(clone_coroutine) + if query.url: + clone_config = query.extract_clone_config() + await clone_repo(clone_config, token=token) + try: + yield + finally: + shutil.rmtree(query.local_path.parent) + else: + yield async def _write_output(tree: str, content: str, target: str | None) -> None: diff --git a/src/gitingest/output_formatter.py b/src/gitingest/output_formatter.py index fb1338fb..94bbee62 100644 --- a/src/gitingest/output_formatter.py +++ b/src/gitingest/output_formatter.py @@ -7,6 +7,7 @@ import tiktoken from gitingest.schemas import FileSystemNode, FileSystemNodeType +from gitingest.utils.compat_func import readlink if TYPE_CHECKING: from gitingest.query_parser import IngestionQuery @@ -157,7 +158,7 @@ def _create_tree_structure( if node.type == FileSystemNodeType.DIRECTORY: display_name += "/" elif node.type == FileSystemNodeType.SYMLINK: - display_name += " -> " + node.path.readlink().name + display_name += " -> " + readlink(node.path).name tree_str += f"{prefix}{current_prefix}{display_name}\n" diff --git a/src/gitingest/query_parser.py b/src/gitingest/query_parser.py index e9ca9664..db9cb3cb 100644 --- a/src/gitingest/query_parser.py +++ b/src/gitingest/query_parser.py @@ -11,7 +11,7 @@ from gitingest.config import TMP_BASE_PATH from gitingest.schemas import IngestionQuery from gitingest.utils.exceptions import InvalidPatternError -from gitingest.utils.git_utils import check_repo_exists, fetch_remote_branch_list +from gitingest.utils.git_utils import check_repo_exists, fetch_remote_branches_or_tags from gitingest.utils.ignore_patterns import DEFAULT_IGNORE_PATTERNS from gitingest.utils.query_parser_utils import ( KNOWN_GIT_HOSTS, @@ -165,28 +165,58 @@ async def _parse_remote_repo(source: str, token: str | None = None) -> Ingestion return parsed # If this is an issues page or pull requests, return early without processing subpath - if remaining_parts and possible_type in ("issues", "pull"): + # TODO: Handle issues and pull requests + if remaining_parts and possible_type in {"issues", "pull"}: + msg = f"Warning: Issues and pull requests are not yet supported: {url}. Returning repository root." + warnings.warn(msg, RuntimeWarning, stacklevel=2) return parsed - parsed.type = possible_type - - # Commit or branch - commit_or_branch = remaining_parts[0] - if _is_valid_git_commit_hash(commit_or_branch): - parsed.commit = commit_or_branch - remaining_parts.pop(0) - else: - parsed.branch = await _configure_branch_and_subpath(remaining_parts, url) + if possible_type not in {"tree", "blob"}: + # TODO: Handle other types + msg = f"Warning: Type '{possible_type}' is not yet supported: {url}. Returning repository root." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return parsed - # Subpath if anything left - if remaining_parts: + parsed.type = possible_type # 'tree' or 'blob' + + # Commit, branch, or tag + commit_or_branch_or_tag = remaining_parts[0] + if _is_valid_git_commit_hash(commit_or_branch_or_tag): # Commit + parsed.commit = commit_or_branch_or_tag + remaining_parts.pop(0) # Consume the commit hash + else: # Branch or tag + # Try to resolve a tag + parsed.tag = await _configure_branch_or_tag( + remaining_parts, + url=url, + ref_type="tags", + token=token, + ) + + # If no tag found, try to resolve a branch + if not parsed.tag: + parsed.branch = await _configure_branch_or_tag( + remaining_parts, + url=url, + ref_type="branches", + token=token, + ) + + # Only configure subpath if we have identified a commit, branch, or tag. + if remaining_parts and (parsed.commit or parsed.branch or parsed.tag): parsed.subpath += "/".join(remaining_parts) return parsed -async def _configure_branch_and_subpath(remaining_parts: list[str], url: str) -> str | None: - """Configure the branch and subpath based on the remaining parts of the URL. +async def _configure_branch_or_tag( + remaining_parts: list[str], + *, + url: str, + ref_type: str, + token: str | None = None, +) -> str | None: + """Configure the branch or tag based on the remaining parts of the URL. Parameters ---------- @@ -194,27 +224,49 @@ async def _configure_branch_and_subpath(remaining_parts: list[str], url: str) -> The remaining parts of the URL path. url : str The URL of the repository. + ref_type : str + The type of reference to configure. Can be "branches" or "tags". + token : str | None + GitHub personal access token (PAT) for accessing private repositories. Returns ------- str | None - The branch name if found, otherwise ``None``. + The branch or tag name if found, otherwise ``None``. + + Raises + ------ + ValueError + If the ``ref_type`` parameter is not "branches" or "tags". """ - try: - # Fetch the list of branches from the remote repository - branches: list[str] = await fetch_remote_branch_list(url) - except RuntimeError as exc: - warnings.warn(f"Warning: Failed to fetch branch list: {exc}", RuntimeWarning, stacklevel=2) - return remaining_parts.pop(0) + if ref_type not in ("branches", "tags"): + msg = f"Invalid reference type: {ref_type}" + raise ValueError(msg) - branch = [] - while remaining_parts: - branch.append(remaining_parts.pop(0)) - branch_name = "/".join(branch) - if branch_name in branches: - return branch_name + _ref_type = "tags" if ref_type == "tags" else "branches" + try: + # Fetch the list of branches or tags from the remote repository + branches_or_tags: list[str] = await fetch_remote_branches_or_tags(url, ref_type=_ref_type, token=token) + except RuntimeError as exc: + # If remote discovery fails, we optimistically treat the first path segment as the branch/tag. + msg = f"Warning: Failed to fetch {_ref_type}: {exc}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return remaining_parts.pop(0) if remaining_parts else None + + # Iterate over the path components and try to find a matching branch/tag + candidate_parts: list[str] = [] + + for part in remaining_parts: + candidate_parts.append(part) + candidate_name = "/".join(candidate_parts) + if candidate_name in branches_or_tags: + # We found a match — now consume exactly the parts that form the branch/tag + del remaining_parts[: len(candidate_parts)] + return candidate_name + + # No match found; leave remaining_parts intact return None @@ -278,14 +330,7 @@ def _parse_local_dir_path(path_str: str) -> IngestionQuery: """ path_obj = Path(path_str).resolve() slug = path_obj.name if path_str == "." else path_str.strip("/") - return IngestionQuery( - user_name=None, - repo_name=None, - url=None, - local_path=path_obj, - slug=slug, - id=str(uuid.uuid4()), - ) + return IngestionQuery(local_path=path_obj, slug=slug, id=str(uuid.uuid4())) async def try_domains_for_user_and_repo(user_name: str, repo_name: str, token: str | None = None) -> str: diff --git a/src/gitingest/schemas/filesystem.py b/src/gitingest/schemas/filesystem.py index 821c46e0..2fbe56d1 100644 --- a/src/gitingest/schemas/filesystem.py +++ b/src/gitingest/schemas/filesystem.py @@ -7,6 +7,7 @@ from enum import Enum, auto from typing import TYPE_CHECKING +from gitingest.utils.compat_func import readlink from gitingest.utils.file_utils import _decodes, _get_preferred_encodings, _read_chunk from gitingest.utils.notebook import process_notebook @@ -96,7 +97,7 @@ def content_string(self) -> str: parts = [ SEPARATOR, f"{self.type.name}: {str(self.path_str).replace(os.sep, '/')}" - + (f" -> {self.path.readlink().name}" if self.type == FileSystemNodeType.SYMLINK else ""), + + (f" -> {readlink(self.path).name}" if self.type == FileSystemNodeType.SYMLINK else ""), SEPARATOR, f"{self.content}", ] diff --git a/src/gitingest/schemas/ingestion.py b/src/gitingest/schemas/ingestion.py index 03c6083b..3e1c5e81 100644 --- a/src/gitingest/schemas/ingestion.py +++ b/src/gitingest/schemas/ingestion.py @@ -27,6 +27,8 @@ class CloneConfig: The specific commit hash to check out after cloning. branch : str | None The branch to clone. + tag: str | None + The tag to clone. subpath : str The subpath to clone from the repository (default: ``"/"``). blob: bool @@ -38,6 +40,7 @@ class CloneConfig: local_path: str commit: str | None = None branch: str | None = None + tag: str | None = None subpath: str = "/" blob: bool = False @@ -67,6 +70,8 @@ class IngestionQuery(BaseModel): # pylint: disable=too-many-instance-attributes The branch of the repository. commit : str | None The commit of the repository. + tag: str | None + The tag of the repository. max_file_size : int The maximum file size to ingest (default: 10 MB). ignore_patterns : set[str] @@ -86,6 +91,7 @@ class IngestionQuery(BaseModel): # pylint: disable=too-many-instance-attributes type: str | None = None branch: str | None = None commit: str | None = None + tag: str | None = None max_file_size: int = Field(default=MAX_FILE_SIZE) ignore_patterns: set[str] = set() # TODO: ignore_patterns and include_patterns have the same type include_patterns: set[str] | None = None @@ -113,6 +119,7 @@ def extract_clone_config(self) -> CloneConfig: local_path=str(self.local_path), commit=self.commit, branch=self.branch, + tag=self.tag, subpath=self.subpath, blob=self.type == "blob", ) diff --git a/src/gitingest/utils/compat_func.py b/src/gitingest/utils/compat_func.py new file mode 100644 index 00000000..0939d9be --- /dev/null +++ b/src/gitingest/utils/compat_func.py @@ -0,0 +1,44 @@ +"""Compatibility functions for Python 3.8.""" + +import os +from pathlib import Path + + +def readlink(path: Path) -> Path: + """Read the target of a symlink. + + Compatible with Python 3.8. + + Parameters + ---------- + path : Path + Path to the symlink. + + Returns + ------- + Path + The target of the symlink. + + """ + return Path(os.readlink(path)) + + +def removesuffix(s: str, suffix: str) -> str: + """Remove a suffix from a string. + + Compatible with Python 3.8. + + Parameters + ---------- + s : str + String to remove suffix from. + suffix : str + Suffix to remove. + + Returns + ------- + str + String with suffix removed. + + """ + return s[: -len(suffix)] if s.endswith(suffix) else s diff --git a/src/gitingest/utils/git_utils.py b/src/gitingest/utils/git_utils.py index 4c713502..2e1e2ebb 100644 --- a/src/gitingest/utils/git_utils.py +++ b/src/gitingest/utils/git_utils.py @@ -18,6 +18,7 @@ HTTP_404_NOT_FOUND, ) +from gitingest.utils.compat_func import removesuffix from gitingest.utils.exceptions import InvalidGitHubTokenError # GitHub Personal-Access tokens (classic + fine-grained). @@ -117,11 +118,10 @@ async def check_repo_exists(url: str, token: str | None = None) -> bool: # TODO: use `requests` instead of `curl` cmd: list[str] = [ "curl", - "--silent", - "--location", - "--head", + "--silent", # Suppress output + "--location", # Follow redirects "--write-out", - "%{http_code}", + "%{http_code}", # Write the HTTP status code to stdout "-o", os.devnull, ] @@ -131,7 +131,7 @@ async def check_repo_exists(url: str, token: str | None = None) -> bool: # Public GitHub vs. GitHub Enterprise base_api = "https://api.github.com" if host == "github.com" else f"https://{host}/api/v3" url = f"{base_api}/repos/{owner}/{repo}" - cmd += [f"Authorization: Bearer {token}"] + cmd += ["--header", f"Authorization: Bearer {token}"] cmd.append(url) @@ -185,7 +185,7 @@ def _parse_github_url(url: str) -> tuple[str, str, str]: msg = f"Un-recognised GitHub hostname: {parsed.hostname!r}" raise ValueError(msg) - parts = parsed.path.strip("/").removesuffix(".git").split("/") + parts = removesuffix(parsed.path, ".git").strip("/").split("/") expected_path_length = 2 if len(parts) != expected_path_length: msg = f"Path must look like //: {parsed.path!r}" @@ -195,13 +195,15 @@ def _parse_github_url(url: str) -> tuple[str, str, str]: return parsed.hostname, owner, repo -async def fetch_remote_branch_list(url: str, token: str | None = None) -> list[str]: - """Fetch the list of branches from a remote Git repository. +async def fetch_remote_branches_or_tags(url: str, *, ref_type: str, token: str | None = None) -> list[str]: + """Fetch the list of branches or tags from a remote Git repository. Parameters ---------- url : str - The URL of the Git repository to fetch branches from. + The URL of the Git repository to fetch branches or tags from. + ref_type: str + The type of reference to fetch. Can be "branches" or "tags". token : str | None GitHub personal access token (PAT) for accessing private repositories. @@ -210,22 +212,45 @@ async def fetch_remote_branch_list(url: str, token: str | None = None) -> list[s list[str] A list of branch names available in the remote repository. + Raises + ------ + ValueError + If the ``ref_type`` parameter is not "branches" or "tags". + """ + if ref_type not in ("branches", "tags"): + msg = f"Invalid fetch type: {ref_type}" + raise ValueError(msg) + cmd = ["git"] # Add authentication if needed if token and is_github_host(url): cmd += ["-c", create_git_auth_header(token, url=url)] - cmd += ["ls-remote", "--heads", url] + cmd += ["ls-remote"] + + fetch_tags = ref_type == "tags" + to_fetch = "tags" if fetch_tags else "heads" + + cmd += [f"--{to_fetch}"] + + # `--refs` filters out the peeled tag objects (those ending with "^{}") (for tags) + if fetch_tags: + cmd += ["--refs"] + + cmd += [url] await ensure_git_installed() stdout, _ = await run_command(*cmd) + # For each line in the output: + # - Skip empty lines and lines that don't contain "refs/{to_fetch}/" + # - Extract the branch or tag name after "refs/{to_fetch}/" return [ - line.split("refs/heads/", 1)[1] + line.split(f"refs/{to_fetch}/", 1)[1] for line in stdout.decode().splitlines() - if line.strip() and "refs/heads/" in line + if line.strip() and f"refs/{to_fetch}/" in line ] diff --git a/src/server/query_processor.py b/src/server/query_processor.py index e76c56e4..8674c18c 100644 --- a/src/server/query_processor.py +++ b/src/server/query_processor.py @@ -102,10 +102,7 @@ async def process_query( print(f"{Colors.BROWN}WARN{Colors.END}: {Colors.RED}<- {Colors.END}", end="") print(f"{Colors.RED}{exc}{Colors.END}") - return IngestErrorResponse( - error="Repository not found. Please make sure it is public." if "405" in str(exc) else "", - repo_url=short_repo_url, - ) + return IngestErrorResponse(error=str(exc), repo_url=short_repo_url) if len(content) > MAX_DISPLAY_SIZE: content = ( diff --git a/src/server/templates/base.jinja b/src/server/templates/base.jinja index cc33dce9..74ca8319 100644 --- a/src/server/templates/base.jinja +++ b/src/server/templates/base.jinja @@ -3,35 +3,44 @@ - - + {# Favicons #} + + + + + {# Search Engine Meta Tags #} + - - - - - - - - - - - - - + + {# Open Graph Meta Tags #} + - - - + + + + {# Web App Meta #} + + + + + + {# Twitter card #} + + + + + {# Title #} {% block title %} {% if short_repo_url %} @@ -41,26 +50,23 @@ {% endif %} {% endblock %} - - - - {% block extra_head %}{% endblock %} + {# Style sheets #} + + {% block css %}{% endblock %} {% include 'components/navbar.jinja' %} - + {# Main content wrapper #}
{% block content %}{% endblock %}
+ {# Footer #} {% include 'components/footer.jinja' %} - {% block extra_scripts %}{% endblock %} + {# Scripts #} + + + diff --git a/src/server/templates/components/_macros.jinja b/src/server/templates/components/_macros.jinja new file mode 100644 index 00000000..74d4612f --- /dev/null +++ b/src/server/templates/components/_macros.jinja @@ -0,0 +1,10 @@ +{# Icon link #} +{% macro icon_link(href, icon, label) -%} + + {{ label }} logo + {{ label }} + +{%- endmacro %} diff --git a/src/server/templates/components/badge_new.jinja b/src/server/templates/components/badge_new.jinja deleted file mode 100644 index dc6dfcad..00000000 --- a/src/server/templates/components/badge_new.jinja +++ /dev/null @@ -1 +0,0 @@ -NEW diff --git a/src/server/templates/components/footer.jinja b/src/server/templates/components/footer.jinja index eb561677..a360997b 100644 --- a/src/server/templates/components/footer.jinja +++ b/src/server/templates/components/footer.jinja @@ -1,40 +1,21 @@ +{% from 'components/_macros.jinja' import icon_link %}