From c38ca7e5994c13c2ec09c50822c5451b5d091e53 Mon Sep 17 00:00:00 2001 From: mifi Date: Tue, 17 Feb 2026 00:59:45 -0300 Subject: [PATCH] Umami analytics stack --- .env.example | 8 + .gitignore | 23 +++ .prettierignore | 2 + .prettierrc.yaml | 2 + .woodpecker/ci.yaml | 89 ++++++++++ .woodpecker/deploy.yaml | 69 ++++++++ AGENTS.md | 47 +++++ SETUP.md | 56 ++++++ docker-compose.yml | 54 ++++++ package.json | 18 ++ pnpm-lock.yaml | 381 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 749 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.yaml create mode 100644 .woodpecker/ci.yaml create mode 100644 .woodpecker/deploy.yaml create mode 100644 AGENTS.md create mode 100644 SETUP.md create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..61a5a21 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Umami stack environment variables. +# Set real values in Portainer (Stack β†’ Editor β†’ Environment variables) or in a +# local .env file. Do not commit real secrets to the repo. + +POSTGRES_DB=umami +POSTGRES_USER=umami +POSTGRES_PASSWORD=REPLACE_ME +APP_SECRET=REPLACE_ME diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b59ab58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Dependencies +node_modules/ + +# Environment (never commit real secrets) +.env +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# Editor / IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Logs and temp +*.log +.tmp/ +tmp/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ee89780 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +pnpm-lock.yaml diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..fe450ac --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,2 @@ +tabWidth: 2 +singleQuote: true diff --git a/.woodpecker/ci.yaml b/.woodpecker/ci.yaml new file mode 100644 index 0000000..c390516 --- /dev/null +++ b/.woodpecker/ci.yaml @@ -0,0 +1,89 @@ +# CI: format check (Prettier) and lint (YAML). Runs on every PR and every push to main. +when: + - event: pull_request + - branch: main + event: push + +steps: + - name: install + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + + - name: format check + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm format:check + depends_on: + - install + + - name: Send Prettier Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Prettier failure"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - format check + when: + - status: [failure] + + - name: lint + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm lint + depends_on: + - format check + + - name: Send Lint Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Lint failure"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - lint + when: + - status: [failure] + + - name: Send CI Pipeline Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] CI pipeline success"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - install + - format check + - lint + when: + - status: [success] diff --git a/.woodpecker/deploy.yaml b/.woodpecker/deploy.yaml new file mode 100644 index 0000000..a2eade0 --- /dev/null +++ b/.woodpecker/deploy.yaml @@ -0,0 +1,69 @@ +# Deploy: trigger Portainer stack redeploy (stack-from-repo; no image build). +# Runs on push/tag/manual to main only, after ci workflow succeeds. +skip_clone: true +workspace: + base: /tmp + path: deploy +when: + - branch: main + event: [push, tag, manual] + - event: deployment + evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"' + +depends_on: + - ci + +steps: + - name: Trigger Portainer stack redeploy + image: curlimages/curl:latest + environment: + PORTAINER_WEBHOOK_URL: + from_secret: portainer_webhook_url + commands: + - set -e + - echo "=== Triggering Portainer stack redeploy ===" + - | + resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL") + body=$(echo "$resp" | head -n -1) + code=$(echo "$resp" | tail -n 1) + if [ "$code" != "200" ] && [ "$code" != "204" ]; then + echo "Webhook failed (HTTP $code): $body" + exit 1 + fi + echo "βœ“ Portainer redeploy triggered (HTTP $code)" + + - name: Send Deploy Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success πŸŽ‰"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Trigger Portainer stack redeploy + when: + - status: [success] + + - name: Send Deploy Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure πŸ’©"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Trigger Portainer stack redeploy + when: + - status: [failure] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5b38c27 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +# Agent guide – Umami analytics stack + +This repo is a **self-hosted Umami analytics stack** for deployment via **Portainer** (stack from repository). It does not contain application source code; it defines the Docker Compose stack, CI, and docs. + +## Purpose + +- Run [Umami](https://umami.is) (privacy-focused analytics) with PostgreSQL. +- Reverse proxy: **Traefik** on the existing Docker network **marina-net**. +- Deploy by adding the stack in Portainer and pointing it at this repo. + +## Architecture + +- **marina-net** (external): Shared network where Traefik runs. Only the **umami** service from this stack is attached so Traefik can route to it. No database on this network. +- **umami-backend** (internal): Only **umami** and **db** (PostgreSQL). Isolated from Traefik and the rest of the host. +- **umami** is on both networks; **db** is only on umami-backend. +- **Volumes**: One external volume, **umami-db-data**, for PostgreSQL data. Declared with `external: true`; the user must create it (e.g. `docker volume create umami-db-data`) before first deploy. + +## Design decisions + +- **No secrets in repo**: All sensitive values (APP_SECRET, POSTGRES_PASSWORD, etc.) come from Portainer stack environment variables or a local `.env`; see `.env.example` and SETUP.md. +- **External volume only**: DB data lives in a named external volume with an `umami-` prefix so it is explicit and portable. +- **Traefik Host rule**: Default host is **analytics.mifi.holdings**; can be changed in `docker-compose.yml` if deploying elsewhere. HTTPS is handled by Traefik (entrypoints `web` and `websecure`); no cert paths in this repo. +- **Variable substitution**: Compose uses `${POSTGRES_DB}`, `${POSTGRES_USER}`, `${POSTGRES_PASSWORD}`, `${APP_SECRET}` so Portainer (or `.env`) injects values. + +## Key files + +| File | Role | +| ------------------------- | --------------------------------------------------------------------------------------------------- | +| `docker-compose.yml` | Stack definition: services, networks, volumes, Traefik labels, env substitution. | +| `.env.example` | List of env var names and placeholders; no real values. | +| `SETUP.md` | Human-facing setup: volume creation, secret generation, Portainer env vars, Host rule, first login. | +| `AGENTS.md` | This file; for agents and developers. | +| `.woodpecker/ci.yaml` | CI: install deps, format check (Prettier). | +| `.woodpecker/deploy.yaml` | Deploy: trigger Portainer webhook after CI (no image build in this repo). | + +## Where to change things + +- **Traefik hostname**: In `docker-compose.yml`, label `traefik.http.routers.umami.rule=Host(\`...\`)`. Default is **analytics.mifi.holdings**. +- **Secrets**: Only in Portainer stack env or a local `.env`; never commit real values. +- **DB volume name**: Compose uses `umami-db-data`; if you change it, update the `volumes` section and document the new name (e.g. in SETUP.md). + +## Deployment context + +- **Repo**: `git.mifi.dev/mifi-holdings/umami.git` +- **CI**: Woodpecker; pipelines under `.woodpecker/` (ci, deploy). Deploy depends on **ci** (not build); this repo has no Docker image build (stack-from-repo only). +- **Prerequisites**: Existing Docker network **marina-net** and Traefik so the Umami service can be reached via the configured host. +- **First login**: No default credentials; the admin account is created in the Umami UI on first visit. diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..7905a84 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,56 @@ +# Umami analytics stack – setup + +Deploy this stack in Portainer as **Stack β†’ Add stack β†’ Build method: Git repository**, then use the web editor and the steps below. + +## 1. Create the external volume (once) + +If the volume does not exist yet, create it on the host (or the node where the stack runs): + +```bash +docker volume create umami-db-data +``` + +The compose file expects this name; the stack will fail to start if the volume is missing. + +## 2. Generate secrets + +Run these commands and paste each output into the corresponding Portainer env var (no quotes): + +**APP_SECRET:** + +```bash +openssl rand -hex 32 +``` + +**POSTGRES_PASSWORD:** + +```bash +openssl rand -hex 32 +``` + +Use different values for each. + +## 3. Environment variables in Portainer + +In Portainer: **Stack β†’ your stack β†’ Editor β†’ Add/Edit environment variables** (or the β€œWeb editor” env vars section). Paste the block below, then replace the `REPLACE_ME` values with the outputs from step 2. + +``` +POSTGRES_DB=umami +POSTGRES_USER=umami +POSTGRES_PASSWORD=REPLACE_ME +APP_SECRET=REPLACE_ME +``` + +Create the external volume(s) (step 1) before the first deploy if they do not exist. + +## 4. Traefik Host rule + +The stack is configured for **analytics.mifi.holdings**. If you deploy elsewhere, edit `docker-compose.yml` and change the Traefik label: + +- `traefik.http.routers.umami.rule=Host(\`your-domain.example.com\`)` + +Then redeploy the stack (and reload Traefik if needed). + +## 5. First login + +After the stack is running, open the app at your configured host (e.g. https://analytics.mifi.holdings). Create the admin account in the Umami UI; there are no default credentials. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..79f5fd3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +services: + umami: + image: ghcr.io/umami-software/umami:postgresql-latest + restart: always + environment: + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + APP_SECRET: ${APP_SECRET} + depends_on: + db: + condition: service_healthy + healthcheck: + test: + ['CMD-SHELL', 'curl -f http://localhost:3000/api/heartbeat || exit 1'] + interval: 10s + timeout: 5s + retries: 3 + networks: + - marina-net + - umami-backend + labels: + - traefik.enable=true + - traefik.docker.network=marina-net + - traefik.http.routers.umami.rule=Host(`analytics.mifi.holdings`) + - traefik.http.routers.umami.entrypoints=websecure + - traefik.http.routers.umami.tls=true + - traefik.http.routers.umami.tls.certresolver=letsencrypt + - traefik.http.services.umami.loadbalancer.server.port=3000 + + db: + image: postgres:16-alpine + restart: always + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - umami-db-data:/var/lib/postgresql/data + networks: + - umami-backend + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'] + interval: 5s + timeout: 5s + retries: 5 + +networks: + marina-net: + external: true + umami-backend: + driver: bridge + +volumes: + umami-db-data: + external: true diff --git a/package.json b/package.json new file mode 100644 index 0000000..4055aa6 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "umami-stack", + "version": "1.0.0", + "private": true, + "packageManager": "pnpm@10.29.2", + "engines": { + "pnpm": ">=10" + }, + "scripts": { + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "yamllint docker-compose.yml .prettierrc.yaml .woodpecker/*.yaml" + }, + "devDependencies": { + "prettier": "3.4.2", + "yaml-lint": "1.7.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..2516933 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,381 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + prettier: + specifier: 3.4.2 + version: 3.4.2 + yaml-lint: + specifier: ^1.7.0 + version: 1.7.0 + +packages: + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + nconf@0.12.1: + resolution: {integrity: sha512-p2cfF+B3XXacQdswUYWZ0w6Vld0832A/tuqjLBu3H1sfUcby4N2oVbGhyuCkZv+t3iY3aiFEj7gZGqax9Q2c1w==} + engines: {node: '>= 0.4.0'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + secure-keys@1.0.0: + resolution: {integrity: sha512-nZi59hW3Sl5P3+wOO89eHBAAGwmCPd2aE1+dLZV5MO+ItQctIvAqihzaAXIQhvtH4KJPxM080HsnqltR2y8cWg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml-lint@1.7.0: + resolution: {integrity: sha512-zeBC/kskKQo4zuoGQ+IYjw6C9a/YILr2SXoEZA9jM0COrSwvwVbfTiFegT8qYBSBgOwLMWGL8sY137tOmFXGnQ==} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + +snapshots: + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + async@3.2.6: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + consola@2.15.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + emoji-regex@8.0.0: {} + + escalade@3.2.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + get-caller-file@2.0.5: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + ignore@5.3.2: {} + + ini@2.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + nconf@0.12.1: + dependencies: + async: 3.2.6 + ini: 2.0.0 + secure-keys: 1.0.0 + yargs: 16.2.0 + + path-type@4.0.0: {} + + picomatch@2.3.1: {} + + prettier@3.4.2: {} + + queue-microtask@1.2.3: {} + + require-directory@2.1.1: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + secure-keys@1.0.0: {} + + slash@3.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yaml-lint@1.7.0: + dependencies: + consola: 2.15.3 + globby: 11.1.0 + js-yaml: 4.1.1 + nconf: 0.12.1 + + yargs-parser@20.2.9: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9