Compare commits
	
		
			144 Commits
		
	
	
		
			v0.14.1
			...
			add3a6229e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					add3a6229e | ||
| 
						 | 
					2e406cac3d | ||
| 
						 | 
					6fa6978f30 | ||
| 
						 | 
					7691594b10 | ||
| 
						 | 
					17325a6e6d | ||
| 
						 | 
					d984891791 | ||
| 
						 | 
					78bac9c9ca | ||
| 
						 | 
					126e60fec7 | ||
| 
						 | 
					7a59070e91 | ||
| 
						 | 
					c9b65c7652 | ||
| 
						 | 
					13d9ce09a4 | ||
| 
						 | 
					f4fc053db3 | ||
| 
						 | 
					d3fc6a237f | ||
| 
						 | 
					8a888ccda6 | ||
| 
						 | 
					a4e20aa62a | ||
| 
						 | 
					1cc4862d51 | ||
| 
						 | 
					c6b64ced91 | ||
| 
						 | 
					38cbf093e5 | ||
| 
						 | 
					d8ddfa31e3 | ||
| 
						 | 
					c3e4f676fc | ||
| 
						 | 
					e668b828ea | ||
| 
						 | 
					eaeac2cb78 | ||
| 
						 | 
					5d74ec59c1 | ||
| 
						 | 
					645b29e5d1 | ||
| 
						 | 
					aee9677029 | ||
| 
						 | 
					da982dc831 | ||
| 
						 | 
					a64eaa3fc0 | ||
| 
						 | 
					a63651f715 | ||
| 
						 | 
					bb67b708e7 | ||
| 
						 | 
					dc0d37c71e | ||
| 
						 | 
					0287c4d458 | ||
| 
						 | 
					47be1061b7 | ||
| 
						 | 
					1c79de2f37 | ||
| 
						 | 
					9696cc7188 | ||
| 
						 | 
					082dd8c1f2 | ||
| 
						 | 
					43524dcdb1 | ||
| 
						 | 
					45a0540edf | ||
| 
						 | 
					2b784d1edc | ||
| 
						 | 
					8650cf9a63 | ||
| 
						 | 
					76c840dbaa | ||
| 
						 | 
					e78de6f6de | ||
| 
						 | 
					554edf5a27 | ||
| 
						 | 
					6fca398a12 | ||
| 
						 | 
					5bf3fbc10e | ||
| 
						 | 
					d994c38219 | ||
| 
						 | 
					3dccbfc797 | ||
| 
						 | 
					c3d461f102 | ||
| 
						 | 
					c0105889ab | ||
| 
						 | 
					c6006b58d2 | ||
| 
						 | 
					178f009458 | ||
| 
						 | 
					9c24cf4aba | ||
| 
						 | 
					af68498494 | ||
| 
						 | 
					eac22d53d3 | ||
| 
						 | 
					e5ac60c187 | ||
| 
						 | 
					4b42a5fbda | ||
| 
						 | 
					08a833f1cf | ||
| 
						 | 
					c0f0dc5192 | ||
| 
						 | 
					6452d0b357 | ||
| 
						 | 
					9f6b815197 | ||
| 
						 | 
					d8cbc0aaee | ||
| 
						 | 
					b1f70ec36c | ||
| 
						 | 
					311d2516ce | ||
| 
						 | 
					5957873534 | ||
| 
						 | 
					7524f304fe | ||
| 
						 | 
					f1730ede97 | ||
| 
						 | 
					7f9c8868fd | ||
| 
						 | 
					72b484a480 | ||
| 
						 | 
					c47c1dd4f4 | ||
| 
						 | 
					3975c70de7 | ||
| 
						 | 
					fd4e73e76c | ||
| 
						 | 
					301fab5c17 | ||
| 
						 | 
					2c90454244 | ||
| 
						 | 
					2db99edeaf | ||
| 
						 | 
					4fa471263f | ||
| 
						 | 
					435f654cbe | ||
| 
						 | 
					15b03d7561 | ||
| 
						 | 
					219e6a29e4 | ||
| 
						 | 
					a85edb6cee | ||
| 
						 | 
					43081c5179 | ||
| 
						 | 
					d390dce843 | ||
| 
						 | 
					e5939aaa5d | ||
| 
						 | 
					1db0c0f531 | ||
| 
						 | 
					eefd33ac88 | ||
| 
						 | 
					de10436c1a | ||
| 
						 | 
					81f109f830 | ||
| 
						 | 
					858ee28ef2 | ||
| 
						 | 
					b89afe1b0c | ||
| 
						 | 
					b4fedc1c1d | ||
| 
						 | 
					d24e8b4027 | ||
| 
						 | 
					93fbdbe0f3 | ||
| 
						 | 
					068d9b8716 | ||
| 
						 | 
					2295f23725 | ||
| 
						 | 
					fed587b4a4 | ||
| 
						 | 
					8f73f9c365 | ||
| 
						 | 
					363efc2e7f | ||
| 
						 | 
					cf93fed64b | ||
| 
						 | 
					99c689657f | ||
| 
						 | 
					394c98c65a | ||
| 
						 | 
					8f93ac29dd | ||
| 
						 | 
					5ffb7f4a01 | ||
| 
						 | 
					f5f718a84a | ||
| 
						 | 
					4c36a950a7 | ||
| 
						 | 
					a9bc9d7e8d | ||
| 
						 | 
					18fed70ddf | ||
| 
						 | 
					dd9d117ab8 | ||
| 
						 | 
					0e94fe354f | ||
| 
						 | 
					3b99c79495 | ||
| 
						 | 
					20e914c85b | ||
| 
						 | 
					efc4b3f84c | ||
| 
						 | 
					2bc6b52e99 | ||
| 
						 | 
					feb59e560b | ||
| 
						 | 
					17be8f3601 | ||
| 
						 | 
					3b053e8222 | ||
| 
						 | 
					9d5050d3ee | ||
| 
						 | 
					1bd56e1d0e | ||
| 
						 | 
					3f46abf261 | ||
| 
						 | 
					6a248e17be | ||
| 
						 | 
					8273a2a6a0 | ||
| 
						 | 
					78f52c769d | ||
| 
						 | 
					fe22b2f8fb | ||
| 
						 | 
					482421f10e | ||
| 
						 | 
					dcb15aee0e | ||
| 
						 | 
					827f22e2fc | ||
| 
						 | 
					bd36314f00 | ||
| 
						 | 
					4ad7892eab | ||
| 
						 | 
					31b7e62983 | ||
| 
						 | 
					0f5ef2f49c | ||
| 
						 | 
					2ce3fee70b | ||
| 
						 | 
					33e5bee9fb | ||
| 
						 | 
					b32f7dba5d | ||
| 
						 | 
					2661acbadb | ||
| 
						 | 
					d28c079a57 | ||
| 
						 | 
					29a159c094 | ||
| 
						 | 
					68dad51948 | ||
| 
						 | 
					3bf82b5b86 | ||
| 
						 | 
					e52e8c12cf | ||
| 
						 | 
					8f7a7faa91 | ||
| 
						 | 
					ff0edec652 | ||
| 
						 | 
					bbcecf274f | ||
| 
						 | 
					cc41be6856 | ||
| 
						 | 
					761f56b869 | ||
| 
						 | 
					d4e8eaadd7 | ||
| 
						 | 
					9f2bdadde7 | ||
| 
						 | 
					f789d9dfe3 | 
							
								
								
									
										1
									
								
								.bun-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
1.2.2
 | 
			
		||||
							
								
								
									
										69
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,69 @@
 | 
			
		||||
FROM debian:trixie-slim
 | 
			
		||||
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  curl \
 | 
			
		||||
  unzip \
 | 
			
		||||
  git \
 | 
			
		||||
  ca-certificates \
 | 
			
		||||
  build-essential \
 | 
			
		||||
  assimp-utils \
 | 
			
		||||
  calibre \
 | 
			
		||||
  dasel \
 | 
			
		||||
  dcraw \
 | 
			
		||||
  dvisvgm \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  ghostscript \
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  imagemagick-7.q16 \
 | 
			
		||||
  inkscape \
 | 
			
		||||
  latexmk \
 | 
			
		||||
  libheif-examples \
 | 
			
		||||
  libjxl-tools \
 | 
			
		||||
  libreoffice \
 | 
			
		||||
  libva2 \
 | 
			
		||||
  libvips-tools \
 | 
			
		||||
  libemail-outlook-message-perl \
 | 
			
		||||
  lmodern \
 | 
			
		||||
  mupdf-tools \
 | 
			
		||||
  pandoc \
 | 
			
		||||
  poppler-utils \
 | 
			
		||||
  potrace \
 | 
			
		||||
  python3-numpy \
 | 
			
		||||
  resvg \
 | 
			
		||||
  texlive \
 | 
			
		||||
  texlive-fonts-recommended \
 | 
			
		||||
  texlive-latex-extra \
 | 
			
		||||
  texlive-latex-recommended \
 | 
			
		||||
  texlive-xetex \
 | 
			
		||||
  --no-install-recommends \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
  curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
 | 
			
		||||
  else \
 | 
			
		||||
  curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
 | 
			
		||||
  fi && \
 | 
			
		||||
  unzip -j bun-linux-*.zip -d /usr/local/bin && \
 | 
			
		||||
  rm bun-linux-*.zip && \
 | 
			
		||||
  chmod +x /usr/local/bin/bun
 | 
			
		||||
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
  VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  else \
 | 
			
		||||
  VTRACER_ASSET="vtracer-x86_64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  fi && \
 | 
			
		||||
  curl -L -o /tmp/vtracer.tar.gz "https://github.com/visioncortex/vtracer/releases/download/0.6.4/${VTRACER_ASSET}" && \
 | 
			
		||||
  tar -xzf /tmp/vtracer.tar.gz -C /tmp/ && \
 | 
			
		||||
  mv /tmp/vtracer /usr/local/bin/vtracer && \
 | 
			
		||||
  chmod +x /usr/local/bin/vtracer && \
 | 
			
		||||
  rm /tmp/vtracer.tar.gz
 | 
			
		||||
 | 
			
		||||
RUN mkdir -p data
 | 
			
		||||
ENV NODE_ENV=development
 | 
			
		||||
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
CMD ["bun", "run", "dev"]
 | 
			
		||||
							
								
								
									
										50
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ConvertX Development Environment",
 | 
			
		||||
  "build": {
 | 
			
		||||
    "dockerfile": "Dockerfile"
 | 
			
		||||
  },
 | 
			
		||||
  "features": {
 | 
			
		||||
    "ghcr.io/devcontainers/features/git:1": {},
 | 
			
		||||
    "ghcr.io/devcontainers/features/github-cli:1": {}
 | 
			
		||||
  },
 | 
			
		||||
  "customizations": {
 | 
			
		||||
    "vscode": {
 | 
			
		||||
      "extensions": [
 | 
			
		||||
        "ms-vscode.vscode-typescript-next",
 | 
			
		||||
        "bradlc.vscode-tailwindcss",
 | 
			
		||||
        "esbenp.prettier-vscode",
 | 
			
		||||
        "dbaeumer.vscode-eslint",
 | 
			
		||||
        "ms-vscode.vscode-json",
 | 
			
		||||
        "ms-vscode.vscode-docker",
 | 
			
		||||
        "oven.bun-vscode"
 | 
			
		||||
      ],
 | 
			
		||||
      "settings": {
 | 
			
		||||
        "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
			
		||||
        "editor.formatOnSave": true,
 | 
			
		||||
        "editor.codeActionsOnSave": {
 | 
			
		||||
          "source.fixAll.eslint": "explicit"
 | 
			
		||||
        },
 | 
			
		||||
        "typescript.preferences.importModuleSpecifier": "relative",
 | 
			
		||||
        "typescript.suggest.autoImports": true,
 | 
			
		||||
        "tailwindCSS.includeLanguages": {
 | 
			
		||||
          "typescript": "javascript",
 | 
			
		||||
          "typescriptreact": "javascript"
 | 
			
		||||
        },
 | 
			
		||||
        "files.associations": {
 | 
			
		||||
          "*.css": "tailwindcss"
 | 
			
		||||
        },
 | 
			
		||||
        "terminal.integrated.defaultProfile.linux": "bash"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "forwardPorts": [3000],
 | 
			
		||||
  "portsAttributes": {
 | 
			
		||||
    "3000": {
 | 
			
		||||
      "label": "ConvertX Application",
 | 
			
		||||
      "onAutoForward": "notify"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "postCreateCommand": "bun install",
 | 
			
		||||
  "remoteUser": "root",
 | 
			
		||||
  "mounts": ["source=${localWorkspaceFolder}/data,target=/app/data,type=bind"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,10 +1,9 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a report to help us improve
 | 
			
		||||
title: ''
 | 
			
		||||
title: ""
 | 
			
		||||
labels: bug
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
@@ -12,10 +11,12 @@ A clear and concise description of what the bug is.
 | 
			
		||||
 | 
			
		||||
**To Reproduce**
 | 
			
		||||
Steps to reproduce the behavior:
 | 
			
		||||
 | 
			
		||||
1. Go to '...'
 | 
			
		||||
2. Click on '....'
 | 
			
		||||
3. Scroll down to '....'
 | 
			
		||||
4. See error
 | 
			
		||||
 | 
			
		||||
**Checklist:**
 | 
			
		||||
 | 
			
		||||
- [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true`
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/converter_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
---
 | 
			
		||||
name: Converter request
 | 
			
		||||
about: Suggest a converter for this project
 | 
			
		||||
title: "[Converter Request]"
 | 
			
		||||
labels: "converter request"
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**What file formats are missing?**
 | 
			
		||||
 | 
			
		||||
<!-- Provide an example of what you would like to convert -->
 | 
			
		||||
 | 
			
		||||
**What converter should be added**
 | 
			
		||||
 | 
			
		||||
<!-- It has to be free and preferably open source -->
 | 
			
		||||
 | 
			
		||||
**Are you willing to add it?**
 | 
			
		||||
 | 
			
		||||
<!-- Adding a converter is very easy just copy one of the existing and modify it -->
 | 
			
		||||
 | 
			
		||||
- [ ] Yes
 | 
			
		||||
- [ ] No
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
 | 
			
		||||
<!-- Add any other context or screenshots about the feature request here. -->
 | 
			
		||||
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -3,8 +3,7 @@ name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
title: "[Feature Request]"
 | 
			
		||||
labels: enhancement
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/check-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
name: Check Lint
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    name: Run linting checks
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Set up Bun
 | 
			
		||||
        uses: oven-sh/setup-bun@v2
 | 
			
		||||
        with:
 | 
			
		||||
          bun-version: 1.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: bun install
 | 
			
		||||
 | 
			
		||||
      - name: Run lint
 | 
			
		||||
        run: bun run lint
 | 
			
		||||
							
								
								
									
										39
									
								
								.github/workflows/docker-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -10,7 +10,6 @@ on:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
env:
 | 
			
		||||
  GHCR_IMAGE: ghcr.io/c4illin/convertx
 | 
			
		||||
  IMAGE_NAME: ${{ github.repository }}
 | 
			
		||||
  DOCKERHUB_USERNAME: c4illin
 | 
			
		||||
 | 
			
		||||
@@ -32,8 +31,7 @@ jobs:
 | 
			
		||||
      contents: write
 | 
			
		||||
      packages: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      checks: write
 | 
			
		||||
      actions: read
 | 
			
		||||
      id-token: write
 | 
			
		||||
 | 
			
		||||
    runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
 | 
			
		||||
 | 
			
		||||
@@ -51,22 +49,26 @@ jobs:
 | 
			
		||||
          echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: downcase REPO
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
 | 
			
		||||
 | 
			
		||||
      - name: Docker meta default
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          images: ${{ env.GHCR_IMAGE }}
 | 
			
		||||
          images: ghcr.io/${{ env.REPO }}
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          platforms: ${{ matrix.platform }}
 | 
			
		||||
 | 
			
		||||
      - name: Login to GitHub Container Registry
 | 
			
		||||
        # here we only login to ghcr.io since the this only pushes internal images
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
@@ -74,7 +76,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Build and push by digest
 | 
			
		||||
        id: build
 | 
			
		||||
        uses: docker/build-push-action@v6.18.0
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_BUILDKIT: 1
 | 
			
		||||
        with:
 | 
			
		||||
@@ -82,7 +84,8 @@ jobs:
 | 
			
		||||
          platforms: ${{ matrix.platform }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
          annotations: ${{ steps.meta.outputs.annotations }}
 | 
			
		||||
          outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true
 | 
			
		||||
          outputs: type=image,name=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,oci-mediatypes=true
 | 
			
		||||
          push: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
 | 
			
		||||
          cache-from: type=gha,scope=${{ matrix.platform }}
 | 
			
		||||
          cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
 | 
			
		||||
 | 
			
		||||
@@ -101,30 +104,36 @@ jobs:
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  merge:
 | 
			
		||||
    if: github.event.pull_request.head.repo.full_name == github.repository
 | 
			
		||||
    name: Merge Docker manifests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    permissions:
 | 
			
		||||
      attestations: write
 | 
			
		||||
      contents: read
 | 
			
		||||
      contents: write
 | 
			
		||||
      packages: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      id-token: write
 | 
			
		||||
 | 
			
		||||
    needs:
 | 
			
		||||
      - build
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v4
 | 
			
		||||
        uses: actions/download-artifact@v5
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
 | 
			
		||||
      - name: downcase REPO
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
 | 
			
		||||
 | 
			
		||||
      - name: Extract Docker metadata
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            ${{ env.GHCR_IMAGE }}
 | 
			
		||||
            ghcr.io/${{ env.REPO }}
 | 
			
		||||
            ${{ env.IMAGE_NAME }}
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
@@ -157,8 +166,8 @@ jobs:
 | 
			
		||||
            --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \
 | 
			
		||||
            $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
 | 
			
		||||
            $(printf 'ghcr.io/${{ env.REPO }}@sha256:%s ' *)
 | 
			
		||||
 | 
			
		||||
      - name: Inspect image
 | 
			
		||||
        run: |
 | 
			
		||||
          docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}'
 | 
			
		||||
          docker buildx imagetools inspect 'ghcr.io/${{ env.REPO }}:${{ steps.meta.outputs.version }}'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/dockerhub-description.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -15,10 +15,10 @@ jobs:
 | 
			
		||||
  dockerHubDescription:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Docker Hub Description
 | 
			
		||||
        uses: peter-evans/dockerhub-description@v4
 | 
			
		||||
        uses: peter-evans/dockerhub-description@v5
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ env.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/run-bun-test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
name: Check Tests
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    name: Run tests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Set up Bun
 | 
			
		||||
        uses: oven-sh/setup-bun@v2
 | 
			
		||||
        with:
 | 
			
		||||
          bun-version: 1.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: bun install
 | 
			
		||||
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        run: bun test
 | 
			
		||||
							
								
								
									
										103
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,51 +1,52 @@
 | 
			
		||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# next.js
 | 
			
		||||
/.next/
 | 
			
		||||
/out/
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
 | 
			
		||||
# local env files
 | 
			
		||||
.env.local
 | 
			
		||||
.env.development.local
 | 
			
		||||
.env.test.local
 | 
			
		||||
.env.production.local
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
 | 
			
		||||
**/*.trace
 | 
			
		||||
**/*.zip
 | 
			
		||||
**/*.tar.gz
 | 
			
		||||
**/*.tgz
 | 
			
		||||
**/*.log
 | 
			
		||||
package-lock.json
 | 
			
		||||
**/*.bun
 | 
			
		||||
/src/uploads
 | 
			
		||||
/uploads
 | 
			
		||||
/mydb.sqlite
 | 
			
		||||
/output
 | 
			
		||||
/db
 | 
			
		||||
/data
 | 
			
		||||
/Bruno
 | 
			
		||||
/tsconfig.tsbuildinfo
 | 
			
		||||
/public/generated.css
 | 
			
		||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# next.js
 | 
			
		||||
/.next/
 | 
			
		||||
/out/
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
 | 
			
		||||
# local env files
 | 
			
		||||
.env.local
 | 
			
		||||
.env.development.local
 | 
			
		||||
.env.test.local
 | 
			
		||||
.env.production.local
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
 | 
			
		||||
**/*.trace
 | 
			
		||||
**/*.zip
 | 
			
		||||
**/*.tar.gz
 | 
			
		||||
**/*.tgz
 | 
			
		||||
**/*.log
 | 
			
		||||
package-lock.json
 | 
			
		||||
**/*.bun
 | 
			
		||||
/src/uploads
 | 
			
		||||
/uploads
 | 
			
		||||
/mydb.sqlite
 | 
			
		||||
/output
 | 
			
		||||
/db
 | 
			
		||||
/data
 | 
			
		||||
/dist
 | 
			
		||||
/Bruno
 | 
			
		||||
/tsconfig.tsbuildinfo
 | 
			
		||||
/public/generated.css
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,9 @@
 | 
			
		||||
 | 
			
		||||
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311)
 | 
			
		||||
- change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311)
 | 
			
		||||
 | 
			
		||||
## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										183
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@@ -1,79 +1,104 @@
 | 
			
		||||
FROM debian:trixie-slim AS base
 | 
			
		||||
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# install bun
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  curl \
 | 
			
		||||
  unzip \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# if architecture is arm64, use the arm64 version of bun
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
    curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
 | 
			
		||||
  else \
 | 
			
		||||
    curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
RUN unzip -j bun-linux-*.zip -d /usr/local/bin && \
 | 
			
		||||
  rm bun-linux-*.zip && \
 | 
			
		||||
  chmod +x /usr/local/bin/bun
 | 
			
		||||
 | 
			
		||||
# install dependencies into temp directory
 | 
			
		||||
# this will cache them and speed up future builds
 | 
			
		||||
FROM base AS install
 | 
			
		||||
RUN mkdir -p /temp/dev
 | 
			
		||||
COPY package.json bun.lock /temp/dev/
 | 
			
		||||
RUN cd /temp/dev && bun install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
# install with --production (exclude devDependencies)
 | 
			
		||||
RUN mkdir -p /temp/prod
 | 
			
		||||
COPY package.json bun.lock /temp/prod/
 | 
			
		||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
 | 
			
		||||
 | 
			
		||||
FROM base AS prerelease
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY --from=install /temp/dev/node_modules node_modules
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# ENV NODE_ENV=production
 | 
			
		||||
RUN bun run build
 | 
			
		||||
 | 
			
		||||
# copy production dependencies and source code into final image
 | 
			
		||||
FROM base AS release
 | 
			
		||||
 | 
			
		||||
# install additional dependencies
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  assimp-utils \
 | 
			
		||||
  calibre \
 | 
			
		||||
  dcraw \
 | 
			
		||||
  dvisvgm \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  ghostscript \
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  imagemagick-7.q16 \
 | 
			
		||||
  inkscape \
 | 
			
		||||
  libheif-examples \
 | 
			
		||||
  libjxl-tools \
 | 
			
		||||
  libva2 \
 | 
			
		||||
  libvips-tools \
 | 
			
		||||
  mupdf-tools \
 | 
			
		||||
  pandoc \
 | 
			
		||||
  poppler-utils \
 | 
			
		||||
  potrace \
 | 
			
		||||
  python3-numpy \
 | 
			
		||||
  resvg \
 | 
			
		||||
  texlive \
 | 
			
		||||
  texlive-latex-extra \
 | 
			
		||||
  texlive-xetex \
 | 
			
		||||
  --no-install-recommends \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
COPY --from=install /temp/prod/node_modules node_modules
 | 
			
		||||
COPY --from=prerelease /app/public/generated.css /app/public/
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000/tcp
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
 | 
			
		||||
FROM debian:trixie-slim AS base
 | 
			
		||||
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# install bun
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  curl \
 | 
			
		||||
  unzip \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# if architecture is arm64, use the arm64 version of bun
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
    curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
 | 
			
		||||
  else \
 | 
			
		||||
    curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
RUN unzip -j bun-linux-*.zip -d /usr/local/bin && \
 | 
			
		||||
  rm bun-linux-*.zip && \
 | 
			
		||||
  chmod +x /usr/local/bin/bun
 | 
			
		||||
 | 
			
		||||
# install dependencies into temp directory
 | 
			
		||||
# this will cache them and speed up future builds
 | 
			
		||||
FROM base AS install
 | 
			
		||||
RUN mkdir -p /temp/dev
 | 
			
		||||
COPY package.json bun.lock /temp/dev/
 | 
			
		||||
RUN cd /temp/dev && bun install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
# install with --production (exclude devDependencies)
 | 
			
		||||
RUN mkdir -p /temp/prod
 | 
			
		||||
COPY package.json bun.lock /temp/prod/
 | 
			
		||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
 | 
			
		||||
 | 
			
		||||
FROM base AS prerelease
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY --from=install /temp/dev/node_modules node_modules
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# ENV NODE_ENV=production
 | 
			
		||||
RUN bun run build
 | 
			
		||||
 | 
			
		||||
# copy production dependencies and source code into final image
 | 
			
		||||
FROM base AS release
 | 
			
		||||
 | 
			
		||||
# install additional dependencies 
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  assimp-utils \
 | 
			
		||||
  calibre \
 | 
			
		||||
  dasel \
 | 
			
		||||
  dcraw \
 | 
			
		||||
  dvisvgm \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  ghostscript \
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  imagemagick-7.q16 \
 | 
			
		||||
  inkscape \
 | 
			
		||||
  latexmk \
 | 
			
		||||
  libheif-examples \
 | 
			
		||||
  libjxl-tools \
 | 
			
		||||
  libreoffice \
 | 
			
		||||
  libva2 \
 | 
			
		||||
  libvips-tools \
 | 
			
		||||
  libemail-outlook-message-perl \
 | 
			
		||||
  lmodern \
 | 
			
		||||
  mupdf-tools \
 | 
			
		||||
  pandoc \
 | 
			
		||||
  poppler-utils \
 | 
			
		||||
  potrace \
 | 
			
		||||
  python3-numpy \
 | 
			
		||||
  resvg \
 | 
			
		||||
  texlive \
 | 
			
		||||
  texlive-fonts-recommended \
 | 
			
		||||
  texlive-latex-extra \
 | 
			
		||||
  texlive-latex-recommended \
 | 
			
		||||
  texlive-xetex \
 | 
			
		||||
  --no-install-recommends \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Install VTracer binary
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
    VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  else \
 | 
			
		||||
    VTRACER_ASSET="vtracer-x86_64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  fi && \
 | 
			
		||||
  curl -L -o /tmp/vtracer.tar.gz "https://github.com/visioncortex/vtracer/releases/download/0.6.4/${VTRACER_ASSET}" && \
 | 
			
		||||
  tar -xzf /tmp/vtracer.tar.gz -C /tmp/ && \
 | 
			
		||||
  mv /tmp/vtracer /usr/local/bin/vtracer && \
 | 
			
		||||
  chmod +x /usr/local/bin/vtracer && \
 | 
			
		||||
  rm /tmp/vtracer.tar.gz
 | 
			
		||||
 | 
			
		||||
COPY --from=install /temp/prod/node_modules node_modules
 | 
			
		||||
COPY --from=prerelease /app/public/ /app/public/
 | 
			
		||||
COPY --from=prerelease /app/dist /app/dist
 | 
			
		||||
 | 
			
		||||
# COPY . .
 | 
			
		||||
RUN mkdir data
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000/tcp
 | 
			
		||||
# used for calibre
 | 
			
		||||
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]
 | 
			
		||||
							
								
								
									
										68
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -25,22 +25,24 @@ A self-hosted online file converter. Supports over a thousand different formats.
 | 
			
		||||
 | 
			
		||||
## Converters supported
 | 
			
		||||
 | 
			
		||||
| Converter                                        | Use case         | Converts from | Converts to |
 | 
			
		||||
| ------------------------------------------------ | ---------------- | ------------- | ----------- |
 | 
			
		||||
| [libjxl](https://github.com/libjxl/libjxl)       | JPEG XL          | 11            | 11          |
 | 
			
		||||
| [resvg](https://github.com/RazrFalcon/resvg)     | SVG              | 1             | 1           |
 | 
			
		||||
| [Vips](https://github.com/libvips/libvips)       | Images           | 45            | 23          |
 | 
			
		||||
| [libheif](https://github.com/strukturag/libheif) | HEIF             | 2             | 4           |
 | 
			
		||||
| [XeLaTeX](https://tug.org/xetex/)                | LaTeX            | 1             | 1           |
 | 
			
		||||
| [Calibre](https://calibre-ebook.com/)            | E-books          | 26            | 19          |
 | 
			
		||||
| [Pandoc](https://pandoc.org/)                    | Documents        | 43            | 65          |
 | 
			
		||||
| [dvisvgm](https://dvisvgm.de/)                   | Vector images    | 4             | 2           |
 | 
			
		||||
| [ImageMagick](https://imagemagick.org/)          | Images           | 245           | 183         |
 | 
			
		||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images           | 167           | 130         |
 | 
			
		||||
| [Inkscape](https://inkscape.org/)                | Vector images    | 7             | 17          |
 | 
			
		||||
| [Assimp](https://github.com/assimp/assimp)       | 3D Assets        | 77            | 23          |
 | 
			
		||||
| [FFmpeg](https://ffmpeg.org/)                    | Video            | ~472          | ~199        |
 | 
			
		||||
| [Potrace](https://potrace.sourceforge.net/)      | Raster to vector | 4             | 11          |
 | 
			
		||||
| Converter                                          | Use case         | Converts from | Converts to |
 | 
			
		||||
| -------------------------------------------------- | ---------------- | ------------- | ----------- |
 | 
			
		||||
| [libjxl](https://github.com/libjxl/libjxl)         | JPEG XL          | 11            | 11          |
 | 
			
		||||
| [resvg](https://github.com/RazrFalcon/resvg)       | SVG              | 1             | 1           |
 | 
			
		||||
| [Vips](https://github.com/libvips/libvips)         | Images           | 45            | 23          |
 | 
			
		||||
| [libheif](https://github.com/strukturag/libheif)   | HEIF             | 2             | 4           |
 | 
			
		||||
| [XeLaTeX](https://tug.org/xetex/)                  | LaTeX            | 1             | 1           |
 | 
			
		||||
| [Calibre](https://calibre-ebook.com/)              | E-books          | 26            | 19          |
 | 
			
		||||
| [Pandoc](https://pandoc.org/)                      | Documents        | 43            | 65          |
 | 
			
		||||
| [dvisvgm](https://dvisvgm.de/)                     | Vector images    | 4             | 2           |
 | 
			
		||||
| [ImageMagick](https://imagemagick.org/)            | Images           | 245           | 183         |
 | 
			
		||||
| [GraphicsMagick](http://www.graphicsmagick.org/)   | Images           | 167           | 130         |
 | 
			
		||||
| [Inkscape](https://inkscape.org/)                  | Vector images    | 7             | 17          |
 | 
			
		||||
| [Assimp](https://github.com/assimp/assimp)         | 3D Assets        | 77            | 23          |
 | 
			
		||||
| [FFmpeg](https://ffmpeg.org/)                      | Video            | ~472          | ~199        |
 | 
			
		||||
| [Potrace](https://potrace.sourceforge.net/)        | Raster to vector | 4             | 11          |
 | 
			
		||||
| [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8             | 1           |
 | 
			
		||||
| [Dasel](https://github.com/TomWright/dasel)        | Data Files       | 5             | 4           |
 | 
			
		||||
 | 
			
		||||
<!-- many ffmpeg fileformats are duplicates -->
 | 
			
		||||
 | 
			
		||||
@@ -62,6 +64,7 @@ services:
 | 
			
		||||
      - "3000:3000"
 | 
			
		||||
    environment:
 | 
			
		||||
      - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
 | 
			
		||||
      # - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./data:/app/data
 | 
			
		||||
```
 | 
			
		||||
@@ -80,16 +83,18 @@ If you get unable to open database file run `chown -R $USER:$USER path` on the p
 | 
			
		||||
 | 
			
		||||
All are optional, JWT_SECRET is recommended to be set.
 | 
			
		||||
 | 
			
		||||
| Name                      | Default                                            | Description                                                                                              |
 | 
			
		||||
| ------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
 | 
			
		||||
| JWT_SECRET                | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token                                                 |
 | 
			
		||||
| ACCOUNT_REGISTRATION      | false                                              | Allow users to register accounts                                                                         |
 | 
			
		||||
| HTTP_ALLOWED              | false                                              | Allow HTTP connections, only set this to true locally                                                    |
 | 
			
		||||
| ALLOW_UNAUTHENTICATED     | false                                              | Allow unauthenticated users to use the service, only set this to true locally                            |
 | 
			
		||||
| AUTO_DELETE_EVERY_N_HOURS | 24                                                 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable                  |
 | 
			
		||||
| WEBROOT                   |                                                    | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
 | 
			
		||||
| FFMPEG_ARGS               |                                                    | Arguments to pass to ffmpeg, e.g. `-preset veryfast`                                                     |
 | 
			
		||||
| HIDE_HISTORY              | false                                              | Hide the history page                                                                                    |
 | 
			
		||||
| Name                         | Default                                            | Description                                                                                                               |
 | 
			
		||||
| ---------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
 | 
			
		||||
| JWT_SECRET                   | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token                                                                  |
 | 
			
		||||
| ACCOUNT_REGISTRATION         | false                                              | Allow users to register accounts                                                                                          |
 | 
			
		||||
| HTTP_ALLOWED                 | false                                              | Allow HTTP connections, only set this to true locally                                                                     |
 | 
			
		||||
| ALLOW_UNAUTHENTICATED        | false                                              | Allow unauthenticated users to use the service, only set this to true locally                                             |
 | 
			
		||||
| AUTO_DELETE_EVERY_N_HOURS    | 24                                                 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable                                   |
 | 
			
		||||
| WEBROOT                      |                                                    | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/"                  |
 | 
			
		||||
| FFMPEG_ARGS                  |                                                    | Arguments to pass to ffmpeg, e.g. `-preset veryfast`                                                                      |
 | 
			
		||||
| HIDE_HISTORY                 | false                                              | Hide the history page                                                                                                     |
 | 
			
		||||
| LANGUAGE                     | en                                                 | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) |
 | 
			
		||||
| UNAUTHENTICATED_USER_SHARING | false                                              | Shares conversion history between all unauthenticated users                                                               |
 | 
			
		||||
 | 
			
		||||
### Docker images
 | 
			
		||||
 | 
			
		||||
@@ -129,19 +134,10 @@ Tutorial in chinese: <https://xzllll.com/24092901/>
 | 
			
		||||
2. `bun install`
 | 
			
		||||
3. `bun run dev`
 | 
			
		||||
 | 
			
		||||
Pull requests are welcome! See below and open issues for the list of todos.
 | 
			
		||||
Pull requests are welcome! See open issues for the list of todos. The ones tagged with "converter request" are quite easy. Help with docs and cleaning up in issues are also very welcome!
 | 
			
		||||
 | 
			
		||||
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
 | 
			
		||||
 | 
			
		||||
## Todo
 | 
			
		||||
 | 
			
		||||
- [ ] Add options for converters
 | 
			
		||||
- [ ] Add tests
 | 
			
		||||
- [ ] Make errors logs visible from the web ui
 | 
			
		||||
- [ ] Add more converters:
 | 
			
		||||
  - [ ] [deark](https://github.com/jsummers/deark)
 | 
			
		||||
  - [ ] LibreOffice
 | 
			
		||||
 | 
			
		||||
## Contributors
 | 
			
		||||
 | 
			
		||||
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										438
									
								
								bun.lock
									
									
									
									
									
								
							
							
						
						@@ -4,120 +4,118 @@
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "convertx-frontend",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@elysiajs/html": "^1.3.0",
 | 
			
		||||
        "@elysiajs/jwt": "^1.3.0",
 | 
			
		||||
        "@elysiajs/static": "^1.3.0",
 | 
			
		||||
        "@kitajs/html": "^4.2.9",
 | 
			
		||||
        "elysia": "^1.3.1",
 | 
			
		||||
        "@elysiajs/html": "^1.4.0",
 | 
			
		||||
        "@elysiajs/jwt": "^1.4.0",
 | 
			
		||||
        "@elysiajs/static": "^1.4.0",
 | 
			
		||||
        "@kitajs/html": "^4.2.10",
 | 
			
		||||
        "elysia": "^1.4.9",
 | 
			
		||||
        "sanitize-filename": "^1.6.3",
 | 
			
		||||
        "tar": "^7.5.1",
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@eslint/js": "^9.27.0",
 | 
			
		||||
        "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
 | 
			
		||||
        "@kitajs/ts-html-plugin": "^4.1.1",
 | 
			
		||||
        "@tailwindcss/cli": "^4.1.7",
 | 
			
		||||
        "@tailwindcss/postcss": "^4.1.7",
 | 
			
		||||
        "@eslint/js": "^9.37.0",
 | 
			
		||||
        "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
 | 
			
		||||
        "@kitajs/ts-html-plugin": "^4.1.3",
 | 
			
		||||
        "@tailwindcss/cli": "^4.1.14",
 | 
			
		||||
        "@tailwindcss/postcss": "^4.1.14",
 | 
			
		||||
        "@total-typescript/ts-reset": "^0.6.1",
 | 
			
		||||
        "@types/bun": "^1.2.14",
 | 
			
		||||
        "@types/node": "^22.15.21",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.33.1",
 | 
			
		||||
        "eslint": "^9.27.0",
 | 
			
		||||
        "eslint-plugin-better-tailwindcss": "^3.0.0",
 | 
			
		||||
        "@types/bun": "latest",
 | 
			
		||||
        "@types/node": "^24.6.2",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.45.0",
 | 
			
		||||
        "eslint": "^9.37.0",
 | 
			
		||||
        "eslint-plugin-better-tailwindcss": "^3.7.9",
 | 
			
		||||
        "eslint-plugin-simple-import-sort": "^12.1.1",
 | 
			
		||||
        "globals": "^16.1.0",
 | 
			
		||||
        "knip": "^5.57.2",
 | 
			
		||||
        "npm-run-all2": "^8.0.3",
 | 
			
		||||
        "postcss": "^8.5.3",
 | 
			
		||||
        "prettier": "^3.5.3",
 | 
			
		||||
        "globals": "^16.4.0",
 | 
			
		||||
        "knip": "^5.64.1",
 | 
			
		||||
        "npm-run-all2": "^8.0.4",
 | 
			
		||||
        "postcss": "^8.5.6",
 | 
			
		||||
        "prettier": "^3.6.2",
 | 
			
		||||
        "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
        "tailwindcss": "^4.1.7",
 | 
			
		||||
        "typescript": "^5.8.3",
 | 
			
		||||
        "typescript-eslint": "^8.32.1",
 | 
			
		||||
        "tailwindcss": "^4.1.14",
 | 
			
		||||
        "typescript": "^5.9.3",
 | 
			
		||||
        "typescript-eslint": "^8.45.0",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "@tailwindcss/oxide",
 | 
			
		||||
    "oxc-resolver",
 | 
			
		||||
    "@parcel/watcher",
 | 
			
		||||
  ],
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
 | 
			
		||||
 | 
			
		||||
    "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
 | 
			
		||||
    "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
 | 
			
		||||
    "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/generator": ["@babel/generator@7.26.5", "", { "dependencies": { "@babel/parser": "^7.26.5", "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw=="],
 | 
			
		||||
    "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
 | 
			
		||||
    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
 | 
			
		||||
    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/parser": ["@babel/parser@7.26.7", "", { "dependencies": { "@babel/types": "^7.26.7" }, "bin": "./bin/babel-parser.js" }, "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w=="],
 | 
			
		||||
    "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="],
 | 
			
		||||
    "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/traverse": ["@babel/traverse@7.26.7", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA=="],
 | 
			
		||||
    "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="],
 | 
			
		||||
    "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/html": ["@elysiajs/html@1.3.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-NpujllWwiEXdsX8GJhbBppOv7+aJr+OU7Gn3K8fVXpwieutwau0/B/M6vzjYXsh9OaoGByUTpL8U9rA/tVSn7w=="],
 | 
			
		||||
    "@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/jwt": ["@elysiajs/jwt@1.3.1", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-BVLAp0ER4839bR82ElgTsI7OoPxvFWP5u02KMgqpNoAM6xirJBYTKqANzY9ghuMfQoVEuD4B1/8lZwdPKAVg9Q=="],
 | 
			
		||||
    "@elysiajs/jwt": ["@elysiajs/jwt@1.4.0", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Z0PvZhQxdDeKZ8HslXzDoXXD83NKExNPmoiAPki3nI2Xvh5wtUrBH+zWOD17yP14IbRo8fxGj3L25MRCAPsgPA=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="],
 | 
			
		||||
    "@elysiajs/static": ["@elysiajs/static@1.4.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-ejZIgmRRethfBrr1iKEca4iBqGjVGppveyVyM0LG7JUhnxns9NtFTC6UQoQLUkRzMRlF65veQg32jwMS01dwzA=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
 | 
			
		||||
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
 | 
			
		||||
    "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="],
 | 
			
		||||
    "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
 | 
			
		||||
    "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/css-tree": ["@eslint/css-tree@3.6.5", "", { "dependencies": { "mdn-data": "2.23.0", "source-map-js": "^1.0.1" } }, "sha512-bJgnXu0D0K1BbfPfHTmCaJe2ucBOjeg/tG37H2CSqYCw51VMmBtPfWrH8LKPLAVCOp0h94e1n8PfR3v9iRbtyA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="],
 | 
			
		||||
    "@eslint/js": ["@eslint/js@9.37.0", "", {}, "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
 | 
			
		||||
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
 | 
			
		||||
    "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
 | 
			
		||||
    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
 | 
			
		||||
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.4.2", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@vue/compiler-sfc": "2.7.x || 3.x", "prettier": "2 || 3 || ^4.0.0-0" }, "optionalPeers": ["@vue/compiler-sfc"] }, "sha512-KkVFy3TLh0OFzimbZglMmORi+vL/i2OFhEs5M07R9w0IwWAGpsNNyE4CY/2u0YoMF5bawKC2+8/fUH60nnNtjw=="],
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.7.0", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@prettier/plugin-oxc": "^0.0.4", "@vue/compiler-sfc": "2.7.x || 3.x", "content-tag": "^4.0.0", "prettier": "2 || 3 || ^4.0.0-0", "prettier-plugin-ember-template-tag": "^2.1.0" }, "optionalPeers": ["@prettier/plugin-oxc", "@vue/compiler-sfc", "content-tag", "prettier-plugin-ember-template-tag"] }, "sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA=="],
 | 
			
		||||
 | 
			
		||||
    "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
 | 
			
		||||
    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
 | 
			
		||||
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
 | 
			
		||||
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
 | 
			
		||||
    "@kitajs/html": ["@kitajs/html@4.2.10", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-q9n2Ig7GlAYOdL+CeWxsIIZFIKna+eCJah15eK8PBIFHW3UcWayAMs8QYGJNYgP3uMucDimIAUBH26xnE7GILw=="],
 | 
			
		||||
 | 
			
		||||
    "@kitajs/html": ["@kitajs/html@4.2.9", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-FDHHf5Mi5nR0D+Btq86IV1O9XfsePVCiC5rwU4PXjw2aHja16FmIiwLZBO0CS16rJxKkibjMldyRLAW2ni2mzA=="],
 | 
			
		||||
    "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.3", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-NlYrID5yMxfRKiO1eiiSC4MWveKe0ffoCJOZm4idNOqwimmLXr0g1NmvCcquOU2XLRrgzynxZqw6rhwR5CY5Nw=="],
 | 
			
		||||
 | 
			
		||||
    "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.1", "", { "dependencies": { "chalk": "^4.1.2", "tslib": "^2.8.1", "yargs": "^17.7.2" }, "peerDependencies": { "@kitajs/html": "^4.2.5", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-wmjyV8hmJmDOnUM/ZyPkc0UBYgUYmf32/93rkW8wr8h+HiHVMU0tEKFnmRdBjTcy9jwoC9Bnt2NuzS9l67lq5g=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.9", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg=="],
 | 
			
		||||
    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.5", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 | 
			
		||||
 | 
			
		||||
@@ -125,31 +123,43 @@
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@9.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MVyRgP2gzJJtAowjG/cHN3VQXwNLWnY+FpOEsyvDepJki1SdAX/8XDijM1yN6ESD1kr9uhBKjGelC6h3qtT+rA=="],
 | 
			
		||||
    "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.8.4", "", { "os": "android", "cpu": "arm" }, "sha512-6BjMji0TcvQfJ4EoSunOSyu/SiyHKficBD0V3Y0NxF0beaNnnZ7GYEi2lHmRNnRCuIPK8IuVqQ6XizYau+CkKw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@9.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7kV0EOFEZ3sk5Hjy4+bfA6XOQpCwbDiDkkHN4BHHyrBHsXxUR05EcEJPPL1WjItefg+9+8hrBmoK0xRoDs41+A=="],
 | 
			
		||||
    "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.8.4", "", { "os": "android", "cpu": "arm64" }, "sha512-SxF4X6rzCBS9XNPXKZGoIHIABjfGmtQpEgRBDzpDHx5VTuLAUmwLTHXnVBAZoX5bmnhF79RiMElavzFdJ2cA1A=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@9.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6OvkEtRXrt8sJ4aVfxHRikjain9nV1clIsWtJ1J3J8NG1ZhjyJFgT00SCvqxbK+pzeWJq6XzHyTCN78ML+lY2w=="],
 | 
			
		||||
    "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.8.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8zWeERrzgscAniE6kh1TQ4E7GJyglYsvdoKrHYLBCbHWD+0/soffiwAYxZuckKEQSc2RXMSPjcu+JTCALaY0Dw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-aYpNL6o5IRAUIdoweW21TyLt54Hy/ZS9tvzNzF6ya1ckOQ8DLaGVPjGpmzxdNja9j/bbV6aIzBH7lNcBtiOTkQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.8.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-BUwggKz8Hi5uEQ0AeVTSun1+sp4lzNcItn+L7fDsHu5Cx0Zueuo10BtVm+dIwmYVVPL5oGYOeD0fS7MKAazKiw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGFW4vCfKMFEIzb9VCY0oWyyY9tR1/o+wDdNePhiUXZU4SVniRPQaZ1SJ0sUFI1k25pXZmzQmIP6cBmazi/Dew=="],
 | 
			
		||||
    "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.8.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fPO5TQhnn8gA6yP4o49lc4Gn8KeDwAp9uYd4PlE3Q00JVqU6cY9WecDhYHrWtiFcyoZ8UVBlIxuhRqT/DP4Z4A=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lxx/PibBfzqYvut2Y8N2D0Ritg9H8pKO+7NUSJb9YjR/bfk2KRmP8iaUz3zB0JhPtf/W3REs65oKpWxgflGToA=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.8.4", "", { "os": "linux", "cpu": "arm" }, "sha512-QuNbdUaVGiP0W0GrXsvCDZjqeL4lZGU7aXlx/S2tCvyTk3wh6skoiLJgqUf/eeqXfUPnzTfntYqyfolzCAyBYA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@9.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-yD28ptS/OuNhwkpXRPNf+/FvrO7lwURLsEbRVcL1kIE0GxNJNMtKgIE4xQvtKDzkhk6ZRpLho5VSrkkF+3ARTQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.8.4", "", { "os": "linux", "cpu": "arm" }, "sha512-p/zLMfza8OsC4BDKxqeZ9Qel+4eA/oiMSyKLRkMrTgt6OWQq1d5nHntjfG35Abcw4ev6Q9lRU3NOW5hj0xlUbw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@9.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-WBwEJdspoga2w+aly6JVZeHnxuPVuztw3fPfWrei2P6rNM5hcKxBGWKKT6zO1fPMCB4sdDkFohGKkMHVV1eryQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bvJF9wWxF1+a5YZATlS5JojpOMC7OsnTatA6sXVHoOb7MIigjledYB5ZMAeRrnWWexRMiEX3YSaA46oSfOzmOg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-a2z3/cbOOTUq0UTBG8f3EO/usFcdwwXnCejfXv42HmV/G8GjrT4fp5+5mVDoMByH3Ce3iVPxj1LmS6OvItKMYQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-gf4nwGBfu+EFwOn5p7/T7VF4jmIdfodwJS9MRkOBHvuAm3LQgCX7O6d3Y80mm0TV7ZMRD/trfW628rHfd5++vQ=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bHZF+WShYQWpuswB9fyxcgMIWVk4sZQT0wnwpnZgQuvGTZLkYJ1JTCXJMtaX5mIFHf69ngvawnwPIUA4Feil0g=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.8.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T120R5GIzRd41rYWWKCI6cSYrZjmRQzf3X4xeE1WX396Uabz5DX8KU7RnVHihSK+KDxccCVOFBxcH3ITd+IEpw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@9.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" }, "cpu": "none" }, "sha512-I5cSgCCh5nFozGSHz+PjIOfrqW99eUszlxKLgoNNzQ1xQ2ou9ZJGzcZ94BHsM9SpyYHLtgHljmOZxCT9bgxYNA=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.8.4", "", { "os": "linux", "cpu": "none" }, "sha512-PVG7SxBFFjAaQ76p9O/0Xt5mTBlziRwpck+6cRNhy/hbWY/hSt8BFfPqw0EDSfnl40Uuh+NPsHFMnaWWyxbQEg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@9.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5IhoOpPr38YWDWRCA5kP30xlUxbIJyLAEsAK7EMyUgqygBHEYLkElaKGgS0X5jRXUQ6l5yNxuW73caogb2FYaw=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.8.4", "", { "os": "linux", "cpu": "none" }, "sha512-L0OklUhM2qLGaKvPSyKmwWpoijfc++VJtPyVgz031ShOXyo0WjD0ZGzusyJMsA1a/gdulAmN6CQ/0Sf4LGXEcw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@9.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Qc40GDkaad9rZksSQr2l/V9UubigIHsW69g94Gswc2sKYB3XfJXfIfyV8WTJ67u6ZMXsZ7BH1msSC6Aen75mCg=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.8.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-18Ajz5hqO4cRGuoHzLFUsIPod9GIaIRDiXFg2m6CS3NgVdHx7iCZscplYH7KtjdE42M8nGWYMyyq5BOk7QVgPw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-uHvH4RyYBdQ/lFGV9H+R1ScHg6EBnAhE3mnX+u+mO/btnalvg7j80okuHf8Qw0tLQiP5P1sEBoVeE6zviXY9IA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-X5z44qh5DdJfVhcqXAQFTDFUpcxdpf6DT/lHL5CFcdQGIZxatjc7gFUy05IXPI9xwfq39RValjJBvFovUk9XBw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.8.4", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.5" }, "cpu": "none" }, "sha512-z3906y+cd8RRhBGNwHRrRAFxnKjXsBeL3+rdQjZpBrUyrhhsaV5iKD/ROx64FNJ9GjL/9mfon8A5xx/McYIqHA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.8.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-70vXFs74uA3X5iYOkpclbkWlQEF+MI325uAQ+Or2n8HJip2T0SEmuBlyw/sRL2E8zLC4oocb+1g25fmzlDVkmg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.8.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-SEOUAzTvr+nyMia3nx1dMtD7YUxZwuhQ3QAPnxy21261Lj0yT3JY4EIfwWH54lAWWfMdRSRRMFuGeF/dq7XjEw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.8.4", "", { "os": "win32", "cpu": "x64" }, "sha512-1gARIQsOPOU7LJ7jvMyPmZEVMapL/PymeG3J7naOdLZDrIZKX6CTvgawJmETYKt+8icP8M6KbBinrVkKVqFd+A=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
 | 
			
		||||
 | 
			
		||||
@@ -179,107 +189,103 @@
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
 | 
			
		||||
 | 
			
		||||
    "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="],
 | 
			
		||||
    "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
 | 
			
		||||
 | 
			
		||||
    "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="],
 | 
			
		||||
    "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/cli": ["@tailwindcss/cli@4.1.8", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.8" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-+6lkjXSr/68zWiabK3mVYVHmOq/SAHjJ13mR8spyB4LgUWZbWzU9kCSErlAUo+gK5aVfgqe8kY6Ltz9+nz5XYA=="],
 | 
			
		||||
    "@tailwindcss/cli": ["@tailwindcss/cli@4.1.14", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.14" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-2cErQRcsI8jIObUMVwcd1H2AWgGxwzozHJk7AKM2KB1taOp7L15xQ8kEsZrvVbOjNrb8yXtnSvNtJ+mhCB7EBg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="],
 | 
			
		||||
    "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="],
 | 
			
		||||
    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="],
 | 
			
		||||
    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="],
 | 
			
		||||
    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="],
 | 
			
		||||
    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="],
 | 
			
		||||
    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="],
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="],
 | 
			
		||||
    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.8", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "postcss": "^8.4.41", "tailwindcss": "4.1.8" } }, "sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw=="],
 | 
			
		||||
 | 
			
		||||
    "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
 | 
			
		||||
 | 
			
		||||
    "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
 | 
			
		||||
    "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.14", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "postcss": "^8.4.41", "tailwindcss": "4.1.14" } }, "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg=="],
 | 
			
		||||
 | 
			
		||||
    "@total-typescript/ts-reset": ["@total-typescript/ts-reset@0.6.1", "", {}, "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg=="],
 | 
			
		||||
 | 
			
		||||
    "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
    "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
 | 
			
		||||
 | 
			
		||||
    "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
 | 
			
		||||
    "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
 | 
			
		||||
 | 
			
		||||
    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="],
 | 
			
		||||
    "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
 | 
			
		||||
 | 
			
		||||
    "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/type-utils": "8.33.1", "@typescript-eslint/utils": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A=="],
 | 
			
		||||
    "@types/react": ["@types/react@19.1.15", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA=="],
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.1", "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw=="],
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1" } }, "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA=="],
 | 
			
		||||
    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.45.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.45.0", "@typescript-eslint/types": "^8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g=="],
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0" } }, "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww=="],
 | 
			
		||||
    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.45.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@8.33.1", "", {}, "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg=="],
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.1", "@typescript-eslint/tsconfig-utils": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA=="],
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@8.45.0", "", {}, "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ=="],
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.45.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.45.0", "@typescript-eslint/tsconfig-utils": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="],
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.45.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg=="],
 | 
			
		||||
 | 
			
		||||
    "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="],
 | 
			
		||||
 | 
			
		||||
    "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
 | 
			
		||||
 | 
			
		||||
    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 | 
			
		||||
 | 
			
		||||
    "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 | 
			
		||||
    "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
 | 
			
		||||
    "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
 | 
			
		||||
 | 
			
		||||
    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 | 
			
		||||
 | 
			
		||||
    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 | 
			
		||||
 | 
			
		||||
    "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
 | 
			
		||||
    "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
 | 
			
		||||
 | 
			
		||||
    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 | 
			
		||||
 | 
			
		||||
    "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
 | 
			
		||||
    "bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
 | 
			
		||||
 | 
			
		||||
    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
 | 
			
		||||
 | 
			
		||||
    "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
    "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
 | 
			
		||||
 | 
			
		||||
    "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
 | 
			
		||||
 | 
			
		||||
    "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
 | 
			
		||||
    "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
 | 
			
		||||
 | 
			
		||||
    "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
 | 
			
		||||
 | 
			
		||||
@@ -297,33 +303,33 @@
 | 
			
		||||
 | 
			
		||||
    "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
 | 
			
		||||
 | 
			
		||||
    "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
 | 
			
		||||
    "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
 | 
			
		||||
 | 
			
		||||
    "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
 | 
			
		||||
 | 
			
		||||
    "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
 | 
			
		||||
 | 
			
		||||
    "elysia": ["elysia@1.3.4", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A=="],
 | 
			
		||||
    "elysia": ["elysia@1.4.9", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["file-type", "typescript"] }, "sha512-BWNhA8DoKQvlQTjAUkMAmNeso24U+ibZxY/8LN96qSDK/6eevaX59r3GISow699JPxSnFY3gLMUzJzCLYVtbvg=="],
 | 
			
		||||
 | 
			
		||||
    "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
 | 
			
		||||
    "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="],
 | 
			
		||||
 | 
			
		||||
    "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
 | 
			
		||||
    "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
 | 
			
		||||
 | 
			
		||||
    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 | 
			
		||||
 | 
			
		||||
    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="],
 | 
			
		||||
    "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.1.0", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "postcss": "^8.5.4", "postcss-import": "^16.1.0", "synckit": "0.9.2" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.0.0" } }, "sha512-qaOnCBBvkxq5O1CPwzD8NWTNbBLY5RtjfpbOXiv0MtjX5GHnraj/cKBvjrfAPGH7FWrDeycR+kQ52aYqNWpujw=="],
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.7.9", "", { "dependencies": { "@eslint/css-tree": "^3.6.5", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "synckit": "^0.11.11", "tailwind-csstree": "^0.1.4", "tsconfig-paths-webpack-plugin": "^4.2.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.1.6" } }, "sha512-xmd3YqRoc57ngplFBZLn13bLpKsq6fe+ipdObilG46llJi0MvHSx8+uQ1VNBE1/ieIcedmVY7quol4WLntM8iw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@12.1.1", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
 | 
			
		||||
    "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
 | 
			
		||||
    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
 | 
			
		||||
 | 
			
		||||
    "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
 | 
			
		||||
    "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
 | 
			
		||||
 | 
			
		||||
    "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
 | 
			
		||||
 | 
			
		||||
@@ -333,7 +339,7 @@
 | 
			
		||||
 | 
			
		||||
    "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
 | 
			
		||||
 | 
			
		||||
    "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
 | 
			
		||||
    "exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
 | 
			
		||||
 | 
			
		||||
    "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
 | 
			
		||||
 | 
			
		||||
@@ -345,33 +351,31 @@
 | 
			
		||||
 | 
			
		||||
    "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
 | 
			
		||||
 | 
			
		||||
    "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
 | 
			
		||||
    "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
 | 
			
		||||
 | 
			
		||||
    "fd-package-json": ["fd-package-json@1.2.0", "", { "dependencies": { "walk-up-path": "^3.0.1" } }, "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA=="],
 | 
			
		||||
 | 
			
		||||
    "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
 | 
			
		||||
    "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="],
 | 
			
		||||
 | 
			
		||||
    "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
 | 
			
		||||
 | 
			
		||||
    "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
 | 
			
		||||
 | 
			
		||||
    "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
 | 
			
		||||
 | 
			
		||||
    "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
 | 
			
		||||
 | 
			
		||||
    "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
 | 
			
		||||
 | 
			
		||||
    "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
 | 
			
		||||
    "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
 | 
			
		||||
 | 
			
		||||
    "formatly": ["formatly@0.2.3", "", { "dependencies": { "fd-package-json": "^1.2.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA=="],
 | 
			
		||||
    "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="],
 | 
			
		||||
 | 
			
		||||
    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 | 
			
		||||
 | 
			
		||||
    "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
 | 
			
		||||
 | 
			
		||||
    "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
 | 
			
		||||
 | 
			
		||||
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 | 
			
		||||
 | 
			
		||||
    "globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="],
 | 
			
		||||
    "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
 | 
			
		||||
 | 
			
		||||
    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -381,11 +385,9 @@
 | 
			
		||||
 | 
			
		||||
    "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
 | 
			
		||||
 | 
			
		||||
    "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
 | 
			
		||||
 | 
			
		||||
    "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 | 
			
		||||
 | 
			
		||||
    "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
 | 
			
		||||
    "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
 | 
			
		||||
 | 
			
		||||
    "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
 | 
			
		||||
 | 
			
		||||
@@ -393,17 +395,15 @@
 | 
			
		||||
 | 
			
		||||
    "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
 | 
			
		||||
 | 
			
		||||
    "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
 | 
			
		||||
 | 
			
		||||
    "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
 | 
			
		||||
 | 
			
		||||
    "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
 | 
			
		||||
 | 
			
		||||
    "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
 | 
			
		||||
 | 
			
		||||
    "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
 | 
			
		||||
    "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
 | 
			
		||||
 | 
			
		||||
    "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
 | 
			
		||||
    "jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
 | 
			
		||||
 | 
			
		||||
    "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -419,9 +419,11 @@
 | 
			
		||||
 | 
			
		||||
    "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
 | 
			
		||||
 | 
			
		||||
    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
 | 
			
		||||
 | 
			
		||||
    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
 | 
			
		||||
 | 
			
		||||
    "knip": ["knip@5.59.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^9.0.2", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-pOMBw6sLQhi/RfnpI6TwBY6NrAtKXDO5wkmMm+pCsSK5eWbVfDnDtPXbLDGNCoZPXiuAojb27y4XOpp4JPNxlA=="],
 | 
			
		||||
    "knip": ["knip@5.64.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^11.8.3", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.4.1", "strip-json-comments": "5.0.2", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-80XnLsyeXuyxj1F4+NBtQFHxaRH0xWRw8EKwfQ6EkVZZ0bSz/kqqan08k/Qg8ajWsFPhFq+0S2RbLCBGIQtuOg=="],
 | 
			
		||||
 | 
			
		||||
    "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -451,7 +453,9 @@
 | 
			
		||||
 | 
			
		||||
    "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
 | 
			
		||||
 | 
			
		||||
    "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
 | 
			
		||||
    "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
 | 
			
		||||
 | 
			
		||||
    "mdn-data": ["mdn-data@2.23.0", "", {}, "sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ=="],
 | 
			
		||||
 | 
			
		||||
    "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
 | 
			
		||||
 | 
			
		||||
@@ -465,9 +469,7 @@
 | 
			
		||||
 | 
			
		||||
    "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
 | 
			
		||||
 | 
			
		||||
    "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
 | 
			
		||||
 | 
			
		||||
    "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
 | 
			
		||||
    "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
 | 
			
		||||
 | 
			
		||||
    "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
 | 
			
		||||
 | 
			
		||||
@@ -475,6 +477,8 @@
 | 
			
		||||
 | 
			
		||||
    "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
 | 
			
		||||
 | 
			
		||||
    "napi-postinstall": ["napi-postinstall@0.3.3", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow=="],
 | 
			
		||||
 | 
			
		||||
    "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
 | 
			
		||||
 | 
			
		||||
    "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
 | 
			
		||||
@@ -489,7 +493,7 @@
 | 
			
		||||
 | 
			
		||||
    "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
 | 
			
		||||
 | 
			
		||||
    "oxc-resolver": ["oxc-resolver@9.0.2", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "9.0.2", "@oxc-resolver/binding-darwin-x64": "9.0.2", "@oxc-resolver/binding-freebsd-x64": "9.0.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "9.0.2", "@oxc-resolver/binding-linux-arm64-gnu": "9.0.2", "@oxc-resolver/binding-linux-arm64-musl": "9.0.2", "@oxc-resolver/binding-linux-riscv64-gnu": "9.0.2", "@oxc-resolver/binding-linux-s390x-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-musl": "9.0.2", "@oxc-resolver/binding-wasm32-wasi": "9.0.2", "@oxc-resolver/binding-win32-arm64-msvc": "9.0.2", "@oxc-resolver/binding-win32-x64-msvc": "9.0.2" } }, "sha512-w838ygc1p7rF+7+h5vR9A+Y9Fc4imy6C3xPthCMkdFUgFvUWkmABeNB8RBDQ6+afk44Q60/UMMQ+gfDUW99fBA=="],
 | 
			
		||||
    "oxc-resolver": ["oxc-resolver@11.8.4", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.8.4", "@oxc-resolver/binding-android-arm64": "11.8.4", "@oxc-resolver/binding-darwin-arm64": "11.8.4", "@oxc-resolver/binding-darwin-x64": "11.8.4", "@oxc-resolver/binding-freebsd-x64": "11.8.4", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.8.4", "@oxc-resolver/binding-linux-arm-musleabihf": "11.8.4", "@oxc-resolver/binding-linux-arm64-gnu": "11.8.4", "@oxc-resolver/binding-linux-arm64-musl": "11.8.4", "@oxc-resolver/binding-linux-ppc64-gnu": "11.8.4", "@oxc-resolver/binding-linux-riscv64-gnu": "11.8.4", "@oxc-resolver/binding-linux-riscv64-musl": "11.8.4", "@oxc-resolver/binding-linux-s390x-gnu": "11.8.4", "@oxc-resolver/binding-linux-x64-gnu": "11.8.4", "@oxc-resolver/binding-linux-x64-musl": "11.8.4", "@oxc-resolver/binding-wasm32-wasi": "11.8.4", "@oxc-resolver/binding-win32-arm64-msvc": "11.8.4", "@oxc-resolver/binding-win32-ia32-msvc": "11.8.4", "@oxc-resolver/binding-win32-x64-msvc": "11.8.4" } }, "sha512-qpimS3tHHEf+kgESMAme+q+rj7aCzMya00u9YdKOKyX2o7q4lozjPo6d7ZTTi979KHEcVOPWdNTueAKdeNq72w=="],
 | 
			
		||||
 | 
			
		||||
    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -503,25 +507,23 @@
 | 
			
		||||
 | 
			
		||||
    "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
 | 
			
		||||
 | 
			
		||||
    "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
 | 
			
		||||
 | 
			
		||||
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
 | 
			
		||||
 | 
			
		||||
    "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
 | 
			
		||||
    "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
 | 
			
		||||
 | 
			
		||||
    "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
 | 
			
		||||
 | 
			
		||||
    "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
 | 
			
		||||
 | 
			
		||||
    "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="],
 | 
			
		||||
    "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-import": ["postcss-import@16.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg=="],
 | 
			
		||||
    "postcss-import": ["postcss-import@16.1.1", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
 | 
			
		||||
 | 
			
		||||
    "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
 | 
			
		||||
 | 
			
		||||
    "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
 | 
			
		||||
    "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
 | 
			
		||||
 | 
			
		||||
    "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
 | 
			
		||||
 | 
			
		||||
@@ -529,105 +531,101 @@
 | 
			
		||||
 | 
			
		||||
    "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
 | 
			
		||||
 | 
			
		||||
    "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
 | 
			
		||||
    "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
 | 
			
		||||
 | 
			
		||||
    "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
 | 
			
		||||
 | 
			
		||||
    "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
 | 
			
		||||
 | 
			
		||||
    "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
 | 
			
		||||
 | 
			
		||||
    "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
 | 
			
		||||
 | 
			
		||||
    "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
 | 
			
		||||
 | 
			
		||||
    "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
 | 
			
		||||
    "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
 | 
			
		||||
 | 
			
		||||
    "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
 | 
			
		||||
 | 
			
		||||
    "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="],
 | 
			
		||||
 | 
			
		||||
    "semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="],
 | 
			
		||||
    "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
 | 
			
		||||
 | 
			
		||||
    "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
 | 
			
		||||
    "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
 | 
			
		||||
 | 
			
		||||
    "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="],
 | 
			
		||||
    "smol-toml": ["smol-toml@1.4.2", "", {}, "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g=="],
 | 
			
		||||
 | 
			
		||||
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
			
		||||
 | 
			
		||||
    "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 | 
			
		||||
    "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
 | 
			
		||||
 | 
			
		||||
    "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 | 
			
		||||
    "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
 | 
			
		||||
 | 
			
		||||
    "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="],
 | 
			
		||||
    "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
 | 
			
		||||
 | 
			
		||||
    "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
 | 
			
		||||
    "strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="],
 | 
			
		||||
 | 
			
		||||
    "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 | 
			
		||||
 | 
			
		||||
    "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
 | 
			
		||||
 | 
			
		||||
    "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="],
 | 
			
		||||
    "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
 | 
			
		||||
 | 
			
		||||
    "tailwind-csstree": ["tailwind-csstree@0.1.4", "", {}, "sha512-FzD187HuFIZEyeR7Xy6sJbJll2d4SybS90satC8SKIuaNRC05CxMvdzN7BUsfDQffcnabckRM5OIcfArjsZ0mg=="],
 | 
			
		||||
 | 
			
		||||
    "tailwind-scrollbar": ["tailwind-scrollbar@4.0.2", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA=="],
 | 
			
		||||
 | 
			
		||||
    "tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="],
 | 
			
		||||
    "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
 | 
			
		||||
 | 
			
		||||
    "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
 | 
			
		||||
    "tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
 | 
			
		||||
 | 
			
		||||
    "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
 | 
			
		||||
    "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="],
 | 
			
		||||
 | 
			
		||||
    "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
 | 
			
		||||
 | 
			
		||||
    "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
 | 
			
		||||
 | 
			
		||||
    "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
 | 
			
		||||
 | 
			
		||||
    "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="],
 | 
			
		||||
 | 
			
		||||
    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
 | 
			
		||||
 | 
			
		||||
    "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
 | 
			
		||||
    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
 | 
			
		||||
 | 
			
		||||
    "typescript-eslint": ["typescript-eslint@8.33.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.1", "@typescript-eslint/parser": "8.33.1", "@typescript-eslint/utils": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A=="],
 | 
			
		||||
    "typescript-eslint": ["typescript-eslint@8.45.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/parser": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg=="],
 | 
			
		||||
 | 
			
		||||
    "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
 | 
			
		||||
 | 
			
		||||
    "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
 | 
			
		||||
    "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
 | 
			
		||||
 | 
			
		||||
    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
 | 
			
		||||
 | 
			
		||||
    "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
 | 
			
		||||
 | 
			
		||||
    "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="],
 | 
			
		||||
    "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
 | 
			
		||||
 | 
			
		||||
    "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
 | 
			
		||||
 | 
			
		||||
    "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
 | 
			
		||||
 | 
			
		||||
    "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
 | 
			
		||||
    "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
 | 
			
		||||
 | 
			
		||||
    "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
 | 
			
		||||
 | 
			
		||||
    "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
 | 
			
		||||
 | 
			
		||||
    "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
 | 
			
		||||
    "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
 | 
			
		||||
 | 
			
		||||
    "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
 | 
			
		||||
    "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
 | 
			
		||||
 | 
			
		||||
    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
 | 
			
		||||
 | 
			
		||||
    "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
 | 
			
		||||
 | 
			
		||||
    "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
 | 
			
		||||
    "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 | 
			
		||||
 | 
			
		||||
@@ -635,40 +633,86 @@
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
    "@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
    "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.5", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
 | 
			
		||||
    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
 | 
			
		||||
 | 
			
		||||
    "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
 | 
			
		||||
    "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
 | 
			
		||||
    "lightningcss/detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 | 
			
		||||
 | 
			
		||||
    "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
    "tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,5 +15,6 @@ services:
 | 
			
		||||
      # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
 | 
			
		||||
      # - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
 | 
			
		||||
      - TZ=Europe/Stockholm # set your timezone, defaults to UTC
 | 
			
		||||
      # - UNAUTHENTICATED_USER_SHARING=true # for use with ALLOW_UNAUTHENTICATED=true to share history with all unauthenticated users / devices
 | 
			
		||||
    ports:
 | 
			
		||||
      - 3000:3000
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,19 @@
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import eslintParserTypeScript from "@typescript-eslint/parser";
 | 
			
		||||
import type { Linter } from "eslint";
 | 
			
		||||
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
 | 
			
		||||
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import tseslint from "typescript-eslint";
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  js.configs.recommended,
 | 
			
		||||
  ...tseslint.configs.recommended,
 | 
			
		||||
  // ...tailwind.configs["flat/recommended"],
 | 
			
		||||
  tseslint.configs.recommended,
 | 
			
		||||
  {
 | 
			
		||||
    plugins: {
 | 
			
		||||
      "simple-import-sort": simpleImportSortPlugin,
 | 
			
		||||
      "better-tailwindcss": eslintPluginBetterTailwindcss,
 | 
			
		||||
    },
 | 
			
		||||
    ignores: ["**/node_modules/**"],
 | 
			
		||||
    ignores: ["**/node_modules/**", "eslint.config.ts"],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      parser: eslintParserTypeScript,
 | 
			
		||||
      parserOptions: {
 | 
			
		||||
@@ -26,10 +24,9 @@ export default [
 | 
			
		||||
      },
 | 
			
		||||
      globals: {
 | 
			
		||||
        ...globals.node,
 | 
			
		||||
        ...globals.browser,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"],
 | 
			
		||||
    files: ["**/*.{tsx,ts}"],
 | 
			
		||||
    settings: {
 | 
			
		||||
      "better-tailwindcss": {
 | 
			
		||||
        entryPoint: "src/main.css",
 | 
			
		||||
@@ -39,7 +36,7 @@ export default [
 | 
			
		||||
      ...(eslintPluginBetterTailwindcss.configs["recommended-warn"] ?? {}).rules,
 | 
			
		||||
      ...(eslintPluginBetterTailwindcss.configs["stylistic-warn"] ?? {}).rules,
 | 
			
		||||
      // "tailwindcss/classnames-order": "off",
 | 
			
		||||
      "better-tailwindcss/multiline": [
 | 
			
		||||
      "better-tailwindcss/enforce-consistent-line-wrapping": [
 | 
			
		||||
        "warn",
 | 
			
		||||
        {
 | 
			
		||||
          group: "newLine",
 | 
			
		||||
@@ -63,4 +60,13 @@ export default [
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
] as Linter.Config[];
 | 
			
		||||
  {
 | 
			
		||||
    files: ["**/*.{js,cjs,mjs,jsx}"],
 | 
			
		||||
    extends: [tseslint.configs.disableTypeChecked],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      globals: {
 | 
			
		||||
        ...globals.browser,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://unpkg.com/knip@5/schema.json",
 | 
			
		||||
  "entry": ["src/index.tsx"],
 | 
			
		||||
  "project": ["src/**/*.ts", "src/**/*.tsx", "src/main.css"],
 | 
			
		||||
  "entry": ["tests/**/*.test.ts"],
 | 
			
		||||
  "project": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"],
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "entry": ["src/main.css"]
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -4,23 +4,25 @@
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "bun run --watch src/index.tsx",
 | 
			
		||||
    "hot": "bun run --hot src/index.tsx",
 | 
			
		||||
    "format": "run-p 'format:*'",
 | 
			
		||||
    "format": "npm-run-all 'format:*'",
 | 
			
		||||
    "format:eslint": "eslint --fix .",
 | 
			
		||||
    "format:prettier": "prettier --write .",
 | 
			
		||||
    "build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css",
 | 
			
		||||
    "lint": "run-p 'lint:*'",
 | 
			
		||||
    "build:js": "tsc",
 | 
			
		||||
    "build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css && bun run build:js",
 | 
			
		||||
    "lint": "npm-run-all 'lint:*'",
 | 
			
		||||
    "lint:tsc": "tsc --noEmit",
 | 
			
		||||
    "lint:knip": "knip",
 | 
			
		||||
    "lint:eslint": "eslint .",
 | 
			
		||||
    "lint:prettier": "prettier --check ."
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@elysiajs/html": "^1.3.0",
 | 
			
		||||
    "@elysiajs/jwt": "^1.3.1",
 | 
			
		||||
    "@elysiajs/static": "^1.3.0",
 | 
			
		||||
    "@kitajs/html": "^4.2.9",
 | 
			
		||||
    "elysia": "^1.3.4",
 | 
			
		||||
    "sanitize-filename": "^1.6.3"
 | 
			
		||||
    "@elysiajs/html": "^1.4.0",
 | 
			
		||||
    "@elysiajs/jwt": "^1.4.0",
 | 
			
		||||
    "@elysiajs/static": "^1.4.0",
 | 
			
		||||
    "@kitajs/html": "^4.2.10",
 | 
			
		||||
    "elysia": "^1.4.9",
 | 
			
		||||
    "sanitize-filename": "^1.6.3",
 | 
			
		||||
    "tar": "^7.5.1"
 | 
			
		||||
  },
 | 
			
		||||
  "module": "src/index.tsx",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
@@ -28,30 +30,31 @@
 | 
			
		||||
    "start": "bun run src/index.tsx"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint/js": "^9.28.0",
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": "^4.4.2",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.1.1",
 | 
			
		||||
    "@tailwindcss/cli": "^4.1.8",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.8",
 | 
			
		||||
    "@eslint/js": "^9.37.0",
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.1.3",
 | 
			
		||||
    "@tailwindcss/cli": "^4.1.14",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.14",
 | 
			
		||||
    "@total-typescript/ts-reset": "^0.6.1",
 | 
			
		||||
    "@types/bun": "^1.2.15",
 | 
			
		||||
    "@types/node": "^22.15.29",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.33.1",
 | 
			
		||||
    "eslint": "^9.28.0",
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": "^3.1.0",
 | 
			
		||||
    "@types/bun": "latest",
 | 
			
		||||
    "@types/node": "^24.6.2",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.45.0",
 | 
			
		||||
    "eslint": "^9.37.0",
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": "^3.7.9",
 | 
			
		||||
    "eslint-plugin-simple-import-sort": "^12.1.1",
 | 
			
		||||
    "globals": "^16.2.0",
 | 
			
		||||
    "knip": "^5.59.1",
 | 
			
		||||
    "globals": "^16.4.0",
 | 
			
		||||
    "knip": "^5.64.1",
 | 
			
		||||
    "npm-run-all2": "^8.0.4",
 | 
			
		||||
    "postcss": "^8.5.4",
 | 
			
		||||
    "prettier": "^3.5.3",
 | 
			
		||||
    "postcss": "^8.5.6",
 | 
			
		||||
    "prettier": "^3.6.2",
 | 
			
		||||
    "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
    "tailwindcss": "^4.1.8",
 | 
			
		||||
    "typescript": "^5.8.3",
 | 
			
		||||
    "typescript-eslint": "^8.33.1"
 | 
			
		||||
    "tailwindcss": "^4.1.14",
 | 
			
		||||
    "typescript": "^5.9.3",
 | 
			
		||||
    "typescript-eslint": "^8.45.0"
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "@parcel/watcher",
 | 
			
		||||
    "@tailwindcss/oxide"
 | 
			
		||||
    "@tailwindcss/oxide",
 | 
			
		||||
    "oxc-resolver"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 7.7 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB  | 
| 
		 Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.6 KiB  | 
| 
		 Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 405 B  | 
| 
		 Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 831 B  | 
| 
		 Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										1
									
								
								public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="512" height="512" style="cursor:default" viewBox="0 0 96.983 132.292"><g transform="translate(-56.568 -82.29)"><path d="M124.878 83.82h-60.91a5.86 5.86 0 0 0-5.87 5.87v117.496a5.855 5.855 0 0 0 5.87 5.866h82.182a5.855 5.855 0 0 0 5.87-5.866v-92.55z" style="display:inline;fill:#111827;stroke:#aeb9d0;stroke-width:3.06006;stroke-dasharray:none;stroke-opacity:1"/><circle cx="84.331" cy="128.904" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="128.904" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="128.904" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="84.331" cy="148.438" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="148.438" r="6.653" style="display:inline;fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="148.438" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="84.331" cy="167.971" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="167.971" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="167.971" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><path d="M119.124 161.326h13.287v13.287h-13.287z" style="fill:#84cc16;fill-opacity:1;stroke:none;stroke-width:3.04496;stroke-opacity:1"/></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
@@ -1,18 +1,4 @@
 | 
			
		||||
const webroot = document.querySelector("meta[name='webroot']").content;
 | 
			
		||||
 | 
			
		||||
window.downloadAll = function () {
 | 
			
		||||
  // Get all download links
 | 
			
		||||
  const downloadLinks = document.querySelectorAll("a[download]");
 | 
			
		||||
 | 
			
		||||
  // Trigger download for each link
 | 
			
		||||
  downloadLinks.forEach((link, index) => {
 | 
			
		||||
    // We add a delay for each download to prevent them from starting at the same time
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      const event = new MouseEvent("click");
 | 
			
		||||
      link.dispatchEvent(event);
 | 
			
		||||
    }, index * 100);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
const jobId = window.location.pathname.split("/").pop();
 | 
			
		||||
const main = document.querySelector("main");
 | 
			
		||||
let progressElem = document.querySelector("progress");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,2 @@
 | 
			
		||||
User-agent: *
 | 
			
		||||
User-agent: *
 | 
			
		||||
Disallow: /
 | 
			
		||||
@@ -4,5 +4,6 @@
 | 
			
		||||
  "lockFileMaintenance": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "automerge": true
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  "ignoreDeps": ["bun-types", "@types/bun"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ export const BaseHtml = ({
 | 
			
		||||
      <link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.png`} />
 | 
			
		||||
      <link rel="manifest" href={`${webroot}/site.webmanifest`} />
 | 
			
		||||
    </head>
 | 
			
		||||
    <body class="flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200">
 | 
			
		||||
    <body class={`flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200`}>
 | 
			
		||||
      {children}
 | 
			
		||||
      <footer class="w-full">
 | 
			
		||||
        <div class="p-4 text-center text-sm text-neutral-500">
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ export const Header = ({
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <header class="w-full p-4">
 | 
			
		||||
      <nav class="mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4">
 | 
			
		||||
      <nav class={`mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4`}>
 | 
			
		||||
        <ul>
 | 
			
		||||
          <li>
 | 
			
		||||
            <strong>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -116,8 +117,8 @@ export async function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -62,28 +63,24 @@ export async function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile(
 | 
			
		||||
      "ebook-convert",
 | 
			
		||||
      [filePath, targetPath],
 | 
			
		||||
      (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(`error: ${error}`);
 | 
			
		||||
        }
 | 
			
		||||
    execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stdout) {
 | 
			
		||||
          console.log(`stdout: ${stdout}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          console.error(`stderr: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        resolve("Done");
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								src/converters/dasel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    document: ["yaml", "toml", "json", "xml", "csv"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    document: ["yaml", "toml", "json", "csv"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const args: string[] = [];
 | 
			
		||||
 | 
			
		||||
  args.push("--file", filePath);
 | 
			
		||||
  args.push("--read", fileType);
 | 
			
		||||
  args.push("--write", convertTo);
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("dasel", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fs.writeFile(targetPath, stdout, (err: NodeJS.ErrnoException | null) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(`Failed to write output: ${err}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve("Done");
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const inputArgs: string[] = [];
 | 
			
		||||
  if (fileType === "eps") {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -691,8 +692,8 @@ export async function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let extraArgs: string[] = [];
 | 
			
		||||
  let message = "Done";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -313,8 +314,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("gm", ["convert", filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -445,8 +446,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let outputArgs: string[] = [];
 | 
			
		||||
  let inputArgs: string[] = [];
 | 
			
		||||
@@ -460,6 +461,13 @@ export function convert(
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle EMF files specifically to avoid LibreOffice delegate issues
 | 
			
		||||
  if (fileType === "emf") {
 | 
			
		||||
    // Use direct conversion without delegates for EMF files
 | 
			
		||||
    inputArgs.push("-define", "emf:delegate=false", "-density", "300");
 | 
			
		||||
    outputArgs.push("-background", "white", "-alpha", "remove");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile(
 | 
			
		||||
      "magick",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -32,8 +33,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -7,7 +8,7 @@ export const properties = {
 | 
			
		||||
    images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    jxl: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
 | 
			
		||||
    jxl: ["apng", "exr", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
 | 
			
		||||
    images: ["jxl"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -17,8 +18,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let tool = "";
 | 
			
		||||
  if (fileType === "jxl") {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										177
									
								
								src/converters/libreoffice.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,177 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    text: [
 | 
			
		||||
      "602",
 | 
			
		||||
      "abw",
 | 
			
		||||
      "csv",
 | 
			
		||||
      "cwk",
 | 
			
		||||
      "doc",
 | 
			
		||||
      "docm",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "dot",
 | 
			
		||||
      "dotx",
 | 
			
		||||
      "dotm",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fb2",
 | 
			
		||||
      "fodt",
 | 
			
		||||
      "htm",
 | 
			
		||||
      "html",
 | 
			
		||||
      "hwp",
 | 
			
		||||
      "mcw",
 | 
			
		||||
      "mw",
 | 
			
		||||
      "mwd",
 | 
			
		||||
      "lwp",
 | 
			
		||||
      "lrf",
 | 
			
		||||
      "odt",
 | 
			
		||||
      "ott",
 | 
			
		||||
      "pages",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "psw",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "sdw",
 | 
			
		||||
      "stw",
 | 
			
		||||
      "sxw",
 | 
			
		||||
      "tab",
 | 
			
		||||
      "tsv",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "wn",
 | 
			
		||||
      "wpd",
 | 
			
		||||
      "wps",
 | 
			
		||||
      "wpt",
 | 
			
		||||
      "wri",
 | 
			
		||||
      "xhtml",
 | 
			
		||||
      "xml",
 | 
			
		||||
      "zabw",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    text: [
 | 
			
		||||
      "csv",
 | 
			
		||||
      "doc",
 | 
			
		||||
      "docm",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "dot",
 | 
			
		||||
      "dotx",
 | 
			
		||||
      "dotm",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fodt",
 | 
			
		||||
      "htm",
 | 
			
		||||
      "html",
 | 
			
		||||
      "odt",
 | 
			
		||||
      "ott",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "tab",
 | 
			
		||||
      "tsv",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "wps",
 | 
			
		||||
      "wpt",
 | 
			
		||||
      "xhtml",
 | 
			
		||||
      "xml",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type FileCategories = "text" | "calc";
 | 
			
		||||
 | 
			
		||||
const filters: Record<FileCategories, Record<string, string>> = {
 | 
			
		||||
  text: {
 | 
			
		||||
    "602": "T602Document",
 | 
			
		||||
    abw: "AbiWord",
 | 
			
		||||
    csv: "Text",
 | 
			
		||||
    doc: "MS Word 97",
 | 
			
		||||
    docm: "MS Word 2007 XML VBA",
 | 
			
		||||
    docx: "MS Word 2007 XML",
 | 
			
		||||
    dot: "MS Word 97 Vorlage",
 | 
			
		||||
    dotx: "MS Word 2007 XML Template",
 | 
			
		||||
    dotm: "MS Word 2007 XML Template",
 | 
			
		||||
    epub: "EPUB",
 | 
			
		||||
    fb2: "Fictionbook 2",
 | 
			
		||||
    fodt: "OpenDocument Text Flat XML",
 | 
			
		||||
    htm: "HTML (StarWriter)",
 | 
			
		||||
    html: "HTML (StarWriter)",
 | 
			
		||||
    hwp: "writer_MIZI_Hwp_97",
 | 
			
		||||
    mcw: "MacWrite",
 | 
			
		||||
    mw: "MacWrite",
 | 
			
		||||
    mwd: "Mariner_Write",
 | 
			
		||||
    lwp: "LotusWordPro",
 | 
			
		||||
    lrf: "BroadBand eBook",
 | 
			
		||||
    odt: "writer8",
 | 
			
		||||
    ott: "writer8_template",
 | 
			
		||||
    pages: "Apple Pages",
 | 
			
		||||
    // pdf: "writer_pdf_import",
 | 
			
		||||
    psw: "PocketWord File",
 | 
			
		||||
    rtf: "Rich Text Format",
 | 
			
		||||
    sdw: "StarOffice_Writer",
 | 
			
		||||
    stw: "writer_StarOffice_XML_Writer_Template",
 | 
			
		||||
    sxw: "StarOffice XML (Writer)",
 | 
			
		||||
    tab: "Text",
 | 
			
		||||
    tsv: "Text",
 | 
			
		||||
    txt: "Text",
 | 
			
		||||
    wn: "WriteNow",
 | 
			
		||||
    wpd: "WordPerfect",
 | 
			
		||||
    wps: "MS Word 97",
 | 
			
		||||
    wpt: "MS Word 97 Vorlage",
 | 
			
		||||
    wri: "MS_Write",
 | 
			
		||||
    xhtml: "HTML (StarWriter)",
 | 
			
		||||
    xml: "OpenDocument Text Flat XML",
 | 
			
		||||
    zabw: "AbiWord",
 | 
			
		||||
  },
 | 
			
		||||
  calc: {},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFilters = (fileType: string, converto: string) => {
 | 
			
		||||
  if (fileType in filters.text && converto in filters.text) {
 | 
			
		||||
    return [filters.text[fileType], filters.text[converto]];
 | 
			
		||||
  } else if (fileType in filters.calc && converto in filters.calc) {
 | 
			
		||||
    return [filters.calc[fileType], filters.calc[converto]];
 | 
			
		||||
  }
 | 
			
		||||
  return [null, null];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "") ?? targetPath;
 | 
			
		||||
 | 
			
		||||
  // Build arguments array
 | 
			
		||||
  const args: string[] = [];
 | 
			
		||||
  args.push("--headless");
 | 
			
		||||
  const [inFilter, outFilter] = getFilters(fileType, convertTo);
 | 
			
		||||
 | 
			
		||||
  if (inFilter) {
 | 
			
		||||
    args.push(`--infilter="${inFilter}"`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (outFilter) {
 | 
			
		||||
    args.push("--convert-to", `${convertTo}:${outFilter}`, "--outdir", outputPath, filePath);
 | 
			
		||||
  } else {
 | 
			
		||||
    args.push("--convert-to", convertTo, "--outdir", outputPath, filePath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("soffice", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { Cookie } from "elysia";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { MAX_CONVERT_PROCESS } from "../helpers/env";
 | 
			
		||||
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
 | 
			
		||||
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
 | 
			
		||||
import { convert as convertDasel, properties as propertiesDasel } from "./dasel";
 | 
			
		||||
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
 | 
			
		||||
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
 | 
			
		||||
import {
 | 
			
		||||
@@ -11,10 +15,13 @@ import { convert as convertImagemagick, properties as propertiesImagemagick } fr
 | 
			
		||||
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
 | 
			
		||||
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
 | 
			
		||||
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
 | 
			
		||||
import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice";
 | 
			
		||||
import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert";
 | 
			
		||||
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
 | 
			
		||||
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
 | 
			
		||||
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
 | 
			
		||||
import { convert as convertImage, properties as propertiesImage } from "./vips";
 | 
			
		||||
import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer";
 | 
			
		||||
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
 | 
			
		||||
 | 
			
		||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
 | 
			
		||||
@@ -47,6 +54,11 @@ const properties: Record<
 | 
			
		||||
    ) => unknown;
 | 
			
		||||
  }
 | 
			
		||||
> = {
 | 
			
		||||
  // Prioritize Inkscape for EMF files as it handles them better than ImageMagick
 | 
			
		||||
  inkscape: {
 | 
			
		||||
    properties: propertiesInkscape,
 | 
			
		||||
    converter: convertInkscape,
 | 
			
		||||
  },
 | 
			
		||||
  libjxl: {
 | 
			
		||||
    properties: propertiesLibjxl,
 | 
			
		||||
    converter: convertLibjxl,
 | 
			
		||||
@@ -71,10 +83,22 @@ const properties: Record<
 | 
			
		||||
    properties: propertiesCalibre,
 | 
			
		||||
    converter: convertCalibre,
 | 
			
		||||
  },
 | 
			
		||||
  dasel: {
 | 
			
		||||
    properties: propertiesDasel,
 | 
			
		||||
    converter: convertDasel,
 | 
			
		||||
  },
 | 
			
		||||
  libreoffice: {
 | 
			
		||||
    properties: propertiesLibreOffice,
 | 
			
		||||
    converter: convertLibreOffice,
 | 
			
		||||
  },
 | 
			
		||||
  pandoc: {
 | 
			
		||||
    properties: propertiesPandoc,
 | 
			
		||||
    converter: convertPandoc,
 | 
			
		||||
  },
 | 
			
		||||
  msgconvert: {
 | 
			
		||||
    properties: propertiesMsgconvert,
 | 
			
		||||
    converter: convertMsgconvert,
 | 
			
		||||
  },
 | 
			
		||||
  dvisvgm: {
 | 
			
		||||
    properties: propertiesDvisvgm,
 | 
			
		||||
    converter: convertDvisvgm,
 | 
			
		||||
@@ -87,10 +111,6 @@ const properties: Record<
 | 
			
		||||
    properties: propertiesGraphicsmagick,
 | 
			
		||||
    converter: convertGraphicsmagick,
 | 
			
		||||
  },
 | 
			
		||||
  inkscape: {
 | 
			
		||||
    properties: propertiesInkscape,
 | 
			
		||||
    converter: convertInkscape,
 | 
			
		||||
  },
 | 
			
		||||
  assimp: {
 | 
			
		||||
    properties: propertiesassimp,
 | 
			
		||||
    converter: convertassimp,
 | 
			
		||||
@@ -103,9 +123,63 @@ const properties: Record<
 | 
			
		||||
    properties: propertiesPotrace,
 | 
			
		||||
    converter: convertPotrace,
 | 
			
		||||
  },
 | 
			
		||||
  vtracer: {
 | 
			
		||||
    properties: propertiesVtracer,
 | 
			
		||||
    converter: convertVtracer,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function mainConverter(
 | 
			
		||||
function chunks<T>(arr: T[], size: number): T[][] {
 | 
			
		||||
  if (size <= 0) {
 | 
			
		||||
    return [arr];
 | 
			
		||||
  }
 | 
			
		||||
  return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) =>
 | 
			
		||||
    arr.slice(i * size, i * size + size),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function handleConvert(
 | 
			
		||||
  fileNames: string[],
 | 
			
		||||
  userUploadsDir: string,
 | 
			
		||||
  userOutputDir: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  converterName: string,
 | 
			
		||||
  jobId: Cookie<string | undefined>,
 | 
			
		||||
) {
 | 
			
		||||
  const query = db.query(
 | 
			
		||||
    "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) {
 | 
			
		||||
    const toProcess: Promise<string>[] = [];
 | 
			
		||||
    for (const fileName of chunk) {
 | 
			
		||||
      const filePath = `${userUploadsDir}${fileName}`;
 | 
			
		||||
      const fileTypeOrig = fileName.split(".").pop() ?? "";
 | 
			
		||||
      const fileType = normalizeFiletype(fileTypeOrig);
 | 
			
		||||
      const newFileExt = normalizeOutputFiletype(convertTo);
 | 
			
		||||
      const newFileName = fileName.replace(
 | 
			
		||||
        new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
 | 
			
		||||
        newFileExt,
 | 
			
		||||
      );
 | 
			
		||||
      const targetPath = `${userOutputDir}${newFileName}`;
 | 
			
		||||
      toProcess.push(
 | 
			
		||||
        new Promise((resolve, reject) => {
 | 
			
		||||
          mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
 | 
			
		||||
            .then((r) => {
 | 
			
		||||
              if (jobId.value) {
 | 
			
		||||
                query.run(jobId.value, fileName, newFileName, r);
 | 
			
		||||
              }
 | 
			
		||||
              resolve(r);
 | 
			
		||||
            })
 | 
			
		||||
            .catch((c) => reject(c));
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(toProcess);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function mainConverter(
 | 
			
		||||
  inputFilePath: string,
 | 
			
		||||
  fileTypeOriginal: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								src/converters/msgconvert.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,52 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    email: ["msg"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    email: ["eml"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    if (fileType === "msg" && convertTo === "eml") {
 | 
			
		||||
      // Convert MSG to EML using msgconvert
 | 
			
		||||
      // msgconvert will output to the same directory as the input file with .eml extension
 | 
			
		||||
      // We need to use --outfile to specify the target path
 | 
			
		||||
      const args = ["--outfile", targetPath, filePath];
 | 
			
		||||
 | 
			
		||||
      execFile("msgconvert", args, (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(new Error(`msgconvert failed: ${error.message}`));
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          // Log sanitized stderr to avoid exposing sensitive paths
 | 
			
		||||
          const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]");
 | 
			
		||||
          console.warn(
 | 
			
		||||
            `msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resolve(targetPath);
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      reject(
 | 
			
		||||
        new Error(
 | 
			
		||||
          `Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -124,8 +125,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  // set xelatex here
 | 
			
		||||
  const xelatex = ["pdf", "latex"];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -26,8 +27,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								src/converters/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { ExecFileOptions } from "child_process";
 | 
			
		||||
 | 
			
		||||
export type ExecFileFn = (
 | 
			
		||||
  cmd: string,
 | 
			
		||||
  args: string[],
 | 
			
		||||
  callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  options?: ExecFileOptions,
 | 
			
		||||
) => void;
 | 
			
		||||
 | 
			
		||||
export type ConvertFnWithExecFile = (
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options: unknown,
 | 
			
		||||
  execFileOverride?: ExecFileFn,
 | 
			
		||||
) => Promise<string>;
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -94,8 +95,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  // if (fileType === "svg") {
 | 
			
		||||
  //   const scale = options.scale || 1;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										80
									
								
								src/converters/vtracer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,80 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["jpg", "jpeg", "png", "bmp", "gif", "tiff", "tif", "webp"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: ["svg"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface VTracerOptions {
 | 
			
		||||
  colormode?: string;
 | 
			
		||||
  hierarchical?: string;
 | 
			
		||||
  mode?: string;
 | 
			
		||||
  filter_speckle?: string | number;
 | 
			
		||||
  color_precision?: string | number;
 | 
			
		||||
  layer_difference?: string | number;
 | 
			
		||||
  corner_threshold?: string | number;
 | 
			
		||||
  length_threshold?: string | number;
 | 
			
		||||
  max_iterations?: string | number;
 | 
			
		||||
  splice_threshold?: string | number;
 | 
			
		||||
  path_precision?: string | number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    // Build vtracer arguments
 | 
			
		||||
    const args = ["--input", filePath, "--output", targetPath];
 | 
			
		||||
 | 
			
		||||
    // Add optional parameter if provided
 | 
			
		||||
    if (options && typeof options === "object") {
 | 
			
		||||
      const opts = options as VTracerOptions;
 | 
			
		||||
      const validOptions: Array<keyof VTracerOptions> = [
 | 
			
		||||
        "colormode",
 | 
			
		||||
        "hierarchical",
 | 
			
		||||
        "mode",
 | 
			
		||||
        "filter_speckle",
 | 
			
		||||
        "color_precision",
 | 
			
		||||
        "layer_difference",
 | 
			
		||||
        "corner_threshold",
 | 
			
		||||
        "length_threshold",
 | 
			
		||||
        "max_iterations",
 | 
			
		||||
        "splice_threshold",
 | 
			
		||||
        "path_precision",
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      for (const option of validOptions) {
 | 
			
		||||
        if (opts[option] !== undefined) {
 | 
			
		||||
          args.push(`--${option}`, String(opts[option]));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execFile("vtracer", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}${stderr ? `\nstderr: ${stderr}` : ""}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.log(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
 | 
			
		||||
 
 | 
			
		||||
@@ -13,3 +13,13 @@ export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
 | 
			
		||||
export const HIDE_HISTORY = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
 | 
			
		||||
 | 
			
		||||
export const WEBROOT = process.env.WEBROOT ?? "";
 | 
			
		||||
 | 
			
		||||
export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en";
 | 
			
		||||
 | 
			
		||||
export const MAX_CONVERT_PROCESS =
 | 
			
		||||
  process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0
 | 
			
		||||
    ? Number(process.env.MAX_CONVERT_PROCESS)
 | 
			
		||||
    : 0;
 | 
			
		||||
 | 
			
		||||
export const UNAUTHENTICATED_USER_SHARING =
 | 
			
		||||
  process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false;
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,16 @@ if (process.env.NODE_ENV === "production") {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("dasel --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("dasel is not installed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("xelatex -version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Tex Live with XeTeX is not installed.");
 | 
			
		||||
@@ -144,6 +154,26 @@ if (process.env.NODE_ENV === "production") {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("soffice --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("libreoffice is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("msgconvert --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("msgconvert (libemail-outlook-message-perl) is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("bun -v", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Bun is not installed. wait what");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										62
									
								
								src/main.css
									
									
									
									
									
								
							
							
						
						@@ -1,21 +1,22 @@
 | 
			
		||||
@import "./theme/theme.css";
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
 | 
			
		||||
@plugin 'tailwind-scrollbar';
 | 
			
		||||
@plugin "tailwind-scrollbar";
 | 
			
		||||
 | 
			
		||||
@theme {
 | 
			
		||||
  --color-contrast: rgba(var(--contrast));
 | 
			
		||||
  --color-neutral-900: rgba(var(--neutral-900));
 | 
			
		||||
  --color-neutral-800: rgba(var(--neutral-800));
 | 
			
		||||
  --color-neutral-700: rgba(var(--neutral-700));
 | 
			
		||||
  --color-neutral-600: rgba(var(--neutral-600));
 | 
			
		||||
  --color-neutral-500: rgba(var(--neutral-500));
 | 
			
		||||
  --color-neutral-400: rgba(var(--neutral-400));
 | 
			
		||||
  --color-neutral-300: rgba(var(--neutral-300));
 | 
			
		||||
  --color-neutral-200: rgba(var(--neutral-200));
 | 
			
		||||
  --color-neutral-100: rgba(var(--neutral-100));
 | 
			
		||||
  --color-accent-600: rgba(var(--accent-600));
 | 
			
		||||
  --color-accent-500: rgba(var(--accent-500));
 | 
			
		||||
  --color-accent-400: rgba(var(--accent-400));
 | 
			
		||||
  --color-contrast: var(--contrast);
 | 
			
		||||
  --color-neutral-900: var(--neutral-900);
 | 
			
		||||
  --color-neutral-800: var(--neutral-800);
 | 
			
		||||
  --color-neutral-700: var(--neutral-700);
 | 
			
		||||
  --color-neutral-600: var(--neutral-600);
 | 
			
		||||
  --color-neutral-500: var(--neutral-500);
 | 
			
		||||
  --color-neutral-400: var(--neutral-400);
 | 
			
		||||
  --color-neutral-300: var(--neutral-300);
 | 
			
		||||
  --color-neutral-200: var(--neutral-200);
 | 
			
		||||
  --color-neutral-100: var(--neutral-100);
 | 
			
		||||
  --color-accent-600: var(--accent-600);
 | 
			
		||||
  --color-accent-500: var(--accent-500);
 | 
			
		||||
  --color-accent-400: var(--accent-400);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@utility article {
 | 
			
		||||
@@ -29,36 +30,3 @@
 | 
			
		||||
@utility btn-secondary {
 | 
			
		||||
  @apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  --contrast: 255, 255, 255;
 | 
			
		||||
  --neutral-900: 243, 244, 246;
 | 
			
		||||
  --neutral-800: 229, 231, 235;
 | 
			
		||||
  --neutral-700: 209, 213, 219;
 | 
			
		||||
  --neutral-600: 156, 163, 175;
 | 
			
		||||
  --neutral-500: 180, 180, 180;
 | 
			
		||||
  --neutral-400: 75, 85, 99;
 | 
			
		||||
  --neutral-300: 55, 65, 81;
 | 
			
		||||
  --neutral-200: 31, 41, 55;
 | 
			
		||||
  --neutral-100: 17, 24, 39;
 | 
			
		||||
  --accent-400: 132, 204, 22;
 | 
			
		||||
  --accent-500: 101, 163, 13;
 | 
			
		||||
  --accent-600: 77, 124, 15;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  :root {
 | 
			
		||||
    --contrast: 0, 0, 0;
 | 
			
		||||
    --neutral-900: 17, 24, 39;
 | 
			
		||||
    --neutral-800: 31, 41, 55;
 | 
			
		||||
    --neutral-700: 55, 65, 81;
 | 
			
		||||
    --neutral-600: 75, 85, 99;
 | 
			
		||||
    --neutral-500: 107, 114, 128;
 | 
			
		||||
    --neutral-300: 209, 213, 219;
 | 
			
		||||
    --neutral-400: 156, 163, 175;
 | 
			
		||||
    --neutral-200: 229, 231, 235;
 | 
			
		||||
    --accent-600: 101, 163, 13;
 | 
			
		||||
    --accent-500: 132, 204, 22;
 | 
			
		||||
    --accent-400: 163, 230, 53;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ export const chooseConverter = new Elysia().use(userService).post(
 | 
			
		||||
        >
 | 
			
		||||
          {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
 | 
			
		||||
            <article
 | 
			
		||||
              class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
 | 
			
		||||
              class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`}
 | 
			
		||||
              data-converter={converter}
 | 
			
		||||
            >
 | 
			
		||||
              <header class="mb-2 w-full text-xl font-bold" safe>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@ import { mkdir } from "node:fs/promises";
 | 
			
		||||
import { Elysia, t } from "elysia";
 | 
			
		||||
import sanitize from "sanitize-filename";
 | 
			
		||||
import { outputDir, uploadsDir } from "..";
 | 
			
		||||
import { mainConverter } from "../converters/main";
 | 
			
		||||
import { handleConvert } from "../converters/main";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { Jobs } from "../db/types";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const convert = new Elysia().use(userService).post(
 | 
			
		||||
@@ -46,6 +46,11 @@ export const convert = new Elysia().use(userService).post(
 | 
			
		||||
 | 
			
		||||
    const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
 | 
			
		||||
    const converterName = body.convert_to.split(",")[1];
 | 
			
		||||
 | 
			
		||||
    if (!converterName) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fileNames = JSON.parse(body.file_names) as string[];
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < fileNames.length; i++) {
 | 
			
		||||
@@ -61,43 +66,15 @@ export const convert = new Elysia().use(userService).post(
 | 
			
		||||
      jobId.value,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const query = db.query(
 | 
			
		||||
      "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Start the conversion process in the background
 | 
			
		||||
    Promise.all(
 | 
			
		||||
      fileNames.map(async (fileName) => {
 | 
			
		||||
        const filePath = `${userUploadsDir}${fileName}`;
 | 
			
		||||
        const fileTypeOrig = fileName.split(".").pop() ?? "";
 | 
			
		||||
        const fileType = normalizeFiletype(fileTypeOrig);
 | 
			
		||||
        const newFileExt = normalizeOutputFiletype(convertTo);
 | 
			
		||||
        const newFileName = fileName.replace(
 | 
			
		||||
          new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
 | 
			
		||||
          newFileExt,
 | 
			
		||||
        );
 | 
			
		||||
        const targetPath = `${userOutputDir}${newFileName}`;
 | 
			
		||||
 | 
			
		||||
        const result = await mainConverter(
 | 
			
		||||
          filePath,
 | 
			
		||||
          fileType,
 | 
			
		||||
          convertTo,
 | 
			
		||||
          targetPath,
 | 
			
		||||
          {},
 | 
			
		||||
          converterName,
 | 
			
		||||
        );
 | 
			
		||||
        if (jobId.value) {
 | 
			
		||||
          query.run(jobId.value, fileName, newFileName, result);
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    )
 | 
			
		||||
    handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        // All conversions are done, update the job status to 'completed'
 | 
			
		||||
        if (jobId.value) {
 | 
			
		||||
          db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete all uploaded files in userUploadsDir
 | 
			
		||||
        // Delete all uploaded files in userUploadsDir
 | 
			
		||||
        // rmSync(userUploadsDir, { recursive: true, force: true });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
@@ -112,5 +89,6 @@ export const convert = new Elysia().use(userService).post(
 | 
			
		||||
      convert_to: t.String(),
 | 
			
		||||
      file_names: t.String(),
 | 
			
		||||
    }),
 | 
			
		||||
    auth: true,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,16 +7,7 @@ import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const deleteFile = new Elysia().use(userService).post(
 | 
			
		||||
  "/delete",
 | 
			
		||||
  async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  async ({ body, redirect, cookie: { jobId }, user }) => {
 | 
			
		||||
    if (!jobId?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
@@ -37,5 +28,5 @@ export const deleteFile = new Elysia().use(userService).post(
 | 
			
		||||
      message: "File deleted successfully.",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  { body: t.Object({ filename: t.String() }) },
 | 
			
		||||
  { body: t.Object({ filename: t.String() }), auth: true },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import sanitize from "sanitize-filename";
 | 
			
		||||
import * as tar from "tar";
 | 
			
		||||
import { outputDir } from "..";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
@@ -9,16 +11,7 @@ export const download = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get(
 | 
			
		||||
    "/download/:userId/:jobId/:fileName",
 | 
			
		||||
    async ({ params, jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
      if (!auth?.value) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const user = await jwt.verify(auth.value);
 | 
			
		||||
      if (!user) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    async ({ params, redirect, user }) => {
 | 
			
		||||
      const job = await db
 | 
			
		||||
        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
        .get(user.id, params.jobId);
 | 
			
		||||
@@ -26,7 +19,7 @@ export const download = new Elysia()
 | 
			
		||||
      if (!job) {
 | 
			
		||||
        return redirect(`${WEBROOT}/results`, 302);
 | 
			
		||||
      }
 | 
			
		||||
      // parse from url encoded string
 | 
			
		||||
      // parse from URL encoded string
 | 
			
		||||
      const userId = decodeURIComponent(params.userId);
 | 
			
		||||
      const jobId = decodeURIComponent(params.jobId);
 | 
			
		||||
      const fileName = sanitize(decodeURIComponent(params.fileName));
 | 
			
		||||
@@ -34,29 +27,39 @@ export const download = new Elysia()
 | 
			
		||||
      const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
 | 
			
		||||
      return Bun.file(filePath);
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      auth: true,
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
  .get("/zip/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    // TODO: Implement zip download
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
  .get(
 | 
			
		||||
    "/archive/:userId/:jobId",
 | 
			
		||||
    async ({ params, redirect, user }) => {
 | 
			
		||||
      const job = await db
 | 
			
		||||
        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
        .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
      if (!job) {
 | 
			
		||||
        return redirect(`${WEBROOT}/results`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    const job = await db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
      .get(user.id, params.jobId);
 | 
			
		||||
      const userId = decodeURIComponent(params.userId);
 | 
			
		||||
      const jobId = decodeURIComponent(params.jobId);
 | 
			
		||||
      const outputPath = `${outputDir}${userId}/${jobId}`;
 | 
			
		||||
      const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
 | 
			
		||||
 | 
			
		||||
    if (!job) {
 | 
			
		||||
      return redirect(`${WEBROOT}/results`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // const userId = decodeURIComponent(params.userId);
 | 
			
		||||
    // const jobId = decodeURIComponent(params.jobId);
 | 
			
		||||
    // const outputPath = `${outputDir}${userId}/`{jobId}/);
 | 
			
		||||
 | 
			
		||||
    // return Bun.zip(outputPath);
 | 
			
		||||
  });
 | 
			
		||||
      await tar.create(
 | 
			
		||||
        {
 | 
			
		||||
          file: outputTar,
 | 
			
		||||
          cwd: outputPath,
 | 
			
		||||
          filter: (path) => {
 | 
			
		||||
            return !path.match(".*\\.tar");
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        ["."],
 | 
			
		||||
      );
 | 
			
		||||
      return Bun.file(outputTar);
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      auth: true,
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -4,21 +4,16 @@ import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { Filename, Jobs } from "../db/types";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const history = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
export const history = new Elysia().use(userService).get(
 | 
			
		||||
  "/history",
 | 
			
		||||
  async ({ redirect, user }) => {
 | 
			
		||||
    if (HIDE_HISTORY) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
@@ -32,7 +27,7 @@ export const history = new Elysia()
 | 
			
		||||
      job.files_detailed = files;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // filter out jobs with no files
 | 
			
		||||
    // Filter out jobs with no files
 | 
			
		||||
    userJobs = userJobs.filter((job) => job.num_files > 0);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -133,7 +128,7 @@ export const history = new Elysia()
 | 
			
		||||
                            />
 | 
			
		||||
                          </svg>
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td safe>{new Date(job.date_created).toLocaleTimeString()}</td>
 | 
			
		||||
                        <td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</td>
 | 
			
		||||
                        <td>{job.num_files}</td>
 | 
			
		||||
                        <td class="max-sm:hidden">{job.finished_files}</td>
 | 
			
		||||
                        <td safe>{job.status}</td>
 | 
			
		||||
@@ -162,7 +157,7 @@ export const history = new Elysia()
 | 
			
		||||
                                  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                                  viewBox="0 0 20 20"
 | 
			
		||||
                                  fill="currentColor"
 | 
			
		||||
                                  class="mx-2 inline-block h-4 w-4 text-neutral-500"
 | 
			
		||||
                                  class={`mx-2 inline-block h-4 w-4 text-neutral-500`}
 | 
			
		||||
                                >
 | 
			
		||||
                                  <path
 | 
			
		||||
                                    fill-rule="evenodd"
 | 
			
		||||
@@ -213,4 +208,8 @@ export const history = new Elysia()
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    auth: true,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -6,18 +6,9 @@ import { getAllInputs, getAllTargets } from "../converters/main";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const listConverters = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export const listConverters = new Elysia().use(userService).get(
 | 
			
		||||
  "/converters",
 | 
			
		||||
  async () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
 | 
			
		||||
        <>
 | 
			
		||||
@@ -77,4 +68,8 @@ export const listConverters = new Elysia()
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    auth: true,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
@@ -8,10 +9,14 @@ import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
function ResultsArticle({
 | 
			
		||||
  user,
 | 
			
		||||
  job,
 | 
			
		||||
  files,
 | 
			
		||||
  outputPath,
 | 
			
		||||
}: {
 | 
			
		||||
  user: {
 | 
			
		||||
    id: string;
 | 
			
		||||
  } & JWTPayloadSpec;
 | 
			
		||||
  job: Jobs;
 | 
			
		||||
  files: Filename[];
 | 
			
		||||
  outputPath: string;
 | 
			
		||||
@@ -21,14 +26,19 @@ function ResultsArticle({
 | 
			
		||||
      <div class="mb-4 flex items-center justify-between">
 | 
			
		||||
        <h1 class="text-xl">Results</h1>
 | 
			
		||||
        <div>
 | 
			
		||||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            class="float-right w-40 btn-primary"
 | 
			
		||||
            onclick="downloadAll()"
 | 
			
		||||
            {...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
 | 
			
		||||
          <a
 | 
			
		||||
            style={files.length !== job.num_files ? "pointer-events: none;" : ""}
 | 
			
		||||
            href={`${WEBROOT}/archive/${user.id}/${job.id}`}
 | 
			
		||||
            download={`converted_files_${job.id}.tar`}
 | 
			
		||||
          >
 | 
			
		||||
            {files.length === job.num_files ? "Download All" : "Converting..."}
 | 
			
		||||
          </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="float-right w-40 btn-primary"
 | 
			
		||||
              {...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
 | 
			
		||||
            >
 | 
			
		||||
              {files.length === job.num_files ? "Download All" : "Converting..."}
 | 
			
		||||
            </button>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <progress
 | 
			
		||||
@@ -126,90 +136,80 @@ function ResultsArticle({
 | 
			
		||||
 | 
			
		||||
export const results = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
  .get(
 | 
			
		||||
    "/results/:jobId",
 | 
			
		||||
    async ({ params, set, cookie: { job_id }, user }) => {
 | 
			
		||||
      if (job_id?.value) {
 | 
			
		||||
        // Clear the job_id cookie since we are viewing the results
 | 
			
		||||
        job_id.remove();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    if (job_id?.value) {
 | 
			
		||||
      // clear the job_id cookie since we are viewing the results
 | 
			
		||||
      job_id.remove();
 | 
			
		||||
    }
 | 
			
		||||
      const job = db
 | 
			
		||||
        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
        .as(Jobs)
 | 
			
		||||
        .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
      if (!job) {
 | 
			
		||||
        set.status = 404;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "Job not found.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    const job = db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
      .as(Jobs)
 | 
			
		||||
      .get(user.id, params.jobId);
 | 
			
		||||
      const outputPath = `${user.id}/${params.jobId}/`;
 | 
			
		||||
 | 
			
		||||
    if (!job) {
 | 
			
		||||
      set.status = 404;
 | 
			
		||||
      return {
 | 
			
		||||
        message: "Job not found.",
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
      const files = db
 | 
			
		||||
        .query("SELECT * FROM file_names WHERE job_id = ?")
 | 
			
		||||
        .as(Filename)
 | 
			
		||||
        .all(params.jobId);
 | 
			
		||||
 | 
			
		||||
    const outputPath = `${user.id}/${params.jobId}/`;
 | 
			
		||||
      return (
 | 
			
		||||
        <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
 | 
			
		||||
          <>
 | 
			
		||||
            <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
 | 
			
		||||
            <main
 | 
			
		||||
              class={`
 | 
			
		||||
                w-full flex-1 px-2
 | 
			
		||||
                sm:px-4
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />
 | 
			
		||||
            </main>
 | 
			
		||||
            <script src={`${WEBROOT}/results.js`} defer />
 | 
			
		||||
          </>
 | 
			
		||||
        </BaseHtml>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    { auth: true },
 | 
			
		||||
  )
 | 
			
		||||
  .post(
 | 
			
		||||
    "/progress/:jobId",
 | 
			
		||||
    async ({ set, params, cookie: { job_id }, user }) => {
 | 
			
		||||
      if (job_id?.value) {
 | 
			
		||||
        // Clear the job_id cookie since we are viewing the results
 | 
			
		||||
        job_id.remove();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    const files = db
 | 
			
		||||
      .query("SELECT * FROM file_names WHERE job_id = ?")
 | 
			
		||||
      .as(Filename)
 | 
			
		||||
      .all(params.jobId);
 | 
			
		||||
      const job = db
 | 
			
		||||
        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
        .as(Jobs)
 | 
			
		||||
        .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <ResultsArticle job={job} files={files} outputPath={outputPath} />
 | 
			
		||||
          </main>
 | 
			
		||||
          <script src={`${WEBROOT}/results.js`} defer />
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
  .post("/progress/:jobId", async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
      if (!job) {
 | 
			
		||||
        set.status = 404;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "Job not found.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    if (job_id?.value) {
 | 
			
		||||
      // clear the job_id cookie since we are viewing the results
 | 
			
		||||
      job_id.remove();
 | 
			
		||||
    }
 | 
			
		||||
      const outputPath = `${user.id}/${params.jobId}/`;
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
      const files = db
 | 
			
		||||
        .query("SELECT * FROM file_names WHERE job_id = ?")
 | 
			
		||||
        .as(Filename)
 | 
			
		||||
        .all(params.jobId);
 | 
			
		||||
 | 
			
		||||
    const job = db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
      .as(Jobs)
 | 
			
		||||
      .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    if (!job) {
 | 
			
		||||
      set.status = 404;
 | 
			
		||||
      return {
 | 
			
		||||
        message: "Job not found.",
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const outputPath = `${user.id}/${params.jobId}/`;
 | 
			
		||||
 | 
			
		||||
    const files = db
 | 
			
		||||
      .query("SELECT * FROM file_names WHERE job_id = ?")
 | 
			
		||||
      .as(Filename)
 | 
			
		||||
      .all(params.jobId);
 | 
			
		||||
 | 
			
		||||
    return <ResultsArticle job={job} files={files} outputPath={outputPath} />;
 | 
			
		||||
  });
 | 
			
		||||
      return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
 | 
			
		||||
    },
 | 
			
		||||
    { auth: true },
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { randomInt } from "node:crypto";
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import { Elysia, t } from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import { getAllTargets } from "../converters/main";
 | 
			
		||||
@@ -12,13 +12,14 @@ import {
 | 
			
		||||
  ALLOW_UNAUTHENTICATED,
 | 
			
		||||
  HIDE_HISTORY,
 | 
			
		||||
  HTTP_ALLOWED,
 | 
			
		||||
  UNAUTHENTICATED_USER_SHARING,
 | 
			
		||||
  WEBROOT,
 | 
			
		||||
} from "../helpers/env";
 | 
			
		||||
import { FIRST_RUN, userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const root = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
 | 
			
		||||
export const root = new Elysia().use(userService).get(
 | 
			
		||||
  "/",
 | 
			
		||||
  async ({ jwt, redirect, cookie: { auth, jobId } }) => {
 | 
			
		||||
    if (!ALLOW_UNAUTHENTICATED) {
 | 
			
		||||
      if (FIRST_RUN) {
 | 
			
		||||
        return redirect(`${WEBROOT}/setup`, 302);
 | 
			
		||||
@@ -33,7 +34,9 @@ export const root = new Elysia()
 | 
			
		||||
    let user: ({ id: string } & JWTPayloadSpec) | false = false;
 | 
			
		||||
    if (ALLOW_UNAUTHENTICATED) {
 | 
			
		||||
      const newUserId = String(
 | 
			
		||||
        randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
 | 
			
		||||
        UNAUTHENTICATED_USER_SHARING
 | 
			
		||||
          ? 0
 | 
			
		||||
          : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
 | 
			
		||||
      );
 | 
			
		||||
      const accessToken = await jwt.sign({
 | 
			
		||||
        id: newUserId,
 | 
			
		||||
@@ -62,7 +65,7 @@ export const root = new Elysia()
 | 
			
		||||
        user.id &&
 | 
			
		||||
        (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
 | 
			
		||||
      ) {
 | 
			
		||||
        // make sure user exists in db
 | 
			
		||||
        // Make sure user exists in db
 | 
			
		||||
        const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
 | 
			
		||||
 | 
			
		||||
        if (!existingUser) {
 | 
			
		||||
@@ -182,7 +185,7 @@ export const root = new Elysia()
 | 
			
		||||
                        <header class="mb-2 w-full text-xl font-bold" safe>
 | 
			
		||||
                          {converter}
 | 
			
		||||
                        </header>
 | 
			
		||||
                        <ul class="convert_to_target flex flex-row flex-wrap gap-1">
 | 
			
		||||
                        <ul class={`convert_to_target flex flex-row flex-wrap gap-1`}>
 | 
			
		||||
                          {targets.map((target) => (
 | 
			
		||||
                            <button
 | 
			
		||||
                              // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
 | 
			
		||||
@@ -237,4 +240,11 @@ export const root = new Elysia()
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    cookie: t.Cookie({
 | 
			
		||||
      auth: t.Optional(t.String()),
 | 
			
		||||
      jobId: t.Optional(t.String()),
 | 
			
		||||
    }),
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -6,16 +6,7 @@ import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const upload = new Elysia().use(userService).post(
 | 
			
		||||
  "/upload",
 | 
			
		||||
  async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  async ({ body, redirect, user, cookie: { jobId } }) => {
 | 
			
		||||
    if (!jobId?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
@@ -44,5 +35,5 @@ export const upload = new Elysia().use(userService).post(
 | 
			
		||||
      message: "Files uploaded successfully.",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  { body: t.Object({ file: t.Files() }) },
 | 
			
		||||
  { body: t.Object({ file: t.Files() }), auth: true },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -32,26 +32,34 @@ export const userService = new Elysia({ name: "user/service" })
 | 
			
		||||
      email: t.String(),
 | 
			
		||||
      password: t.String(),
 | 
			
		||||
    }),
 | 
			
		||||
    session: t.Cookie({
 | 
			
		||||
      auth: t.String(),
 | 
			
		||||
      jobId: t.Optional(t.String()),
 | 
			
		||||
    }),
 | 
			
		||||
    optionalSession: t.Cookie({
 | 
			
		||||
      auth: t.Optional(t.String()),
 | 
			
		||||
      jobId: t.Optional(t.String()),
 | 
			
		||||
    }),
 | 
			
		||||
  })
 | 
			
		||||
  .macro({
 | 
			
		||||
    isSignIn(enabled: boolean) {
 | 
			
		||||
      if (!enabled) return;
 | 
			
		||||
 | 
			
		||||
  .macro("auth", {
 | 
			
		||||
    cookie: "session",
 | 
			
		||||
    async resolve({ status, jwt, cookie: { auth } }) {
 | 
			
		||||
      if (!auth.value) {
 | 
			
		||||
        return status(401, {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: "Unauthorized",
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      const user = await jwt.verify(auth.value);
 | 
			
		||||
      if (!user) {
 | 
			
		||||
        return status(401, {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: "Unauthorized",
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        async beforeHandle({ status, jwt, cookie: { auth } }) {
 | 
			
		||||
          if (auth?.value) {
 | 
			
		||||
            const user = await jwt.verify(auth.value);
 | 
			
		||||
            return {
 | 
			
		||||
              success: true,
 | 
			
		||||
              user,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return status(401, {
 | 
			
		||||
            success: false,
 | 
			
		||||
            message: "Unauthorized",
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
        success: true,
 | 
			
		||||
        user,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
@@ -228,82 +236,86 @@ export const user = new Elysia()
 | 
			
		||||
    },
 | 
			
		||||
    { body: "signIn" },
 | 
			
		||||
  )
 | 
			
		||||
  .get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (FIRST_RUN) {
 | 
			
		||||
      return redirect(`${WEBROOT}/setup`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if already logged in, redirect to home
 | 
			
		||||
    if (auth?.value) {
 | 
			
		||||
      const user = await jwt.verify(auth.value);
 | 
			
		||||
 | 
			
		||||
      if (user) {
 | 
			
		||||
        return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
  .get(
 | 
			
		||||
    "/login",
 | 
			
		||||
    async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
      if (FIRST_RUN) {
 | 
			
		||||
        return redirect(`${WEBROOT}/setup`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      auth.remove();
 | 
			
		||||
    }
 | 
			
		||||
      // if already logged in, redirect to home
 | 
			
		||||
      if (auth?.value) {
 | 
			
		||||
        const user = await jwt.verify(auth.value);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Login">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header
 | 
			
		||||
            webroot={WEBROOT}
 | 
			
		||||
            accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
            hideHistory={HIDE_HISTORY}
 | 
			
		||||
          />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <form method="post" class="flex flex-col gap-4">
 | 
			
		||||
                <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Email
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="email"
 | 
			
		||||
                      name="email"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Email"
 | 
			
		||||
                      autocomplete="email"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Password
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="password"
 | 
			
		||||
                      name="password"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
                      autocomplete="current-password"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
                <div class="flex flex-row gap-4">
 | 
			
		||||
                  {ACCOUNT_REGISTRATION ? (
 | 
			
		||||
                    <a
 | 
			
		||||
                      href={`${WEBROOT}/register`}
 | 
			
		||||
                      role="button"
 | 
			
		||||
                      class="w-full btn-secondary text-center"
 | 
			
		||||
                    >
 | 
			
		||||
                      Register
 | 
			
		||||
                    </a>
 | 
			
		||||
                  ) : null}
 | 
			
		||||
                  <input type="submit" value="Login" class="w-full btn-primary" />
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
            </article>
 | 
			
		||||
          </main>
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
        if (user) {
 | 
			
		||||
          return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        auth.remove();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <BaseHtml webroot={WEBROOT} title="ConvertX | Login">
 | 
			
		||||
          <>
 | 
			
		||||
            <Header
 | 
			
		||||
              webroot={WEBROOT}
 | 
			
		||||
              accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
              allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
              hideHistory={HIDE_HISTORY}
 | 
			
		||||
            />
 | 
			
		||||
            <main
 | 
			
		||||
              class={`
 | 
			
		||||
                w-full flex-1 px-2
 | 
			
		||||
                sm:px-4
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              <article class="article">
 | 
			
		||||
                <form method="post" class="flex flex-col gap-4">
 | 
			
		||||
                  <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                    <label class="flex flex-col gap-1">
 | 
			
		||||
                      Email
 | 
			
		||||
                      <input
 | 
			
		||||
                        type="email"
 | 
			
		||||
                        name="email"
 | 
			
		||||
                        class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                        placeholder="Email"
 | 
			
		||||
                        autocomplete="email"
 | 
			
		||||
                        required
 | 
			
		||||
                      />
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <label class="flex flex-col gap-1">
 | 
			
		||||
                      Password
 | 
			
		||||
                      <input
 | 
			
		||||
                        type="password"
 | 
			
		||||
                        name="password"
 | 
			
		||||
                        class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                        placeholder="Password"
 | 
			
		||||
                        autocomplete="current-password"
 | 
			
		||||
                        required
 | 
			
		||||
                      />
 | 
			
		||||
                    </label>
 | 
			
		||||
                  </fieldset>
 | 
			
		||||
                  <div class="flex flex-row gap-4">
 | 
			
		||||
                    {ACCOUNT_REGISTRATION ? (
 | 
			
		||||
                      <a
 | 
			
		||||
                        href={`${WEBROOT}/register`}
 | 
			
		||||
                        role="button"
 | 
			
		||||
                        class="w-full btn-secondary text-center"
 | 
			
		||||
                      >
 | 
			
		||||
                        Register
 | 
			
		||||
                      </a>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <input type="submit" value="Login" class="w-full btn-primary" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </form>
 | 
			
		||||
              </article>
 | 
			
		||||
            </main>
 | 
			
		||||
          </>
 | 
			
		||||
        </BaseHtml>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    { body: "signIn", cookie: "optionalSession" },
 | 
			
		||||
  )
 | 
			
		||||
  .post(
 | 
			
		||||
    "/login",
 | 
			
		||||
    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
 | 
			
		||||
@@ -363,85 +375,86 @@ export const user = new Elysia()
 | 
			
		||||
 | 
			
		||||
    return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
  })
 | 
			
		||||
  .get("/account", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`);
 | 
			
		||||
    }
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
  .get(
 | 
			
		||||
    "/account",
 | 
			
		||||
    async ({ user, redirect }) => {
 | 
			
		||||
      if (!user) {
 | 
			
		||||
        return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
      const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
 | 
			
		||||
 | 
			
		||||
    const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
 | 
			
		||||
      if (!userData) {
 | 
			
		||||
        return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    if (!userData) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Account">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header
 | 
			
		||||
            webroot={WEBROOT}
 | 
			
		||||
            accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
            hideHistory={HIDE_HISTORY}
 | 
			
		||||
            loggedIn
 | 
			
		||||
          />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <form method="post" class="flex flex-col gap-4">
 | 
			
		||||
                <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Email
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="email"
 | 
			
		||||
                      name="email"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Email"
 | 
			
		||||
                      autocomplete="email"
 | 
			
		||||
                      value={userData.email}
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Password (leave blank for unchanged)
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="password"
 | 
			
		||||
                      name="newPassword"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
                      autocomplete="new-password"
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Current Password
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="password"
 | 
			
		||||
                      name="password"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
                      autocomplete="current-password"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
                <div role="group">
 | 
			
		||||
                  <input type="submit" value="Update" class="w-full btn-primary" />
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
            </article>
 | 
			
		||||
          </main>
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
      return (
 | 
			
		||||
        <BaseHtml webroot={WEBROOT} title="ConvertX | Account">
 | 
			
		||||
          <>
 | 
			
		||||
            <Header
 | 
			
		||||
              webroot={WEBROOT}
 | 
			
		||||
              accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
              allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
              hideHistory={HIDE_HISTORY}
 | 
			
		||||
              loggedIn
 | 
			
		||||
            />
 | 
			
		||||
            <main
 | 
			
		||||
              class={`
 | 
			
		||||
                w-full flex-1 px-2
 | 
			
		||||
                sm:px-4
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              <article class="article">
 | 
			
		||||
                <form method="post" class="flex flex-col gap-4">
 | 
			
		||||
                  <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                    <label class="flex flex-col gap-1">
 | 
			
		||||
                      Email
 | 
			
		||||
                      <input
 | 
			
		||||
                        type="email"
 | 
			
		||||
                        name="email"
 | 
			
		||||
                        class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                        placeholder="Email"
 | 
			
		||||
                        autocomplete="email"
 | 
			
		||||
                        value={userData.email}
 | 
			
		||||
                        required
 | 
			
		||||
                      />
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <label class="flex flex-col gap-1">
 | 
			
		||||
                      Password (leave blank for unchanged)
 | 
			
		||||
                      <input
 | 
			
		||||
                        type="password"
 | 
			
		||||
                        name="newPassword"
 | 
			
		||||
                        class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                        placeholder="Password"
 | 
			
		||||
                        autocomplete="new-password"
 | 
			
		||||
                      />
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <label class="flex flex-col gap-1">
 | 
			
		||||
                      Current Password
 | 
			
		||||
                      <input
 | 
			
		||||
                        type="password"
 | 
			
		||||
                        name="password"
 | 
			
		||||
                        class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                        placeholder="Password"
 | 
			
		||||
                        autocomplete="current-password"
 | 
			
		||||
                        required
 | 
			
		||||
                      />
 | 
			
		||||
                    </label>
 | 
			
		||||
                  </fieldset>
 | 
			
		||||
                  <div role="group">
 | 
			
		||||
                    <input type="submit" value="Update" class="w-full btn-primary" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </form>
 | 
			
		||||
              </article>
 | 
			
		||||
            </main>
 | 
			
		||||
          </>
 | 
			
		||||
        </BaseHtml>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      auth: true,
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
  .post(
 | 
			
		||||
    "/account",
 | 
			
		||||
    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
 | 
			
		||||
@@ -505,5 +518,6 @@ export const user = new Elysia()
 | 
			
		||||
        newPassword: t.MaybeEmpty(t.String()),
 | 
			
		||||
        password: t.String(),
 | 
			
		||||
      }),
 | 
			
		||||
      cookie: "session",
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								src/theme/theme.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
:root {
 | 
			
		||||
  /* Light mode */
 | 
			
		||||
  --contrast: oklch(100% 0 0);
 | 
			
		||||
  /* Neutral colors - Gray */
 | 
			
		||||
  --neutral-950: oklch(98.5% 0.002 247.839);
 | 
			
		||||
  --neutral-900: oklch(96.7% 0.003 264.542);
 | 
			
		||||
  --neutral-800: oklch(92.8% 0.006 264.531);
 | 
			
		||||
  --neutral-700: oklch(87.2% 0.01 258.338);
 | 
			
		||||
  --neutral-600: oklch(70.7% 0.022 261.325);
 | 
			
		||||
  --neutral-500: oklch(55.1% 0.027 264.364);
 | 
			
		||||
  --neutral-400: oklch(44.6% 0.03 256.802);
 | 
			
		||||
  --neutral-300: oklch(37.3% 0.034 259.733);
 | 
			
		||||
  --neutral-200: oklch(26.9% 0 0);
 | 
			
		||||
  --neutral-100: oklch(21% 0.034 264.665);
 | 
			
		||||
  --neutral-50: oklch(13% 0.028 261.692);
 | 
			
		||||
  /* lime-700 */
 | 
			
		||||
  --accent-600: oklch(53.2% 0.157 131.589);
 | 
			
		||||
  /* lime-600 */
 | 
			
		||||
  --accent-500: oklch(64.8% 0.2 131.684);
 | 
			
		||||
  /* lime-500 */
 | 
			
		||||
  --accent-400: oklch(76.8% 0.233 130.85);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  /* Dark mode */
 | 
			
		||||
  :root {
 | 
			
		||||
    --contrast: oklch(0% 0 0);
 | 
			
		||||
    /* Neutral colors - Gray */
 | 
			
		||||
    --neutral-950: oklch(13% 0.028 261.692);
 | 
			
		||||
    --neutral-900: oklch(21% 0.034 264.665);
 | 
			
		||||
    --neutral-800: oklch(27.8% 0.033 256.848);
 | 
			
		||||
    --neutral-700: oklch(37.3% 0.034 259.733);
 | 
			
		||||
    --neutral-600: oklch(44.6% 0.03 256.802);
 | 
			
		||||
    --neutral-500: oklch(55.1% 0.027 264.364);
 | 
			
		||||
    --neutral-400: oklch(70.7% 0.022 261.325);
 | 
			
		||||
    --neutral-300: oklch(87.2% 0.01 258.338);
 | 
			
		||||
    --neutral-200: oklch(92.8% 0.006 264.531);
 | 
			
		||||
    --neutral-100: oklch(96.7% 0.003 264.542);
 | 
			
		||||
    --neutral-50: oklch(98.5% 0.002 247.839);
 | 
			
		||||
    /* lime-600 */
 | 
			
		||||
    --accent-600: oklch(64.8% 0.2 131.684);
 | 
			
		||||
    /* lime-500 */
 | 
			
		||||
    --accent-500: oklch(76.8% 0.233 130.85);
 | 
			
		||||
    /* lime-400 */
 | 
			
		||||
    --accent-400: oklch(84.1% 0.238 128.85);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/assimp.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/assimp";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/calibre.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/calibre";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										91
									
								
								tests/converters/dvisvgm.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/dvisvgm";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert respects eps filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.eps", "eps", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["--eps", "input.eps", "output.stl"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects pdf filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.pdf", "pdf", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["--pdf", "input.pdf", "output.stl"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects svgz conversion target type", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.obj", "eps", "svgz", "output.svgz", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-z", "input.obj", "output.svgz"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										181
									
								
								tests/converters/ffmpeg.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,181 @@
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/ffmpeg";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
function mockExecFile(
 | 
			
		||||
  _cmd: string,
 | 
			
		||||
  args: string[],
 | 
			
		||||
  callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
) {
 | 
			
		||||
  calls.push(args);
 | 
			
		||||
  if (args.includes("fail.mov")) {
 | 
			
		||||
    callback(new Error("mock failure"), "", "Fake stderr: fail");
 | 
			
		||||
  } else {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
  delete process.env.FFMPEG_ARGS;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("converts a normal file", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-i", "in.mp4", "out.avi"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("adds resize for ico output", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("in.png", "png", "ico", "out.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done: resized to 256x256");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining(["-filter:v", expect.stringContaining("scale=")]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libaom-av1 for av1.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "av1.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libaom-av1"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx264 for h264.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx264"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx265 for h265.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h265.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx265"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx266 for h266.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h266.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx266"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("respects FFMPEG_ARGS", async () => {
 | 
			
		||||
  process.env.FFMPEG_ARGS = "-hide_banner -y";
 | 
			
		||||
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]?.slice(0, 2)).toEqual(["-hide_banner", "-y"]);
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("fails on exec error", async () => {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(convert("fail.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile)).rejects.toThrow(
 | 
			
		||||
    "mock failure",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Fake stderr: fail");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("logs stderr when execFile returns only stderr and no error", async () => {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Mock execFile to call back with no error, no stdout, but with stderr
 | 
			
		||||
  const mockExecFileStderrOnly = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Only stderr output");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFileStderrOnly);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Only stderr output");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/graphicsmagick.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/graphicsmagick";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										26
									
								
								tests/converters/helpers/commonTests.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { ConvertFnWithExecFile } from "../../../src/converters/types";
 | 
			
		||||
import {
 | 
			
		||||
  runConvertFailTest,
 | 
			
		||||
  runConvertLogsStderror,
 | 
			
		||||
  runConvertLogsStderrorAndStdout,
 | 
			
		||||
  runConvertSuccessTest,
 | 
			
		||||
} from "./converters";
 | 
			
		||||
 | 
			
		||||
export function runCommonTests(convert: ConvertFnWithExecFile) {
 | 
			
		||||
  test("convert resolves when execFile succeeds", async () => {
 | 
			
		||||
    await runConvertSuccessTest(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert rejects when execFile fails", async () => {
 | 
			
		||||
    await runConvertFailTest(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert logs stderr when present", async () => {
 | 
			
		||||
    await runConvertLogsStderror(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert logs both stderr and stdout when present", async () => {
 | 
			
		||||
    await runConvertLogsStderrorAndStdout(convert);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								tests/converters/helpers/converters.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,121 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { expect } from "bun:test";
 | 
			
		||||
import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types";
 | 
			
		||||
 | 
			
		||||
export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertFailTest(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(new Error("Test error"), "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile),
 | 
			
		||||
  ).rejects.toMatch(/error: Error: Test error/);
 | 
			
		||||
 | 
			
		||||
  // Test with error object lacking 'message' property
 | 
			
		||||
  const mockExecFileNoMessage: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    // Simulate a non-standard error object
 | 
			
		||||
    callback({ notMessage: true } as unknown as ExecFileException, "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileNoMessage),
 | 
			
		||||
  ).rejects.toMatch(/error:/i);
 | 
			
		||||
 | 
			
		||||
  // Test with a non-object error (e.g., a string)
 | 
			
		||||
  const mockExecFileStringError: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback("string error" as unknown as ExecFileException, "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileStringError),
 | 
			
		||||
  ).rejects.toMatch(/error:/i);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertLogsStderror(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Fake stderr");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertLogsStderrorAndStdout(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedError = "";
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedError = msg;
 | 
			
		||||
  };
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(loggedError).toBe("stderr: Fake stderr");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										165
									
								
								tests/converters/imagemagick.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,165 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/imagemagick";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert respects ico conversion target type", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.obj", "eps", "ico", "output.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "icon:auto-resize=256,128,64,48,32,16",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "none",
 | 
			
		||||
      "input.obj",
 | 
			
		||||
      "output.ico",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects ico conversion target type with svg as input filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.svg", "svg", "ico", "output.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "icon:auto-resize=256,128,64,48,32,16",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "none",
 | 
			
		||||
      "-density",
 | 
			
		||||
      "512",
 | 
			
		||||
      "input.svg",
 | 
			
		||||
      "output.ico",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects ico conversion target type with emf as input filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.emf", "emf", "ico", "output.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "icon:auto-resize=256,128,64,48,32,16",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "none",
 | 
			
		||||
      "emf:delegate=false",
 | 
			
		||||
      "-density",
 | 
			
		||||
      "300",
 | 
			
		||||
      "white",
 | 
			
		||||
      "-alpha",
 | 
			
		||||
      "remove",
 | 
			
		||||
      "input.emf",
 | 
			
		||||
      "output.ico",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects emf as input filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.emf", "emf", "obj", "output.obj", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "emf:delegate=false",
 | 
			
		||||
      "-density",
 | 
			
		||||
      "300",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "white",
 | 
			
		||||
      "-alpha",
 | 
			
		||||
      "remove",
 | 
			
		||||
      "input.emf",
 | 
			
		||||
      "output.obj",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/inkscape.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/inkscape";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/libheif.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/libheif";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										91
									
								
								tests/converters/libjxl.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/libjxl";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let command: string = "";
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  command = "";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert uses djxl with input filetype being jxl", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    command = _cmd;
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.jxl", "jxl", "png", "output.png", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(command).toEqual("djxl");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert uses cjxl with output filetype being jxl", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    command = _cmd;
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.png", "png", "jxl", "output.jxl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(command).toEqual("cjxl");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert uses empty string as command with neither input nor output filetype being jxl", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    command = _cmd;
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.png", "png", "jpg", "output.jpg", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(command).toEqual("");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										61
									
								
								tests/converters/msgconvert.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,61 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/msgconvert";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
 | 
			
		||||
test("convert rejects conversion if input filetype is not msg and output type is not eml", async () => {
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const expectedError = new Error(
 | 
			
		||||
    "Unsupported conversion from obj to stl. Only MSG to EML conversion is currently supported.",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  expect(convert("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile)).rejects.toEqual(
 | 
			
		||||
    expectedError,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert rejects conversion on error", async () => {
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(new Error("Test error"), "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const expectedError = new Error("msgconvert failed: Test error");
 | 
			
		||||
 | 
			
		||||
  expect(convert("input.msg", "msg", "eml", "output.eml", undefined, mockExecFile)).rejects.toEqual(
 | 
			
		||||
    expectedError,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert logs stderr as warning", async () => {
 | 
			
		||||
  const originalConsoleWarn = console.warn;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.warn = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("file.msg", "msg", "eml", "out.eml", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleWarn;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("msgconvert stderr: Fake stderr");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/potrace.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/potrace";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/resvg.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/resvg";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										65
									
								
								tests/converters/vips.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,65 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { convert } from "../../src/converters/vips";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert uses action pdfload with filetype being pdf", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.pdf", "pdf", "obj", "output.obj", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["pdfload"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert uses action copy with filetype being anything but pdf", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.jpg", "jpg", "obj", "output.obj", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["copy"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/xelatex.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/xelatex";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
@@ -5,8 +5,9 @@
 | 
			
		||||
    "target": "ES2021",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "moduleDetection": "force",
 | 
			
		||||
    "allowImportingTsExtensions": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    // "allowImportingTsExtensions": true,
 | 
			
		||||
    "outDir": "dist",
 | 
			
		||||
    "noEmit": false,
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "downlevelIteration": true,
 | 
			
		||||
@@ -24,7 +25,10 @@
 | 
			
		||||
    // "noUnusedParameters": true,
 | 
			
		||||
    "exactOptionalPropertyTypes": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "noImplicitOverride": true
 | 
			
		||||
    "noImplicitOverride": true,
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "esModuleInterop": true
 | 
			
		||||
    // "noImplicitReturns": true
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src", "tests", "package.json", "reset.d.ts"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||