diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..46a71a84080a254a2f3d75f1e025f60573f2d1cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+.cache/
+.venv/
+logs/
+output/
+
+*.log
+git_add_*.txt
+git_add_output.log
+
+imageforge/.cache/
+imageforge/logs/
+imageforge/output/
+
+stable-diffusion-webui/models/
+stable-diffusion-webui/repositories/
+stable-diffusion-webui/tmp/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..2197ec22781bdef7dc0965cd66fcb4839107c281
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "chat.tools.terminal.autoApprove": {
+ "&": true,
+ "npm run dev:web": true
+ }
+}
\ No newline at end of file
diff --git a/ARCHITECTURE_HUGGINGFACE.md b/ARCHITECTURE_HUGGINGFACE.md
new file mode 100644
index 0000000000000000000000000000000000000000..f56a13aef35badeb53b3cf942b9d41dc4a0f24f0
--- /dev/null
+++ b/ARCHITECTURE_HUGGINGFACE.md
@@ -0,0 +1,245 @@
+# 🏗️ PixelForge Architektur mit HuggingFace Spaces
+
+## Übersicht
+
+PixelForge wird als **Orchestrator** fungieren, der externe AI-Services via REST API orchestriert. Die Heavy-Lifting (GPU-intensive Image Generation) läuft auf HuggingFace Spaces.
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ PixelForge Frontend (React/Vite) │
+│ http://127.0.0.1:5173 │
+└────────────────────┬────────────────────────────────────┘
+ │
+ │ HTTP Requests
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ PixelForge Backend (FastAPI) │
+│ http://127.0.0.1:8008 │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ Provider Factory & Job Manager │ │
+│ └────┬─────────────────────────────────────────────┘ │
+└────┼──────────────────────────────────────────────────────┘
+ │
+ ├─ Lokale Provider (CPU):
+ │ ├─ dummy: Dummy bilder
+ │ ├─ localai: LocalAI Engine
+ │ └─ diffusion: Stable Diffusion (lokal)
+ │
+ └─ Remote Provider (API/GPU):
+ ├─ zimageturbo: Cloud API (Colab/RunPod) [ÜBERGANGSLÖSUNG]
+ ├─ huggingface: HuggingFace Space API [ZUKÜNFTIG]
+ │ ├─ Heartsync/Adult Space (erwachsene Inhalte)
+ │ ├─ weitere HF Spaces...
+ │ └─ Authentifizierung via HF API Token
+ │
+ └─ a1111: AUTOMATIC1111 API (lokal falls vorhanden)
+```
+
+---
+
+## Phase 1: Aktuelle State (Übergangslösung)
+
+**Colab + Z-Image Turbo** als schneller Remote-Provider:
+- ✅ Funktioniert sofort
+- ⚠️ Session-basiert (max 12 Stunden)
+- ⚠️ Colab kann jeden Moment beenden
+- ✅ Kostenlos
+
+**Implementation**: `zimageturbo_provider.py` (HTTP API Wrapper)
+
+### Zu tun jetzt:
+1. Frontend testen (Modellauswahl wurde gefixt)
+2. Colab Notebook hochladen & API URL setzen
+3. Z-Image Turbo Provider testen
+
+---
+
+## Phase 2: HuggingFace Space Integration (Zukünftig)
+
+**Adult_repo** bleibt als eigenständige Einheit auf HuggingFace Spaces:
+
+```bash
+# Adult_repo läuft auf:
+https://huggingface.co/spaces/Heartsync/Adult
+# → Hat eigene GPU/Inference Engine
+# → Verfügbar 24/7 (solange Space online)
+```
+
+### Architektur:
+
+1. **Adult_repo auf HF Space** (remote)
+ - Gradio Interface (aber nicht für PixelForge relevant)
+ - Inference Endpoint für API-Calls
+ - Nutzt Z-Image Turbo Model intern
+
+2. **HuggingFace Space API Provider** (neu in PixelForge)
+ ```python
+ # Backend Provider
+ class HuggingFaceSpaceProvider(IImageProvider):
+ def __init__(self, space_name: str, token: str):
+ self.space_name = "Heartsync/Adult" # oder andere Spaces
+ self.api_token = os.getenv("HF_API_TOKEN")
+
+ def generate(self, request: ProviderRequest) -> ProviderResult:
+ # POST zu HF Inference API
+ # https://api-inference.huggingface.co/models/{space}
+ ...
+ ```
+
+3. **PixelForge kennt mehrere Spaces**:
+ - `heartsync/adult` → Erwachsene Inhalte
+ - `heartsync/anime` → Anime-Style
+ - `custom/space` → Beliebige andere Spaces
+
+---
+
+## Vergleich: Colab vs HuggingFace Space
+
+| Aspekt | Colab | HF Space | Lokal |
+|--------|-------|----------|-------|
+| **Setup** | Jupyter Notebook | Einmal deployen | Python Virtual Env |
+| **Kosten** | Kostenlos | Kostenlos (mit Limits) | Nur Hardware |
+| **Persistenz** | 12h max | 24/7 (solange online) | 24/7 |
+| **GPU** | T4/A100 (kostenlos) | Space-spezifisch | Deine Hardware |
+| **API-Zugriff** | über Ngrok-Tunnel | HF Inference API | localhost:PORT |
+| **Startzeit** | ~2 min (Kernel) | instant | instant |
+| **Geeignet für** | Schnelle Tests | Production | Development |
+
+---
+
+## Implementierungs-Roadmap
+
+### ✅ Heute (Phase 1)
+- [x] Frontend Model-Picker repariert
+- [x] Colab Notebook erstellt
+- [x] zimageturbo_provider.py ✓
+- [ ] Testen mit Colab
+
+### 📅 Später (Phase 2)
+- [ ] HuggingFaceSpaceProvider erstellen
+- [ ] HF API Token Authentication
+- [ ] Adult_repo auf HF Space deployen
+- [ ] Provider registrieren & testen
+- [ ] Colab durch HF Space ersetzen
+
+### 🎯 Langfristig (Phase 3)
+- [ ] Multi-Space Support (verschiedene Spaces wählen)
+- [ ] Space Health Monitoring
+- [ ] Fallback-Strategie (wenn Space down ist)
+- [ ] Queue Management für lange Jobs
+
+---
+
+## HuggingFace API Details (für später)
+
+### Space Deployment (Adult_repo)
+```bash
+# HF CLI installieren
+pip install huggingface-hub
+
+# Private Space erstellen (Adult content)
+huggingface-cli repo create --type space Adult_repo --private
+
+# Adult_repo hochladen
+git push huggingface main
+```
+
+### PixelForge Provider (HTTP API Call)
+```python
+import requests
+
+# HF Inference API Endpoint
+url = "https://api-inference.huggingface.co/models/Heartsync/Adult"
+
+payload = {
+ "inputs": "A beautiful sunset over mountains",
+}
+
+headers = {
+ "Authorization": f"Bearer {HF_API_TOKEN}"
+}
+
+response = requests.post(url, json=payload, headers=headers)
+image_data = response.content # PIL Image bytes
+```
+
+### Alternative: Space API (wenn Gradio genutzt)
+```python
+# Falls Adult_repo Gradio API exponiert
+from gradio_client import Client
+
+client = Client("https://huggingface.co/spaces/Heartsync/Adult")
+result = client.predict(
+ prompt="A girl in a school uniform",
+ seed=42,
+ api_name="/predict"
+)
+```
+
+---
+
+## Umgebungsvariablen (Phase 2)
+
+```powershell
+# HuggingFace Integration
+$env:HF_API_TOKEN = "hf_xxxxxxxxxxxxxxxxxxxxx"
+$env:HF_SPACE_ADULT = "Heartsync/Adult"
+$env:HF_SPACE_DEFAULT = "Heartsync/Adult"
+$env:HF_REQUEST_TIMEOUT = "300" # Sekunden
+
+# Colab (Phase 1 - temporär)
+$env:ZIMAGETURBO_API_URL = "https://abc123.ngrok.io"
+$env:ZIMAGETURBO_TIMEOUT = "300"
+```
+
+---
+
+## Adult_repo Struktur (vor Ort)
+
+```
+d:/VSC Codes/Bild/Adult_repo/
+├── app.py # Gradio Interface (wird auf HF Space laufen)
+├── requirements.txt # Dependencies
+├── README.md
+└── .git # Git Repo
+```
+
+**Wichtig**: Adult_repo ist momentan nicht in PixelForge integriert!
+- Es läuft später als eigenständiger Service auf HF Space
+- PixelForge ruft es via HF API auf
+- Keine lokale Abhängigkeit → Clean Architecture ✨
+
+---
+
+## Nächste Schritte
+
+### Sofort (Phase 1):
+```powershell
+# 1. Frontend aktualisieren (Done ✓)
+# 2. Browser aktualisieren (F5)
+# 3. Modellauswahl testen
+# 4. Colab konfigurieren & testen
+```
+
+### Später (Phase 2):
+```powershell
+# 1. Adult_repo zu HF Space pushen
+# 2. HF API Token besorgen
+# 3. HuggingFaceSpaceProvider schreiben
+# 4. In Factory registrieren
+# 5. Testen & Colab deaktivieren
+```
+
+---
+
+## Fragen für Phase 2?
+
+- Welche anderen Spaces möchtest du anbinden?
+- Benötigst du Content Moderation (Safety Filter)?
+- Wie sollen lange Jobs (>5min) gehandhabt werden?
+- Fallback-Strategie wenn Space down ist?
+
+---
+
+**Status**: 🟢 Phase 1 bereitet sich vor
+**Phase 2 Start**: Nach Colab-Test & HF Space Deployment
diff --git a/Adult_repo/.gitattributes b/Adult_repo/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b
--- /dev/null
+++ b/Adult_repo/.gitattributes
@@ -0,0 +1,35 @@
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.arrow filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.ckpt filter=lfs diff=lfs merge=lfs -text
+*.ftz filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.h5 filter=lfs diff=lfs merge=lfs -text
+*.joblib filter=lfs diff=lfs merge=lfs -text
+*.lfs.* filter=lfs diff=lfs merge=lfs -text
+*.mlmodel filter=lfs diff=lfs merge=lfs -text
+*.model filter=lfs diff=lfs merge=lfs -text
+*.msgpack filter=lfs diff=lfs merge=lfs -text
+*.npy filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.onnx filter=lfs diff=lfs merge=lfs -text
+*.ot filter=lfs diff=lfs merge=lfs -text
+*.parquet filter=lfs diff=lfs merge=lfs -text
+*.pb filter=lfs diff=lfs merge=lfs -text
+*.pickle filter=lfs diff=lfs merge=lfs -text
+*.pkl filter=lfs diff=lfs merge=lfs -text
+*.pt filter=lfs diff=lfs merge=lfs -text
+*.pth filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.safetensors filter=lfs diff=lfs merge=lfs -text
+saved_model/**/* filter=lfs diff=lfs merge=lfs -text
+*.tar.* filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.tflite filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.wasm filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.zst filter=lfs diff=lfs merge=lfs -text
+*tfevents* filter=lfs diff=lfs merge=lfs -text
diff --git a/Adult_repo/README.md b/Adult_repo/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..534b4721a7c658033e3082531f5aa60ab240ccd2
--- /dev/null
+++ b/Adult_repo/README.md
@@ -0,0 +1,13 @@
+---
+title: NSFW Uncensored Adult Image
+emoji: 📈
+colorFrom: green
+colorTo: blue
+sdk: gradio
+sdk_version: 6.0.2
+app_file: app.py
+pinned: false
+short_description: Based 'Z-IMAGE TURBO'
+---
+
+Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
diff --git a/Adult_repo/app.py b/Adult_repo/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..1aef0de6e3598b02e01f21ae7b0f4a4329b7bfc6
--- /dev/null
+++ b/Adult_repo/app.py
@@ -0,0 +1,207 @@
+import torch
+import spaces
+import gradio as gr
+from diffusers import DiffusionPipeline
+import diffusers
+import numpy as np
+import random
+
+# =========================================================
+# MODEL CONFIGURATION
+# =========================================================
+MAX_SEED = np.iinfo(np.int32).max
+
+# =========================================================
+# PROMPT EXAMPLES
+# =========================================================
+prompt_examples = [
+ "The shy college girl, with glasses and a tight plaid skirt, nervously approaches her professor",
+ "Her skirt rose a little higher with each gentle push, a soft blush of blush spreading across her cheeks as she felt the satisfying warmth of his breath on her cheek.",
+ "a girl in a school uniform having her skirt pulled up by a boy, and then being fucked",
+ "Moody mature anime scene of two lovers fuck under neon rain, sensual atmosphere",
+ "Moody mature anime scene of two lovers kissing under neon rain, sensual atmosphere",
+ "The girl sits on the boy's lap by the window, his hands resting on her waist. She is unbuttoning his shirt, her expression focused and intense.",
+ "A girl with long, black hair is sleeping on her desk in the classroom. Her skirt has ridden up, revealing her thighs, and a trail of drool escapes her slightly parted lips.",
+ "The waves rolled gently, a slow, sweet kiss of the lip, a slow, slow build of anticipation as their toes bumped gently – a slow, sweet kiss of the lip, a promise of more to come.",
+ "Her elegant silk gown swayed gracefully as she approached him, the delicate fabric brushing against her legs. A warm blush spread across her cheeks as she felt his breath on her face.",
+ "Her white blouse and light cotton skirt rose a little higher with each gentle push, a soft blush spreading across her cheeks as she felt the satisfying warmth of his breath on her cheek.",
+ "A woman in a business suit having her skirt lifted by a man, and then being sexually assaulted.",
+ "The older woman sits on the man's lap by the fireplace, his hands resting on her hips. She is unbuttoning his vest, her expression focused and intense. He takes control of the situation as she finishes unbuttoning his shirt, pushing her onto her back and begins to have sex with her.",
+ "There is a woman with long black hair. Her face features alluring eyes and full lips, with a slender figure adorned in black lace lingerie. She lies on the bed, loosening her lingerie strap with one hand while seductively glancing downward.",
+ "In a dimly lit room, the same woman teases with her dark, flowing hair, now covering her voluptuous breasts, while a black garter belt accentuates her thighs. She sits on the sofa, leaning back, lifting one leg to expose her most private areas through the sheer lingerie.",
+ "A woman with glasses, lying on the bed in just her bra, spreads her legs wide, revealing all! She wears a sultry expression, gazing directly at the viewer with her brown eyes, her short black hair cascading over the pillow. Her slim figure, accentuated by the lacy lingerie, exudes a seductive aura.",
+ "A soft focus on the girl's face, eyes closed, biting her lip, as her roommate performs oral pleasure, the experienced woman's hair cascading between her thighs.",
+ "A woman in a blue hanbok sits on a wooden floor, her legs folded beneath her, gazing out of a window, the sunlight highlighting the graceful lines of her clothing.",
+ "The couple, immersed in a wooden outdoor bath, share an intimate moment, her wet kimono clinging to her curves, his hands exploring her body beneath the water's surface.",
+ "A steamy shower scene, the twins embrace under the warm water, their soapy hands gliding over each other's curves, their passion intensifying as they explore uncharted territories.",
+ "The teacher, with a firm grip, pins the student against the blackboard, her skirt hiked up, exposing her delicate lace panties. Their heavy breathing echoes in the quiet room as they share an intense, intimate moment.",
+ "After hours, the girl sits on top of the teacher's lap, riding him on the classroom floor, her hair cascading over her face as she moves with increasing intensity, their bodies glistening with sweat.",
+ "In the dimly lit dorm room, the roommates lay entangled in a passionate embrace, their naked bodies glistening with sweat, as the experienced woman teaches her lover the art of kissing and touching.",
+ "The once-innocent student, now confident, takes charge, straddling her lover on the couch, their bare skin illuminated by the warm glow of the sunset through the window.",
+ "A close-up of the secretary's hand unzipping her boss's dress shirt, her fingers gently caressing his chest, their eyes locked in a heated embrace in the supply closet.",
+ "The secretary, in a tight pencil skirt and silk blouse, leans back on the boss's desk, her legs wrapped around his waist, her blouse unbuttoned, revealing her lace bra, as he passionately kisses her, his hands exploring her body.",
+ "On the living room couch, one twin sits astride her sister's lap, their lips locked in a passionate kiss, their hands tangled in each other's hair, unraveling a new level of intimacy.",
+ "In a dimly lit chamber, the dominant woman, dressed in a leather corset and thigh-high boots, stands tall, her hand gripping her submissive partner's hair, his eyes closed in submission as she instructs him to please her.",
+ "The dominant, in a sheer lace bodysuit, sits on a throne-like chair, her legs spread, as the submissive, on his knees, worships her with his tongue, his hands bound behind his back.",
+ "A traditional Japanese onsen, with steam rising, a young woman in a colorful kimono kneels on a tatami mat, her back to the viewer, as her male partner, also in a kimono, gently unties her obi, revealing her bare back.",
+ "In a serene outdoor setting, the woman, in a vibrant summer kimono, sits on a bench, her legs slightly spread, her partner kneeling before her, his hands gently caressing her exposed thigh.",
+]
+
+# =========================================================
+# LOAD PIPELINE
+# =========================================================
+print("Loading Z-Image-Turbo pipeline...")
+diffusers.utils.logging.set_verbosity_info()
+
+pipe = DiffusionPipeline.from_pretrained(
+ "Tongyi-MAI/Z-Image-Turbo",
+ torch_dtype=torch.bfloat16,
+ low_cpu_mem_usage=False,
+ attn_implementation="kernels-community/vllm-flash-attn3",
+)
+pipe.to("cuda")
+
+# =========================================================
+# RANDOM PROMPT FUNCTION
+# =========================================================
+def get_random_prompt():
+ return random.choice(prompt_examples)
+
+# =========================================================
+# IMAGE GENERATOR
+# =========================================================
+@spaces.GPU
+def generate_image(prompt, height, width, num_inference_steps, seed, randomize_seed, num_images):
+ if not prompt:
+ raise gr.Error("Please enter a prompt.")
+
+ if randomize_seed:
+ seed = torch.randint(0, 2**32 - 1, (1,)).item()
+
+ num_images = min(max(1, int(num_images)), 4)
+
+ generator = torch.Generator("cuda").manual_seed(int(seed))
+
+ result = pipe(
+ prompt=prompt,
+ height=int(height),
+ width=int(width),
+ num_inference_steps=int(num_inference_steps),
+ guidance_scale=0.0,
+ generator=generator,
+ max_sequence_length=1024,
+ num_images_per_prompt=num_images,
+ output_type="pil",
+ )
+
+ return result.images, seed
+
+# =========================================================
+# GRADIO UI
+# =========================================================
+with gr.Blocks() as demo:
+
+ gr.HTML("""
+
+
+
+
+ 🖼️ NSFW Uncensored Adult "Text to Image"
+
+
+
+ Powered by Z-Image-Turbo Model
+
+
+
+
+ """)
+
+
+ with gr.Row():
+ with gr.Column(scale=1):
+ prompt_input = gr.Textbox(
+ label="✏️ Prompt",
+ placeholder="Describe the image you want to create...",
+ lines=3
+ )
+
+ random_button = gr.Button("🎲 Random Prompt", variant="secondary")
+
+ with gr.Row():
+ height_input = gr.Slider(512, 2048, 1024, step=64, label="Height")
+ width_input = gr.Slider(512, 2048, 1024, step=64, label="Width")
+
+ num_images_input = gr.Slider(1, 4, 2, step=1, label="🖼️ Number of Images")
+
+ with gr.Accordion("⚙️ Options", open=False):
+ steps_slider = gr.Slider(
+ minimum=1,
+ maximum=30,
+ step=1,
+ value=18,
+ label="Inference Steps"
+ )
+ seed_input = gr.Slider(
+ label="Seed",
+ minimum=0,
+ maximum=MAX_SEED,
+ step=1,
+ value=42
+ )
+ randomize_seed_checkbox = gr.Checkbox(
+ label="Randomize Seed",
+ value=True
+ )
+
+ generate_button = gr.Button(
+ "✨ Generate Image",
+ variant="primary"
+ )
+
+ with gr.Column(scale=1):
+ output_gallery = gr.Gallery(
+ label="🎨 Generated Images",
+ height=450,
+ columns=2
+ )
+ used_seed_output = gr.Number(label="Seed Used", interactive=False)
+
+ random_button.click(
+ fn=get_random_prompt,
+ outputs=[prompt_input]
+ )
+
+ generate_button.click(
+ fn=generate_image,
+ inputs=[prompt_input, height_input, width_input, steps_slider, seed_input, randomize_seed_checkbox, num_images_input],
+ outputs=[output_gallery, used_seed_output],
+ )
+
+if __name__ == "__main__":
+ demo.queue().launch()
\ No newline at end of file
diff --git a/Adult_repo/requirements.txt b/Adult_repo/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a760ea9fba92bfd36303a5ffe540c7515a8a2d2c
--- /dev/null
+++ b/Adult_repo/requirements.txt
@@ -0,0 +1,9 @@
+gradio
+git+https://github.com/huggingface/diffusers
+transformers
+kernels
+
+torch
+transformers
+accelerate
+spaces
diff --git a/FIXED_README.md b/FIXED_README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f97c3275f234588b30ec76b78067900da93b7dc8
--- /dev/null
+++ b/FIXED_README.md
@@ -0,0 +1,87 @@
+# ImageForge - Fixed & Working!
+
+## ✓ Problem gelöst!
+
+Die Bildgenerierung funktioniert jetzt mit echten KI-Modellen!
+
+### Was wurde behoben:
+
+1. **GPU-Aktivierung**: PyTorch mit CUDA 12.1 installiert - GPU wird jetzt verwendet!
+2. **Threading-Fehler**: Race Condition bei parallelen Jobs behoben
+3. **Modell-Download**: `segmind/tiny-sd` vollständig heruntergeladen (~1 GB)
+4. **urllib3-Problem**: Downgrade auf 1.26.20 (behob Import-Fehler bei diffusers)
+5. **Default Model**: API verwendet jetzt `localai` statt `dummy` als Standard
+
+### Wie starten:
+
+Doppelklick auf `START_IMAGEFORGE.bat` in `d:\VSC Codes\Bild\`
+
+Das startet:
+- Backend auf http://127.0.0.1:8008
+- Frontend auf http://127.0.0.1:5173
+
+Nach ~15-20 Sekunden ist das Programm bereit!
+
+### Erste Bildgenerierung:
+
+1. Browser öffnet sich automatisch bei http://127.0.0.1:5173
+2. Prompt eingeben (z.B. "a beautiful sunset over mountains")
+3. Auf "Generate" klicken
+4. Warten (~45-60 Sekunden für das erste Bild)
+5. **Ergebnis**: Echtes KI-generiertes Bild (>400 KB), KEIN weißes Dummy-Bild mehr!
+
+### Verwendetes Modell:
+
+- **segmind/tiny-sd** (Standard)
+ - Schnell: ~45-60 Sekunden pro Bild (512x512)
+ - Speicher: ~2 GB VRAM
+ - Qualität: Gut für schnelle Tests
+
+### Optional: Besseres Modell (in Zukunft):
+
+Die `runwayml/stable-diffusion-v1-5` Download wurde gestartet (~4 GB).
+Wenn vollständig heruntergeladen, kann das Modell in `backend/app/local_ai/engine.py`
+umgestellt werden (Zeile 49):
+
+```python
+self.model_id = os.getenv("IMAGEFORGE_LOCALAI_MODEL", "runwayml/stable-diffusion-v1-5")
+```
+
+### Systemanforderungen (erfüllt):
+
+- ✓ Python 3.12.10 mit venv
+- ✓ PyTorch 2.5.1+cu121 (CUDA 12.1)
+- ✓ NVIDIA GeForce GTX 1050 (CUDA verfügbar)
+- ✓ diffusers 0.36.0
+- ✓ urllib3 1.26.20 (wichtig!)
+
+### Bekannte Einschränkungen:
+
+- Erste Generation pro Sitzung dauert länger (Modell laden: ~30 Sekunden)
+- tiny-sd erzeugt kleinere Bilder mit weniger Details als größere Modelle
+- GTX 1050 hat nur 2 GB VRAM - größere Modelle nicht möglich
+
+### Bei Problemen:
+
+Wenn wieder weiße Bilder erscheinen:
+
+1. Backend-Fenster prüfen auf Fehler
+2. Sicherstellen dass urllib3==1.26.20 installiert ist:
+ ```
+ d:\VSC Codes\Bild\.venv\Scripts\python.exe -m pip list | findstr urllib3
+ ```
+3. Falls 2.x: Downgrade durchführen:
+ ```
+ d:\VSC Codes\Bild\.venv\Scripts\python.exe -m pip install "urllib3<2.0" --force-reinstall
+ ```
+
+### Nächste Schritte (optional):
+
+1. **Qualität verbessern**: SD 1.5 Modell verwenden (sobald Download fertig)
+2. **Frontend anpassen**: UI-Elemente nach Wunsch ändern
+3. **Weitere Modelle**: Andere HuggingFace Modelle testen
+
+---
+
+**Status**: ✓ Funktioniert! Echte Bilder werden generiert!
+**Letzte Änderung**: 2026-02-19 22:20
diff --git a/START_IMAGEFORGE.bat b/START_IMAGEFORGE.bat
new file mode 100644
index 0000000000000000000000000000000000000000..0063f4551ab25e270fba6587a1f3f296f2f45042
--- /dev/null
+++ b/START_IMAGEFORGE.bat
@@ -0,0 +1,43 @@
+@echo off
+echo ======================================
+echo ImageForge Stack Startup
+echo ======================================
+echo.
+
+REM Set HuggingFace cache and environment variables
+set HF_HOME=d:/VSC Codes/Bild/.cache/hf
+set TRANSFORMERS_CACHE=d:/VSC Codes/Bild/.cache/hf
+
+cd /d "%~dp0imageforge"
+
+echo [1/2] Starting Backend...
+start "ImageForge Backend" /MIN cmd /k "set HF_HOME=d:/VSC Codes/Bild/.cache/hf && set TRANSFORMERS_CACHE=d:/VSC Codes/Bild/.cache/hf && set IMAGEFORGE_HOST=127.0.0.1 && set IMAGEFORGE_PORT=8008 && D:\VSC Codes\Bild\.venv\Scripts\python.exe -m backend.app.main"
+
+timeout /t 3 /nobreak >nul
+
+echo [2/2] Starting Frontend...
+cd frontend
+start "ImageForge Frontend" /MIN cmd /k "npm run dev"
+
+timeout /t 5 /nobreak >nul
+
+echo.
+echo ======================================
+echo Services Started!
+echo ======================================
+echo Backend: http://127.0.0.1:8008
+echo Frontend: http://127.0.0.1:5173
+echo ======================================
+echo.
+echo Opening UI in browser...
+timeout /t 2 /nobreak >nul
+start http://127.0.0.1:5173
+
+echo.
+echo Stack is running! Press any key to stop all services...
+pause >nul
+
+echo.
+echo Stopping services...
+taskkill /FI "WINDOWTITLE eq ImageForge*" /F >nul 2>&1
+echo Done!
diff --git a/ZIMAGETURBO_QUICKSTART.md b/ZIMAGETURBO_QUICKSTART.md
new file mode 100644
index 0000000000000000000000000000000000000000..70b500fc591eceb0530496acaacf7493dccc5ee7
--- /dev/null
+++ b/ZIMAGETURBO_QUICKSTART.md
@@ -0,0 +1,171 @@
+# 🚀 Z-Image Turbo - Schnellstart für PixelForge
+
+## ⏱️ 5 Minuten Installation & Test
+
+### 1️⃣ Google Colab Vorbereitung (2 Min)
+
+```bash
+# 1. Gehe zu https://colab.research.google.com
+# 2. Wähle: File → Open notebook → Upload
+# 3. Wähle: pixelforge_colab_test.ipynb
+# 4. Runtime → Change runtime type → GPU (T4)
+# 5. Registriere bei https://ngrok.com (kostenlos)
+# 6. Kopiere Auth Token aus Account Settings
+```
+
+### 2️⃣ Colab Notebook Ausführen (2 Min)
+
+```python
+# Cell 1: Abhängigkeiten installieren
+# → Drücke Shift+Enter zum Ausführen
+
+# Cell 2: GPU-Check
+# → Sollte zeigen: "GPU T4 erkannt ✓"
+
+# Cell 3: Modell laden
+# → Lädt Stable Diffusion (ca. 2 GB)
+
+# Cell 4: Test-Generierung
+# → Generiert ein Testbild (~30 Sekunden)
+
+# Cell 5: API-Server starten
+# → Ersetze "YOUR_NGROK_TOKEN" mit deinem Token
+# → Drücke Ausführen & kopiere Public URL
+# → Z.B.: https://abc123def456.ngrok.io
+```
+
+### 3️⃣ PixelForge Starten (1 Min)
+
+**Option A: PowerShell (Empfohlen)**
+```powershell
+cd "d:\VSC Codes\Bild"
+.\start_zimageturbo.ps1
+# → Gebe Colab Ngrok-URL ein
+```
+
+**Option B: Batch-Datei**
+```cmd
+cd d:\VSC Codes\Bild
+start_zimageturbo.bat
+# → Gebe Colab Ngrok-URL ein
+```
+
+### 4️⃣ Terminal-Befehle (Manuell)
+
+```powershell
+# Backend
+cd "d:\VSC Codes\Bild\imageforge"
+$env:ZIMAGETURBO_API_URL = "https://abc123def456.ngrok.io"
+$env:ZIMAGETURBO_TIMEOUT = "300"
+& "D:/VSC Codes/Bild/.venv/Scripts/python.exe" -m backend.app.main
+
+# Separate PowerShell Tab:
+cd "d:\VSC Codes\Bild\imageforge\frontend"
+npm run dev:web
+```
+
+### 5️⃣ Im Browser Testen
+
+```
+1. Öffne: http://127.0.0.1:5173
+2. Wähle "Z-Image Turbo" im Modell-Dropdown
+3. Gib Prompt ein: "A beautiful sunset over mountains"
+4. Klicke "Generate"
+5. Warte 30-60 Sekunden
+6. Bild sollte in Browser erscheinen ✓
+```
+
+---
+
+## ✅ Checkliste
+
+- [ ] Colab-Konto aktiviert & GPU eingeschaltet
+- [ ] ngrok-Account registriert & Auth Token kopiert
+- [ ] pixelforge_colab_test.ipynb in Colab hochgeladen
+- [ ] Alle 5 Cells im Notebook ausgeführt
+- [ ] Ngrok Public URL kopiert (z.B. https://abc123.ngrok.io)
+- [ ] start_zimageturbo.ps1 oder .bat ausgeführt
+- [ ] ZIMAGETURBO_API_URL korrekt gesetzt
+- [ ] Backend läuft: http://127.0.0.1:8008/health (sollte OK sein)
+- [ ] Frontend läuft: http://127.0.0.1:5173 (sollte React-UI zeigen)
+- [ ] Test-Generierung abgeschlossen (echtes Bild!)
+
+---
+
+## 🆘 Häufige Probleme
+
+| Problem | Lösung |
+|---------|--------|
+| "API nicht erreichbar" | Colab noch am Laden? Oder Ngrok Token ungültig? |
+| "Timeout nach 300s" | Colab-Model zu lange zum Laden? Timeout erhöhen: `$env:ZIMAGETURBO_TIMEOUT = "600"` |
+| "Colab-Sitzung verloren" | Google Colab beendet nach 4h Leerlauf. Notebook neu starten! |
+| "Port 5173 bereits in Verwendung" | Alte Vite instanz lauft noch: `Get-Process -Name "node" \| Stop-Process` |
+| "Fehler: Unknown Provider" | Backend neu starten nach Env-Variablen-Änderung! |
+
+---
+
+## 📊 Performance erwartet
+
+| Hardware | Zeit | Qualität |
+|----------|------|----------|
+| Colab T4 GPU | 30-60s | High ⭐⭐⭐ |
+| Lokal tiny-sd | 5-10s | Medium ⭐⭐ |
+| i9 + RTX 4070 | 5-10s | High ⭐⭐⭐ |
+| RunPod A100 | 2-3s | Ultra ⭐⭐⭐⭐ |
+
+---
+
+## 🔧 Umgebungsvariablen
+
+```powershell
+# Manuell (falls nicht über Skript):
+$env:ZIMAGETURBO_API_URL = "https://deine-ngrok-url.ngrok.io"
+$env:ZIMAGETURBO_TIMEOUT = "300" # Sekunden
+$env:ZIMAGETURBO_PROXY = "" # Optional für Proxy
+
+# Oder permanent in Windows:
+# Systemsteuerung → Umgebungsvariablen → Neue Var hinzufügen
+```
+
+---
+
+## 🌐 Alternative: Lokale Installation (Ohne Colab)
+
+```bash
+# Wenn du Z-Image Turbo lokal installieren möchtest:
+# 1. Lade Modell von HuggingFace herunter
+# 2. Starte lokalen Flask-Server (ähnlich Colab-Notebook)
+# 3. Setze ZIMAGETURBO_API_URL auf http://localhost:5000
+```
+
+---
+
+## 📝 Mehrere Modelle testen
+
+Im ZIMAGETURBO_SETUP.md gibt es weitere Optionen:
+- **Stable Diffusion v1.5** (schnell, gut)
+- **Stable Diffusion XL** (langsam, sehr gut)
+- **Custom Models** von HuggingFace
+
+Ändere einfach im Colab Cell 3 das Modell!
+
+---
+
+## 💡 Tipps
+
+1. **Colab bleibt aktiv**: Öffne DevTools (F12) → Keep console open
+2. **Bessere Prompts**: Nutze Details wie "4K, professional, highly detailed"
+3. **Batch-Generierung**: Mehrere Prompts nacheinander → Job-Queue
+4. **Speichern**: Generierte Bilder landen in output/YYYY-MM-DD/job_*/
+
+---
+
+## ❓ Mehr Hilfe
+
+- **Technisches**: Siehe [ZIMAGETURBO_SETUP.md](ZIMAGETURBO_SETUP.md)
+- **Provider Code**: [zimageturbo_provider.py](imageforge/backend/app/providers/zimageturbo_provider.py)
+- **Colab-Notebook**: [pixelforge_colab_test.ipynb](pixelforge_colab_test.ipynb)
+
+---
+
+**Viel Spaß beim Testen! 🎨✨**
diff --git a/ZIMAGETURBO_SETUP.md b/ZIMAGETURBO_SETUP.md
new file mode 100644
index 0000000000000000000000000000000000000000..75f4c2dbebaa6905f20f9f93eaff5e47a654b272
--- /dev/null
+++ b/ZIMAGETURBO_SETUP.md
@@ -0,0 +1,96 @@
+# Z-Image Turbo Integration für PixelForge
+
+## Schritt 1: Google Colab Setup
+
+1. Öffne `pixelforge_colab_test.ipynb` in Google Colab
+2. Führe alle Cells aus:
+ - Dependencies installieren
+ - GPU-Check
+ - Modell laden (Colab T4 = ~2-3 Min)
+ - API-Server starten
+
+3. **Ngrok einrichten (für Public URL):**
+ ```bash
+ # Registriere dich auf ngrok.com (kostenlos)
+ # Kopiere Auth-Token in Colab Cell 5
+ ```
+
+## Schritt 2: PixelForge konfigurieren
+
+Setze diese **Umgebungsvariablen** vor dem Backend-Start:
+
+```powershell
+$env:ZIMAGETURBO_API_URL = "https://dein-ngrok-url.ngrok.io" # Colab Public URL
+$env:ZIMAGETURBO_API_KEY = "" # Leer, wenn keine Auth benötigt
+$env:ZIMAGETURBO_TIMEOUT = "300" # 5 Minuten
+```
+
+**Beispiel (Windows PowerShell):**
+```powershell
+cd "d:/VSC Codes/Bild/imageforge"
+$env:ZIMAGETURBO_API_URL = "https://abc123.ngrok.io"
+& "D:/VSC Codes/Bild/.venv/Scripts/python.exe" -m backend.app.main
+```
+
+## Schritt 3: Backend-Start
+
+Backend wird neu gestartet und Z-Image Turbo sollte verfügbar sein:
+
+```
+[2026-02-20 12:34:56] ✓ Z-Image Turbo API verfügbar: https://abc123.ngrok.io
+```
+
+## Schritt 4: Im UI verwenden
+
+1. Öffne http://127.0.0.1:5173/
+2. Wechsle das Modell zu **"Z-Image Turbo"**
+3. Gib einen Prompt ein und klicke **"Generate"**
+4. Das Bild wird über Colab generiert ☁️
+
+## Lokale Installation (statt Colab)
+
+Wenn du Z-Image Turbo lokal installieren möchtest:
+
+```python
+# 1. Download & Install
+pip install z-image-turbo torch diffusers
+
+# 2. Lokaler API-Server (optional)
+from flask import Flask
+from z_image_turbo import Pipeline
+
+app = Flask(__name__)
+pipe = Pipeline("zimageturbo-fast") # Oder dein Modell
+
+@app.route('/health', methods=['GET'])
+def health():
+ return {"status": "ok"}
+
+@app.route('/generate', methods=['POST'])
+def generate():
+ # ... generiere Bild ...
+ pass
+```
+
+Dann: `ZIMAGETURBO_API_URL=http://localhost:5000`
+
+## Tipps & Troubleshooting
+
+| Problem | Lösung |
+|---------|--------|
+| **API antwortet nicht** | Ngrok-URL in Colab und Env-Var prüfen |
+| **Timeout (300s zu kurz)** | `ZIMAGETURBO_TIMEOUT=600` erhöhen |
+| **Colab-Session beendet** | Notebook neu starten (Ngrok-Token erforderlich) |
+| **"Unknown provider"** | Backend Cache leeren: `killall python` + Neustart |
+
+## Performance
+
+| Setup | Speed | Kosten |
+|-------|-------|--------|
+| **Colab + T4** | 30-60s/Bild | Kostenlos |
+| **Lokal (RTX 4070)** | 5-10s/Bild | ~€500 |
+| **RunPod (A100)** | 2-3s/Bild | €0.50-1.00/h |
+
+---
+
+**Viel Spaß mit Z-Image Turbo! 🚀**
diff --git a/add_exit.txt b/add_exit.txt
new file mode 100644
index 0000000000000000000000000000000000000000..462b5c5a6b7371d86179aab6f67e82da8ebcb491
--- /dev/null
+++ b/add_exit.txt
@@ -0,0 +1 @@
+EXIT:128
diff --git a/download_model.py b/download_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fd5f0293f5b634686338613069392fb54efac3d
--- /dev/null
+++ b/download_model.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+"""
+Download and cache a real Stable Diffusion model for offline use.
+This runs once, then the model is cached forever.
+"""
+
+import os
+import sys
+from pathlib import Path
+
+# Setup cache
+cache_root = Path("d:/VSC Codes/Bild/.cache/hf")
+cache_root.mkdir(parents=True, exist_ok=True)
+
+os.environ['HF_HOME'] = str(cache_root)
+os.environ['TORCH_HOME'] = str(cache_root / 'torch')
+
+print(f"Cache directory: {cache_root}")
+print()
+
+# Use direct API instead of CLI
+print("Downloading Stable Diffusion 2.1...")
+print("(This is one-time setup, then cached offline)")
+print()
+
+try:
+ # Use snapshot_download which is more robust
+ from huggingface_hub import snapshot_download
+
+ model_id = "stabilityai/stable-diffusion-2-1"
+ print(f"Model: {model_id}")
+ print()
+ print("Downloading (this may take 5-10 minutes on first run)...")
+
+ model_path = snapshot_download(
+ repo_id=model_id,
+ cache_dir=str(cache_root),
+ local_dir_use_symlinks=False
+ )
+
+ print()
+ print(f"✓ SUCCESS! Model cached at:")
+ print(f" {model_path}")
+ print()
+ print("Backend can now generate images offline!")
+ sys.exit(0)
+
+except Exception as e:
+ print(f"✗ FAILED: {type(e).__name__}: {e}")
+ print()
+ print("Trying tiny-sd as fallback (smaller, faster)...")
+ print()
+
+ try:
+ from huggingface_hub import snapshot_download
+ model_id = "segmind/tiny-sd"
+ model_path = snapshot_download(
+ repo_id=model_id,
+ cache_dir=str(cache_root),
+ local_dir_use_symlinks=False
+ )
+ print(f"✓ Fallback successful! Cached at: {model_path}")
+ sys.exit(0)
+ except Exception as e2:
+ print(f"✗ Fallback failed: {e2}")
+ sys.exit(1)
diff --git a/download_sd15.py b/download_sd15.py
new file mode 100644
index 0000000000000000000000000000000000000000..4680d2928de3856d1618b122cdf3246f00593852
--- /dev/null
+++ b/download_sd15.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+import os
+import sys
+from pathlib import Path
+from huggingface_hub import snapshot_download
+
+os.environ['HF_HUB_DISABLE_SYMLINKS'] = '1'
+
+cache = Path("d:/VSC Codes/Bild/.cache/hf")
+cache.mkdir(parents=True, exist_ok=True)
+
+print("Downloading runwayml/stable-diffusion-v1-5...")
+print("(~4GB, this is one-time setup)")
+print()
+
+try:
+ path = snapshot_download(
+ "runwayml/stable-diffusion-v1-5",
+ cache_dir=str(cache),
+ local_dir_use_symlinks=False
+ )
+ print()
+ print(f"✓ SUCCESS!")
+ print(f"Model cached to: {path}")
+ print()
+ print("ImageForge can now generate real images!")
+ sys.exit(0)
+except Exception as e:
+ print(f"✗ Error: {e}")
+ sys.exit(1)
diff --git a/download_tiny_sd.py b/download_tiny_sd.py
new file mode 100644
index 0000000000000000000000000000000000000000..af2b8581da583411a7d4a615b255aec2d5a3e87a
--- /dev/null
+++ b/download_tiny_sd.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+"""Download tiny-sd model - FAST download, REAL images (not dummy)"""
+import os
+import sys
+from pathlib import Path
+from huggingface_hub import snapshot_download
+
+os.environ['HF_HUB_DISABLE_SYMLINKS'] = '1'
+cache = Path("d:/VSC Codes/Bild/.cache/hf")
+
+print("=" * 60)
+print(" QUICK MODEL DOWNLOAD: tiny-sd")
+print("=" * 60)
+print()
+print("This is a small model (~600MB) that generates REAL images")
+print("(Not as good as SD 1.5, but better than white dummy images)")
+print()
+
+try:
+ print("Downloading segmind/tiny-sd...")
+ path = snapshot_download(
+ "segmind/tiny-sd",
+ cache_dir=str(cache),
+ local_dir_use_symlinks=False
+ )
+ print()
+ print("=" * 60)
+ print("✓ SUCCESS!")
+ print("=" * 60)
+ print(f"Model cached at: {path}")
+ print()
+ print("Backend can now generate REAL images!")
+ print("Just restart the backend to use it.")
+ sys.exit(0)
+except Exception as e:
+ print(f"✗ Error: {e}")
+ sys.exit(1)
diff --git a/imageforge/.github/workflows/ci.yml b/imageforge/.github/workflows/ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..201de71802d184f90688a35bbded9603a064b16a
--- /dev/null
+++ b/imageforge/.github/workflows/ci.yml
@@ -0,0 +1,31 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - name: Install Python deps
+ run: pip install -r requirements.txt
+ - name: Run backend tests
+ run: pytest
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ - name: Install frontend deps
+ run: npm --prefix frontend ci
+ - name: Typecheck frontend
+ run: npm --prefix frontend run typecheck
+ - name: Build renderer
+ run: npm --prefix frontend run build:renderer
+ - name: Install Playwright browser
+ run: npx --prefix frontend playwright install --with-deps chromium
+ - name: Run E2E smoke
+ run: npm --prefix frontend run test:e2e
diff --git a/imageforge/.gitignore b/imageforge/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..02064773b67d9145dfc31bd2127bcdfdf2f26397
--- /dev/null
+++ b/imageforge/.gitignore
@@ -0,0 +1,18 @@
+__pycache__/
+*.pyc
+.venv/
+venv/
+node_modules/
+frontend/node_modules/
+frontend/dist/
+frontend/dist-electron/
+output/
+app.log
+prompt_history.json
+.pytest_cache/
+frontend/test-results/
+frontend/playwright-report/
+backups/
+jobs_state.json
+settings.json
+admin_audit.log
diff --git a/imageforge/.npmrc b/imageforge/.npmrc
new file mode 100644
index 0000000000000000000000000000000000000000..b6dcc65762c989794f4386f796991d2aaa7d04f3
--- /dev/null
+++ b/imageforge/.npmrc
@@ -0,0 +1,2 @@
+cache=D:/AI/npm-cache
+logs-dir=D:/AI/npm-cache/_logs
diff --git a/imageforge/README.md b/imageforge/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..6743443da31a009285e0f8658903fed9070433b4
--- /dev/null
+++ b/imageforge/README.md
@@ -0,0 +1,199 @@
+# ImageForge
+
+ImageForge ist eine lokal laufende Desktop-App zur Bild-Erstellung per Prompt. Die App nutzt ein Python-Backend (FastAPI + Job-Queue) und ein Electron+React-Frontend.
+
+## Features
+
+- Prompt + Negative Prompt
+- Modellauswahl (`dummy`, `localai`, `diffusion`)
+- Bildtyp-Presets und Stil-Presets
+- Image-to-Image (Startbild + Strength)
+- Dashboard (Queue, Status, Retry/Cancel, Compare)
+- Preset-System (CRUD)
+- Export (PNG/JPG/WEBP)
+- Prompt-Versionierung (`config_hash` in `meta.json`)
+- API-Key + Rollenmodell (`viewer`, `operator`, `admin`)
+- Rate-Limit pro Client
+- Health + Readiness + Metrics
+- Recovery von Job-Status nach Neustart
+- Storage-Governance (Retention-Cleanup)
+
+## Setup
+
+```powershell
+cd imageforge
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+pip install -r requirements.txt
+npm --prefix frontend install
+```
+
+## Start
+
+```powershell
+npm run dev
+```
+
+Für den kompletten lokalen Stack (Backend + Frontend, inkl. Healthcheck):
+
+```powershell
+npm run dev:stack
+```
+
+## Serverbetrieb
+
+```powershell
+$env:IMAGEFORGE_HOST="0.0.0.0"
+$env:IMAGEFORGE_PORT="8008"
+$env:IMAGEFORGE_CORS_ORIGINS="http://localhost:5173"
+$env:IMAGEFORGE_API_KEYS="viewerKey:viewer,opsKey:operator,adminKey:admin"
+$env:IMAGEFORGE_RATE_LIMIT_PER_MIN="120"
+$env:IMAGEFORGE_CONTENT_PROFILE="internal-relaxed"
+$env:IMAGEFORGE_ADMIN_TOKEN="change-me"
+python -m backend.app.main
+```
+
+## Wichtige Endpunkte
+
+- `GET /health`
+- `GET /ready`
+- `GET /metrics`
+- `GET /metrics/prometheus`
+- `POST /generate`
+- `GET /jobs`
+- `POST /jobs/{id}/retry`
+- `POST /jobs/{id}/cancel`
+- `GET /dashboard/stats`
+- `GET/POST/DELETE /presets`
+- `POST /export`
+- `GET/PUT /admin/settings`
+- `POST /admin/cleanup`
+
+## Security und Policy
+
+- API-Key Header: `X-ImageForge-Api-Key`
+- Admin Override Header: `X-ImageForge-Admin-Token`
+- Policy Profile:
+ - `strict`
+ - `internal-relaxed`
+- Audit-Logs:
+ - `policy_audit.log`
+ - `admin_audit.log`
+
+## Backup / Restore
+
+```powershell
+npm run backup
+npm run restore -- -Source backups\imageforge_backup_YYYYMMDD_HHMMSS
+```
+
+## Tests
+
+```powershell
+npm run test
+npm run test:e2e
+```
+
+## CI
+
+GitHub Actions Workflow liegt in `.github/workflows/ci.yml`.
+
+## Optional LocalAI / Diffusion
+
+```powershell
+pip install diffusers torch transformers accelerate
+$env:IMAGEFORGE_LOCALAI_MODEL="stabilityai/sd-turbo"
+$env:IMAGEFORGE_LOCALAI_IMAGE_TIMEOUT_SECONDS="180"
+$env:IMAGEFORGE_LOCALAI_LOCAL_ONLY="1"
+$env:IMAGEFORGE_ENABLE_ATTENTION_SLICING="1"
+```
+
+Wenn `torch.cuda.is_available()` auf `False` bleibt, ist oft eine CPU-only Torch-Build installiert.
+Für NVIDIA-GPU unter Windows kann eine CUDA-Build so installiert werden:
+
+```powershell
+python -m pip install --upgrade --index-url https://download.pytorch.org/whl/cu121 torch torchvision torchaudio
+```
+
+## Optional AUTOMATIC1111 Integration
+
+ImageForge kann AUTOMATIC1111 als Provider nutzen (`model = a1111`).
+
+### Empfohlener Produktionspfad (Stability Matrix)
+
+Für stabilen Betrieb auf Windows wird eine **saubere, manager-gesteuerte Installation** empfohlen (statt manuell gepatchter `stable-diffusion-webui`-Klone):
+
+1. Stability Matrix installieren und dort eine frische WebUI-Instanz mit aktivierter API starten.
+2. API-Endpunkt prüfen:
+
+```powershell
+Invoke-RestMethod http://127.0.0.1:7860/sdapi/v1/sd-models
+```
+
+3. ImageForge auf diese Instanz zeigen (Default ist bereits `127.0.0.1:7860`):
+
+```powershell
+$env:IMAGEFORGE_A1111_BASE_URL="http://127.0.0.1:7860"
+```
+
+1. AUTOMATIC1111 lokal starten (mit API), z. B.:
+
+```powershell
+webui-user.bat --api
+```
+
+2. Optional URL/Timeout konfigurieren:
+
+```powershell
+$env:IMAGEFORGE_A1111_BASE_URL="http://127.0.0.1:7860"
+$env:IMAGEFORGE_A1111_TIMEOUT_SECONDS="180"
+$env:IMAGEFORGE_A1111_HEALTH_ENDPOINT="/sdapi/v1/sd-models"
+$env:IMAGEFORGE_A1111_RETRY_COUNT="2"
+$env:IMAGEFORGE_A1111_RETRY_BACKOFF_SECONDS="1.0"
+```
+
+Falls A1111 mit `--api-auth user:password` läuft:
+
+```powershell
+$env:IMAGEFORGE_A1111_API_USER="user"
+$env:IMAGEFORGE_A1111_API_PASSWORD="password"
+```
+
+Alternativ als ein String:
+
+```powershell
+$env:IMAGEFORGE_A1111_API_AUTH="user:password"
+```
+
+Danach erscheint `AUTOMATIC1111` in `/models` als verfügbar, sobald der A1111-Server erreichbar ist.
+
+### Robuster Betrieb bei A1111-Ausfällen
+
+Wenn A1111 nicht erreichbar ist oder Fehler liefert, kann ImageForge automatisch auf andere Provider wechseln (z. B. `localai`, `diffusion`, `dummy`) statt den Job direkt abzubrechen.
+
+```powershell
+$env:IMAGEFORGE_ENABLE_AUTO_FALLBACK="1"
+$env:IMAGEFORGE_FALLBACK_MODELS="a1111,localai,diffusion,dummy"
+$env:IMAGEFORGE_FALLBACK_TIMEOUT_SECONDS="90"
+$env:IMAGEFORGE_FALLBACK_MAX_STEPS="24"
+```
+
+- `IMAGEFORGE_ENABLE_AUTO_FALLBACK`: `1` aktiviert automatische Umschaltung.
+- `IMAGEFORGE_FALLBACK_MODELS`: Priorisierte Reihenfolge der Fallback-Provider.
+- `IMAGEFORGE_FALLBACK_TIMEOUT_SECONDS`: Kürzerer Timeout pro Fallback-Versuch.
+- `IMAGEFORGE_FALLBACK_MAX_STEPS`: Deckel für Steps bei Fallback, um Laufzeit zu reduzieren.
+
+### Stack-Readiness prüfen
+
+Nach dem Start von Backend und A1111:
+
+```powershell
+./scripts/healthcheck-stack.ps1 -RequireA1111
+```
+
+Das Skript validiert `/health`, `/ready`, `/models` und optional die A1111-API selbst.
+
+## Troubleshooting
+
+- Falls Desktop-Build auf Windows an Symlink-Rechten scheitert: Entwickler-Modus oder Admin-Rechte aktivieren.
+- Logs: `app.log`, `policy_audit.log`, `admin_audit.log`.
diff --git a/imageforge/backend/__init__.py b/imageforge/backend/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/__init__.py b/imageforge/backend/app/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/app/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/api/__init__.py b/imageforge/backend/app/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/app/api/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/api/schemas.py b/imageforge/backend/app/api/schemas.py
new file mode 100644
index 0000000000000000000000000000000000000000..6af57f3400fc57187e37a56a8366150ae2663770
--- /dev/null
+++ b/imageforge/backend/app/api/schemas.py
@@ -0,0 +1,163 @@
+from __future__ import annotations
+
+from datetime import datetime
+from enum import Enum
+from typing import Literal
+
+from pydantic import BaseModel, Field, field_validator
+
+
+class JobStatus(str, Enum):
+ QUEUED = "queued"
+ RUNNING = "running"
+ DONE = "done"
+ ERROR = "error"
+ CANCELLED = "cancelled"
+
+
+class GenerateRequest(BaseModel):
+ prompt: str = Field(min_length=1, max_length=2000)
+ negative_prompt: str = Field(default="", max_length=2000)
+ model: str = Field(default="localai") # Changed from "dummy" to "localai"
+ size: Literal["512x512", "768x768", "1024x1024", "1024x1536", "1536x1024"] = "512x512"
+ count: int = Field(default=1, ge=1, le=4)
+ seed: int | None = Field(default=None, ge=0)
+ random_seed: bool = True
+ steps: int = Field(default=30, ge=1, le=100)
+ guidance: float = Field(default=6.5, ge=1.0, le=20.0)
+ image_type: Literal[
+ "general",
+ "photo",
+ "portrait",
+ "landscape",
+ "architecture",
+ "product",
+ "logo",
+ "icon",
+ "poster",
+ "illustration",
+ "anime",
+ "pixel_art",
+ "sketch",
+ "painting",
+ "3d",
+ ] = "general"
+ style_preset: Literal[
+ "auto",
+ "photorealistic",
+ "cinematic",
+ "minimal",
+ "vibrant",
+ "monochrome",
+ "watercolor",
+ "oil",
+ "noir",
+ "fantasy",
+ ] = "auto"
+ style_strength: int = Field(default=60, ge=0, le=100)
+ admin_override: bool = False
+ init_image_path: str | None = None
+ img2img_strength: float = Field(default=0.45, ge=0.0, le=1.0)
+ model_variant: str | None = Field(default=None, max_length=200)
+
+ @field_validator("prompt")
+ @classmethod
+ def validate_prompt(cls, value: str) -> str:
+ value = value.strip()
+ if not value:
+ raise ValueError("Prompt must not be empty")
+ return value
+
+
+class GenerateResponse(BaseModel):
+ job_id: str
+
+
+class RetryResponse(BaseModel):
+ old_job_id: str
+ new_job_id: str
+
+
+class JobInfoResponse(BaseModel):
+ job_id: str
+ status: JobStatus
+ progress: int
+ message: str
+ created_at: datetime
+ updated_at: datetime
+ image_paths: list[str] = Field(default_factory=list)
+ output_dir: str | None = None
+ error: str | None = None
+
+
+class CancelResponse(BaseModel):
+ success: bool
+
+
+class ModelInfo(BaseModel):
+ id: str
+ name: str
+ available: bool
+ description: str
+
+
+class HistoryItem(BaseModel):
+ prompt: str
+ negative_prompt: str
+ timestamp: datetime
+
+
+class HealthResponse(BaseModel):
+ status: str
+ timestamp: datetime
+
+
+class PresetPayload(BaseModel):
+ name: str = Field(min_length=1, max_length=80)
+ prompt: str = ""
+ negative_prompt: str = ""
+ model: str = "dummy"
+ size: Literal["512x512", "768x768", "1024x1024", "1024x1536", "1536x1024"] = "512x512"
+ count: int = Field(default=1, ge=1, le=4)
+ steps: int = Field(default=30, ge=1, le=100)
+ guidance: float = Field(default=6.5, ge=1.0, le=20.0)
+ image_type: str = "general"
+ style_preset: str = "auto"
+ style_strength: int = Field(default=60, ge=0, le=100)
+
+
+class PresetResponse(PresetPayload):
+ updated_at: datetime
+
+
+class DashboardStats(BaseModel):
+ queued: int
+ running: int
+ done: int
+ error: int
+ cancelled: int
+ total: int
+ last_24h: int
+
+
+class ExportRequest(BaseModel):
+ source_path: str
+ format: Literal["png", "jpg", "webp"] = "png"
+ quality: int = Field(default=92, ge=1, le=100)
+ max_width: int | None = Field(default=None, ge=64, le=8192)
+ max_height: int | None = Field(default=None, ge=64, le=8192)
+
+
+class ExportResponse(BaseModel):
+ output_path: str
+
+
+class AdminSettings(BaseModel):
+ content_profile: str
+ rate_limit_per_minute: int
+ output_retention_days: int
+ adult_enabled: bool = False
+
+
+class MetricsResponse(BaseModel):
+ metrics: dict[str, float | int]
diff --git a/imageforge/backend/app/core/__init__.py b/imageforge/backend/app/core/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/app/core/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/core/config.py b/imageforge/backend/app/core/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..bc4aad9f303ac6ebacb2fb27c4b1992e2147c8d0
--- /dev/null
+++ b/imageforge/backend/app/core/config.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+BASE_DIR = Path(__file__).resolve().parents[3]
+AI_CACHE_ROOT = Path(os.getenv("IMAGEFORGE_CACHE_ROOT", str(BASE_DIR / ".cache"))).resolve()
+AI_TMP_ROOT = Path(os.getenv("IMAGEFORGE_TMP_ROOT", str(AI_CACHE_ROOT / "tmp"))).resolve()
+
+for directory in [
+ AI_CACHE_ROOT,
+ AI_CACHE_ROOT / "hf",
+ AI_CACHE_ROOT / "hf" / "transformers",
+ AI_CACHE_ROOT / "torch",
+ AI_CACHE_ROOT / "pip",
+ AI_TMP_ROOT,
+]:
+ directory.mkdir(parents=True, exist_ok=True)
+
+os.environ.setdefault("HF_HOME", str(AI_CACHE_ROOT / "hf"))
+os.environ.setdefault("TRANSFORMERS_CACHE", str(AI_CACHE_ROOT / "hf" / "transformers"))
+os.environ.setdefault("TORCH_HOME", str(AI_CACHE_ROOT / "torch"))
+os.environ.setdefault("PIP_CACHE_DIR", str(AI_CACHE_ROOT / "pip"))
+os.environ.setdefault("TMP", str(AI_TMP_ROOT))
+os.environ.setdefault("TEMP", str(AI_TMP_ROOT))
+
+OUTPUT_DIR = BASE_DIR / "output"
+LOG_FILE = BASE_DIR / "app.log"
+POLICY_AUDIT_FILE = BASE_DIR / "policy_audit.log"
+HISTORY_FILE = BASE_DIR / "prompt_history.json"
+PRESETS_FILE = BASE_DIR / "presets.json"
+JOBS_STATE_FILE = BASE_DIR / "jobs_state.json"
+SETTINGS_FILE = BASE_DIR / "settings.json"
+ADMIN_AUDIT_FILE = BASE_DIR / "admin_audit.log"
+DEFAULT_BACKEND_HOST = os.getenv("IMAGEFORGE_HOST", "127.0.0.1")
+DEFAULT_BACKEND_PORT = int(os.getenv("IMAGEFORGE_PORT", "8008"))
+CORS_ORIGINS = os.getenv("IMAGEFORGE_CORS_ORIGINS", "*")
+CONTENT_PROFILE = os.getenv("IMAGEFORGE_CONTENT_PROFILE", "strict")
+ADMIN_TOKEN = os.getenv("IMAGEFORGE_ADMIN_TOKEN", "")
+API_KEYS = os.getenv("IMAGEFORGE_API_KEYS", "")
+RATE_LIMIT_PER_MINUTE = int(os.getenv("IMAGEFORGE_RATE_LIMIT_PER_MIN", "120"))
+REQUEST_MAX_BYTES = int(os.getenv("IMAGEFORGE_REQUEST_MAX_BYTES", str(2 * 1024 * 1024)))
+OUTPUT_RETENTION_DAYS = int(os.getenv("IMAGEFORGE_OUTPUT_RETENTION_DAYS", "30"))
+DASHBOARD_REFRESH_SECONDS = int(os.getenv("IMAGEFORGE_DASHBOARD_REFRESH_SECONDS", "2"))
+JOB_TIMEOUT_SECONDS = int(os.getenv("IMAGEFORGE_JOB_TIMEOUT_SECONDS", "180"))
+A1111_BASE_URL = os.getenv("IMAGEFORGE_A1111_BASE_URL", "http://127.0.0.1:7860")
+A1111_TIMEOUT_SECONDS = int(os.getenv("IMAGEFORGE_A1111_TIMEOUT_SECONDS", "180"))
+A1111_HEALTH_ENDPOINT = os.getenv("IMAGEFORGE_A1111_HEALTH_ENDPOINT", "/sdapi/v1/sd-models")
+A1111_RETRY_COUNT = int(os.getenv("IMAGEFORGE_A1111_RETRY_COUNT", "2"))
+A1111_RETRY_BACKOFF_SECONDS = float(os.getenv("IMAGEFORGE_A1111_RETRY_BACKOFF_SECONDS", "1.0"))
+A1111_API_AUTH = os.getenv("IMAGEFORGE_A1111_API_AUTH", "")
+A1111_API_USER = os.getenv("IMAGEFORGE_A1111_API_USER", "")
+A1111_API_PASSWORD = os.getenv("IMAGEFORGE_A1111_API_PASSWORD", "")
diff --git a/imageforge/backend/app/core/logging.py b/imageforge/backend/app/core/logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5f1df9c2df5f7a711d65e1a83a9803b63d6f779
--- /dev/null
+++ b/imageforge/backend/app/core/logging.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+import logging
+from logging.handlers import RotatingFileHandler
+
+from .config import LOG_FILE
+
+
+def setup_logging() -> None:
+ LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
+ root = logging.getLogger()
+ if root.handlers:
+ return
+
+ root.setLevel(logging.INFO)
+ formatter = logging.Formatter(
+ "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
+ )
+
+ file_handler = RotatingFileHandler(LOG_FILE, maxBytes=1_000_000, backupCount=3)
+ file_handler.setFormatter(formatter)
+ root.addHandler(file_handler)
+
+ stream_handler = logging.StreamHandler()
+ stream_handler.setFormatter(formatter)
+ root.addHandler(stream_handler)
diff --git a/imageforge/backend/app/core/observability.py b/imageforge/backend/app/core/observability.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0d6955415eebe39579b790a624419df54bb4ac3
--- /dev/null
+++ b/imageforge/backend/app/core/observability.py
@@ -0,0 +1,38 @@
+from __future__ import annotations
+
+import threading
+from collections import defaultdict
+
+
+class MetricsStore:
+ def __init__(self) -> None:
+ self._lock = threading.Lock()
+ self._counters: dict[str, int] = defaultdict(int)
+ self._timings_ms_sum: dict[str, float] = defaultdict(float)
+ self._timings_count: dict[str, int] = defaultdict(int)
+
+ def incr(self, key: str, delta: int = 1) -> None:
+ with self._lock:
+ self._counters[key] += delta
+
+ def observe_ms(self, key: str, value_ms: float) -> None:
+ with self._lock:
+ self._timings_ms_sum[key] += value_ms
+ self._timings_count[key] += 1
+
+ def snapshot(self) -> dict[str, float | int]:
+ with self._lock:
+ out: dict[str, float | int] = dict(self._counters)
+ for key, total in self._timings_ms_sum.items():
+ count = max(1, self._timings_count[key])
+ out[f"{key}_avg_ms"] = total / count
+ out[f"{key}_count"] = self._timings_count[key]
+ return out
+
+ def to_prometheus(self) -> str:
+ data = self.snapshot()
+ lines = ["# TYPE imageforge_metric gauge"]
+ for key, value in sorted(data.items()):
+ prom_name = "imageforge_" + key.replace("-", "_").replace("/", "_").replace(".", "_")
+ lines.append(f"{prom_name} {value}")
+ return "\n".join(lines) + "\n"
diff --git a/imageforge/backend/app/core/policy.py b/imageforge/backend/app/core/policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..51cc6f2b5005fa23739e3b44f90b98949103c7d0
--- /dev/null
+++ b/imageforge/backend/app/core/policy.py
@@ -0,0 +1,117 @@
+from __future__ import annotations
+
+import json
+import re
+from dataclasses import dataclass
+from datetime import datetime, timezone
+
+from .config import POLICY_AUDIT_FILE
+
+
+HARD_BLOCK_PATTERNS = [
+ r"\bminor\b",
+ r"\bunderage\b",
+ r"\bchild\b",
+ r"\bteen\s*sex\b",
+ r"\brape\b",
+ r"\bsexual\s*assault\b",
+ r"\bnon\s*-?consensual\b",
+ r"\bforced\s*sex\b",
+ r"\bincest\b",
+ r"\bbestiality\b",
+]
+
+ADULT_SEXUAL_PATTERNS = [
+ r"\berotic\b",
+ r"\bsensual\b",
+ r"\bnude\b",
+ r"\bnudity\b",
+ r"\bsexy\b",
+ r"\blingerie\b",
+ r"\badult\b",
+]
+
+EXPLICIT_PATTERNS = [
+ r"\bporn\b",
+ r"\bhardcore\b",
+ r"\bexplicit\s*sex\b",
+ r"\bpenetration\b",
+ r"\bblowjob\b",
+ r"\bsex\s*act\b",
+]
+
+
+@dataclass(slots=True)
+class PolicyDecision:
+ allowed: bool
+ reason: str
+ matched: list[str]
+
+
+class ContentPolicy:
+ def __init__(self, profile: str) -> None:
+ self.profile = profile.strip().lower() or "strict"
+
+ def evaluate(self, text: str, admin_override: bool) -> PolicyDecision:
+ combined = text.lower()
+ hard_hits = _find_matches(combined, HARD_BLOCK_PATTERNS)
+ if hard_hits:
+ return PolicyDecision(False, "blocked_illegal_content", hard_hits)
+
+ sexual_hits = _find_matches(combined, ADULT_SEXUAL_PATTERNS)
+ explicit_hits = _find_matches(combined, EXPLICIT_PATTERNS)
+
+ if self.profile == "strict":
+ if (sexual_hits or explicit_hits) and not admin_override:
+ return PolicyDecision(False, "blocked_sexual_in_strict_profile", sexual_hits + explicit_hits)
+ if (sexual_hits or explicit_hits) and admin_override:
+ return PolicyDecision(True, "allowed_admin_override", sexual_hits + explicit_hits)
+
+ if self.profile == "internal-relaxed":
+ if explicit_hits and not admin_override:
+ return PolicyDecision(False, "explicit_requires_admin_override", explicit_hits)
+ if explicit_hits and admin_override:
+ return PolicyDecision(True, "allowed_explicit_admin_override", explicit_hits)
+ if sexual_hits:
+ return PolicyDecision(True, "allowed_adult_sexual_relaxed", sexual_hits)
+
+ return PolicyDecision(True, "allowed", [])
+
+
+class PolicyAuditStore:
+ def write(
+ self,
+ *,
+ prompt: str,
+ negative_prompt: str,
+ profile: str,
+ decision: PolicyDecision,
+ client_ip: str,
+ model: str,
+ admin_override_requested: bool,
+ admin_override_applied: bool,
+ ) -> None:
+ POLICY_AUDIT_FILE.parent.mkdir(parents=True, exist_ok=True)
+ entry = {
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "profile": profile,
+ "allowed": decision.allowed,
+ "reason": decision.reason,
+ "matched": decision.matched,
+ "model": model,
+ "client_ip": client_ip,
+ "admin_override_requested": admin_override_requested,
+ "admin_override_applied": admin_override_applied,
+ "prompt_preview": prompt[:180],
+ "negative_prompt_preview": negative_prompt[:180],
+ }
+ with POLICY_AUDIT_FILE.open("a", encoding="utf-8") as fp:
+ fp.write(json.dumps(entry, ensure_ascii=True) + "\n")
+
+
+def _find_matches(text: str, patterns: list[str]) -> list[str]:
+ matches: list[str] = []
+ for pattern in patterns:
+ if re.search(pattern, text, flags=re.IGNORECASE):
+ matches.append(pattern)
+ return matches
diff --git a/imageforge/backend/app/core/prompting.py b/imageforge/backend/app/core/prompting.py
new file mode 100644
index 0000000000000000000000000000000000000000..77d32a5ada3c63261851d41424cd7430b9661044
--- /dev/null
+++ b/imageforge/backend/app/core/prompting.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+
+_IMAGE_TYPE_HINTS: dict[str, str] = {
+ "general": "high quality image",
+ "photo": "real-world photography, natural lighting, realistic textures",
+ "portrait": "portrait photography, detailed face, sharp eyes, skin texture",
+ "landscape": "wide landscape composition, depth, atmospheric perspective",
+ "architecture": "architectural photography, perspective lines, material details",
+ "product": "studio product shot, clean background, commercial lighting",
+ "logo": "minimal vector logo, clean geometry, high contrast",
+ "icon": "flat app icon design, simple silhouette, readable at small size",
+ "poster": "poster design, bold composition, impactful typography space",
+ "illustration": "digital illustration, clean linework, cohesive color palette",
+ "anime": "anime style illustration, expressive line art, cel shading",
+ "pixel_art": "pixel art style, crisp pixels, limited palette",
+ "sketch": "pencil sketch, rough strokes, paper texture",
+ "painting": "painterly artwork, brush strokes, rich color blending",
+ "3d": "3d render style, global illumination, physically based materials",
+}
+
+_STYLE_HINTS: dict[str, str] = {
+ "auto": "",
+ "photorealistic": "photorealistic, ultra detailed, natural color grading",
+ "cinematic": "cinematic lighting, dramatic framing, filmic color",
+ "minimal": "minimalist design, clean shapes, negative space",
+ "vibrant": "vivid colors, high saturation, energetic contrast",
+ "monochrome": "monochrome palette, tonal depth, strong value structure",
+ "watercolor": "watercolor texture, soft pigment edges",
+ "oil": "oil painting texture, impasto details",
+ "noir": "film noir, moody shadows, high contrast",
+ "fantasy": "fantasy art, epic atmosphere, imaginative details",
+}
+
+_BASE_NEGATIVE = (
+ "low quality, blurry, noisy, jpeg artifacts, deformed anatomy, bad composition, "
+ "watermark, signature, text clutter"
+)
+
+
+@dataclass(slots=True)
+class PromptCompileResult:
+ prompt: str
+ negative_prompt: str
+
+
+def compile_prompts(
+ prompt: str,
+ negative_prompt: str,
+ image_type: str,
+ style_preset: str,
+ style_strength: int,
+) -> PromptCompileResult:
+ type_hint = _IMAGE_TYPE_HINTS.get(image_type, _IMAGE_TYPE_HINTS["general"])
+ style_hint = _STYLE_HINTS.get(style_preset, "")
+
+ strength = max(0, min(100, style_strength))
+ style_weight = ""
+ if style_hint:
+ style_weight = f" style influence {strength}%: {style_hint}."
+
+ compiled_prompt = f"{prompt.strip()}. Target: {type_hint}.{style_weight}".strip()
+
+ merged_negative = _BASE_NEGATIVE
+ if negative_prompt.strip():
+ merged_negative = f"{negative_prompt.strip()}, {merged_negative}"
+
+ return PromptCompileResult(prompt=compiled_prompt, negative_prompt=merged_negative)
diff --git a/imageforge/backend/app/core/security.py b/imageforge/backend/app/core/security.py
new file mode 100644
index 0000000000000000000000000000000000000000..91637bf0977c91d3d4a46f50b2713521edcd1a74
--- /dev/null
+++ b/imageforge/backend/app/core/security.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+import time
+from collections import defaultdict, deque
+from dataclasses import dataclass
+
+from fastapi import HTTPException
+
+from .config import API_KEYS, RATE_LIMIT_PER_MINUTE
+
+
+@dataclass(slots=True)
+class Principal:
+ client_id: str
+ role: str
+ api_key: str | None
+
+
+class ApiSecurity:
+ def __init__(self) -> None:
+ self.required_keys: dict[str, str] = {}
+ for raw in [item.strip() for item in API_KEYS.split(",") if item.strip()]:
+ if ":" in raw:
+ key, role = raw.split(":", maxsplit=1)
+ self.required_keys[key.strip()] = role.strip() or "operator"
+ else:
+ self.required_keys[raw] = "operator"
+ self.limit = RATE_LIMIT_PER_MINUTE
+ self._hits: dict[str, deque[float]] = defaultdict(deque)
+
+ def authenticate(self, api_key: str | None, client_id: str) -> Principal:
+ role = "operator"
+ if self.required_keys:
+ if not api_key or api_key not in self.required_keys:
+ raise HTTPException(status_code=401, detail="Missing or invalid API key")
+ role = self.required_keys[api_key]
+ self._check_rate_limit(client_id)
+ return Principal(client_id=client_id, role=role, api_key=api_key)
+
+ @staticmethod
+ def require_role(principal: Principal, minimum_role: str) -> None:
+ rank = {"viewer": 1, "operator": 2, "admin": 3}
+ if rank.get(principal.role, 0) < rank.get(minimum_role, 0):
+ raise HTTPException(status_code=403, detail=f"Role '{minimum_role}' required")
+
+ def _check_rate_limit(self, client_id: str) -> None:
+ if client_id in {"127.0.0.1", "::1", "localhost"}:
+ return
+ now = time.time()
+ q = self._hits[client_id]
+ window_start = now - 60.0
+ while q and q[0] < window_start:
+ q.popleft()
+ if len(q) >= self.limit:
+ raise HTTPException(status_code=429, detail="Rate limit exceeded")
+ q.append(now)
diff --git a/imageforge/backend/app/jobs/__init__.py b/imageforge/backend/app/jobs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/app/jobs/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/jobs/manager.py b/imageforge/backend/app/jobs/manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..3aad8d295f6d601109bad668a105b94b9eef8430
--- /dev/null
+++ b/imageforge/backend/app/jobs/manager.py
@@ -0,0 +1,455 @@
+from __future__ import annotations
+
+import hashlib
+import json
+import logging
+import queue
+import threading
+import time
+import uuid
+import os
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from pathlib import Path
+import random
+
+from ..api.schemas import GenerateRequest, JobStatus
+from ..core.config import JOBS_STATE_FILE, JOB_TIMEOUT_SECONDS, OUTPUT_DIR
+from ..core.prompting import compile_prompts
+from ..providers.factory import ProviderRegistry
+from ..providers.interface import ProviderRequest, ProviderResult, ProviderUnavailableError
+from ..storage.history import PromptHistoryStore
+
+LOGGER = logging.getLogger(__name__)
+
+
+@dataclass
+class JobState:
+ job_id: str
+ request: GenerateRequest
+ status: JobStatus = JobStatus.QUEUED
+ progress: int = 0
+ message: str = "Queued"
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
+ updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
+ image_paths: list[str] = field(default_factory=list)
+ output_dir: str | None = None
+ error: str | None = None
+ cancel_requested: bool = False
+
+
+class JobManager:
+ def __init__(
+ self,
+ provider_registry: ProviderRegistry,
+ history_store: PromptHistoryStore,
+ state_file: Path | None = None,
+ ) -> None:
+ self.provider_registry = provider_registry
+ self.history_store = history_store
+ self._state_file = state_file or JOBS_STATE_FILE
+ self._jobs: dict[str, JobState] = {}
+ self._queue: queue.Queue[str] = queue.Queue()
+ self._lock = threading.RLock()
+ self._state_write_lock = threading.Lock()
+ self._worker = threading.Thread(target=self._worker_loop, daemon=True)
+ self._load_state()
+ self._worker.start()
+
+ def submit(self, request: GenerateRequest) -> str:
+ job_id = uuid.uuid4().hex[:12]
+ state = JobState(job_id=job_id, request=request)
+ with self._lock:
+ self._jobs[job_id] = state
+ self._queue.put(job_id)
+ self.history_store.add(request.prompt, request.negative_prompt)
+ self._save_state()
+ LOGGER.info("Job %s queued with model %s", job_id, request.model)
+ return job_id
+
+ def get(self, job_id: str) -> JobState | None:
+ with self._lock:
+ return self._jobs.get(job_id)
+
+ def list(self) -> list[JobState]:
+ with self._lock:
+ return sorted(self._jobs.values(), key=lambda item: item.created_at, reverse=True)
+
+ def retry(self, job_id: str) -> str | None:
+ with self._lock:
+ state = self._jobs.get(job_id)
+ if state is None:
+ return None
+ return self.submit(state.request)
+
+ def stats(self) -> dict[str, int]:
+ with self._lock:
+ items = list(self._jobs.values())
+ now = datetime.now(timezone.utc)
+ counts = {status.value: 0 for status in JobStatus}
+ last_24h = 0
+ for item in items:
+ counts[item.status.value] += 1
+ if (now - item.created_at).total_seconds() <= 86400:
+ last_24h += 1
+ return {
+ "queued": counts["queued"],
+ "running": counts["running"],
+ "done": counts["done"],
+ "error": counts["error"],
+ "cancelled": counts["cancelled"],
+ "total": len(items),
+ "last_24h": last_24h,
+ }
+
+ def cancel(self, job_id: str) -> bool:
+ with self._lock:
+ state = self._jobs.get(job_id)
+ if state is None:
+ return False
+ state.cancel_requested = True
+ if state.status == JobStatus.QUEUED:
+ state.status = JobStatus.CANCELLED
+ state.message = "Cancelled"
+ state.updated_at = datetime.now(timezone.utc)
+ self._save_state()
+ return True
+
+ def _update(self, job_id: str, **updates) -> None: # noqa: ANN003
+ with self._lock:
+ state = self._jobs.get(job_id)
+ if state is None:
+ return
+ for key, value in updates.items():
+ setattr(state, key, value)
+ state.updated_at = datetime.now(timezone.utc)
+ self._save_state()
+
+ def _worker_loop(self) -> None:
+ while True:
+ job_id = self._queue.get()
+ state = self.get(job_id)
+ if state is None:
+ continue
+ if state.status == JobStatus.CANCELLED:
+ continue
+ self._run_job(job_id, state)
+
+ def _run_job(self, job_id: str, state: JobState) -> None:
+ self._update(job_id, status=JobStatus.RUNNING, progress=1, message="Starting generation")
+ date_dir = datetime.now(timezone.utc).strftime("%Y-%m-%d")
+ output_dir = OUTPUT_DIR / date_dir / f"job_{job_id}"
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ try:
+ width, height = self._parse_size(state.request.size)
+ seed = state.request.seed
+ if state.request.random_seed or seed is None:
+ seed = random.randint(0, 2**31 - 1)
+ if state.request.init_image_path:
+ init_path = Path(state.request.init_image_path)
+ if not init_path.exists() or not init_path.is_file():
+ raise ValueError("init_image_path does not exist or is not a file")
+ compiled = compile_prompts(
+ prompt=state.request.prompt,
+ negative_prompt=state.request.negative_prompt,
+ image_type=state.request.image_type,
+ style_preset=state.request.style_preset,
+ style_strength=state.request.style_strength,
+ )
+
+ provider_request = ProviderRequest(
+ prompt=compiled.prompt,
+ negative_prompt=compiled.negative_prompt,
+ count=state.request.count,
+ width=width,
+ height=height,
+ seed=seed,
+ steps=state.request.steps,
+ guidance=state.request.guidance,
+ init_image_path=state.request.init_image_path,
+ img2img_strength=state.request.img2img_strength,
+ model_variant=state.request.model_variant,
+ )
+
+ def progress_cb(progress: int, message: str) -> None:
+ self._update(job_id, progress=max(1, min(99, progress)), message=message)
+
+ def is_cancelled() -> bool:
+ current = self.get(job_id)
+ return bool(current and current.cancel_requested)
+ selected_provider_id, result = self._generate_with_fallback(
+ requested_model=state.request.model,
+ request=provider_request,
+ output_dir=output_dir,
+ progress_cb=progress_cb,
+ is_cancelled=is_cancelled,
+ )
+ if is_cancelled():
+ self._update(job_id, status=JobStatus.CANCELLED, progress=0, message="Cancelled")
+ return
+ image_paths = [str(path.resolve()) for path in result.image_paths]
+ meta = {
+ "prompt": state.request.prompt,
+ "compiled_prompt": compiled.prompt,
+ "neg_prompt": state.request.negative_prompt,
+ "compiled_neg_prompt": compiled.negative_prompt,
+ "seed": seed,
+ "model": state.request.model,
+ "size": state.request.size,
+ "count": state.request.count,
+ "steps": state.request.steps,
+ "guidance": state.request.guidance,
+ "image_type": state.request.image_type,
+ "style_preset": state.request.style_preset,
+ "style_strength": state.request.style_strength,
+ "init_image_path": state.request.init_image_path,
+ "img2img_strength": state.request.img2img_strength,
+ "model_variant": state.request.model_variant,
+ "requested_model": state.request.model,
+ "used_provider": selected_provider_id,
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "images": image_paths,
+ }
+ stable_blob = json.dumps(
+ {
+ "compiled_prompt": compiled.prompt,
+ "compiled_neg_prompt": compiled.negative_prompt,
+ "model": selected_provider_id,
+ "size": state.request.size,
+ "steps": state.request.steps,
+ "guidance": state.request.guidance,
+ "seed": seed,
+ },
+ sort_keys=True,
+ ).encode("utf-8")
+ meta["config_hash"] = hashlib.sha256(stable_blob).hexdigest()
+ meta_path = output_dir / "meta.json"
+ meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
+ self._update(
+ job_id,
+ status=JobStatus.DONE,
+ progress=100,
+ message="Generation complete",
+ image_paths=image_paths,
+ output_dir=str(output_dir.resolve()),
+ )
+ LOGGER.info("Job %s completed with %s image(s)", job_id, len(image_paths))
+ self._save_state()
+ except Exception as exc: # noqa: BLE001
+ LOGGER.exception("Job %s failed", job_id)
+ self._update(
+ job_id,
+ status=JobStatus.ERROR,
+ progress=0,
+ message="Generation failed",
+ error=str(exc),
+ output_dir=str(output_dir.resolve()),
+ )
+ self._save_state()
+
+ def _generate_with_fallback(
+ self,
+ requested_model: str,
+ request: ProviderRequest,
+ output_dir: Path,
+ progress_cb,
+ is_cancelled,
+ ) -> tuple[str, ProviderResult]:
+ candidates = self._candidate_models(requested_model)
+ failures: list[str] = []
+
+ for idx, model_id in enumerate(candidates):
+ provider = self.provider_registry.get(model_id)
+ if not provider.is_available():
+ failures.append(f"{model_id}: unavailable")
+ continue
+
+ adjusted_request = request
+ if idx > 0:
+ adjusted_request = self._adjust_request_for_fallback(model_id, request)
+ progress_cb(2, f"Fallback to '{model_id}'")
+ else:
+ progress_cb(2, f"Using provider '{model_id}'")
+
+ try:
+ result = self._generate_with_timeout(
+ provider=provider,
+ request=adjusted_request,
+ output_dir=output_dir,
+ progress_cb=progress_cb,
+ is_cancelled=is_cancelled,
+ timeout_seconds=self._timeout_for_attempt(idx),
+ )
+ if getattr(result, "image_paths", None):
+ return model_id, result
+ failures.append(f"{model_id}: produced no images")
+ except Exception as exc: # noqa: BLE001
+ failures.append(f"{model_id}: {exc}")
+
+ raise ProviderUnavailableError(
+ "No provider could generate images. Attempts: " + " | ".join(failures)
+ )
+
+ def _candidate_models(self, requested_model: str) -> list[str]:
+ known = {provider.id for provider in self.provider_registry.list()}
+ if requested_model not in known:
+ raise KeyError(f"Unknown provider: {requested_model}")
+
+ auto_fallback = os.getenv("IMAGEFORGE_ENABLE_AUTO_FALLBACK", "1") == "1"
+ if not auto_fallback:
+ return [requested_model]
+
+ # Try localai and diffusion first (real models), fallback to dummy if needed
+ fallback_raw = os.getenv("IMAGEFORGE_FALLBACK_MODELS", "localai,diffusion,a1111,dummy")
+ fallback_order = [part.strip() for part in fallback_raw.split(",") if part.strip()]
+
+ models: list[str] = [requested_model]
+ for model_id in fallback_order:
+ if model_id in known and model_id not in models:
+ models.append(model_id)
+ return models
+
+ @staticmethod
+ def _timeout_for_attempt(attempt_index: int) -> int:
+ if attempt_index == 0:
+ return JOB_TIMEOUT_SECONDS
+ fallback_timeout = int(os.getenv("IMAGEFORGE_FALLBACK_TIMEOUT_SECONDS", "90"))
+ return max(10, fallback_timeout)
+
+ @staticmethod
+ def _adjust_request_for_fallback(model_id: str, request: ProviderRequest) -> ProviderRequest:
+ max_steps = int(os.getenv("IMAGEFORGE_FALLBACK_MAX_STEPS", "24"))
+ capped_steps = max(1, min(request.steps, max_steps))
+ capped_guidance = min(request.guidance, 8.0)
+ max_side = int(os.getenv("IMAGEFORGE_FALLBACK_MAX_SIDE", "768"))
+ width = min(request.width, max_side)
+ height = min(request.height, max_side)
+ width = max(64, (width // 8) * 8)
+ height = max(64, (height // 8) * 8)
+ return ProviderRequest(
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ count=request.count,
+ width=width,
+ height=height,
+ seed=request.seed,
+ steps=capped_steps,
+ guidance=capped_guidance,
+ init_image_path=request.init_image_path,
+ img2img_strength=request.img2img_strength,
+ model_variant=request.model_variant if model_id in {"a1111", "localai", "diffusion"} else None,
+ )
+
+ @staticmethod
+ def _parse_size(size: str) -> tuple[int, int]:
+ try:
+ width_raw, height_raw = size.split("x", maxsplit=1)
+ width = int(width_raw)
+ height = int(height_raw)
+ except Exception as exc: # noqa: BLE001
+ raise ValueError(f"Invalid size format: {size}") from exc
+ if (width, height) not in {(512, 512), (768, 768), (1024, 1024), (1024, 1536), (1536, 1024)}:
+ raise ValueError(f"Unsupported size: {size}")
+ return width, height
+
+ @staticmethod
+ def _generate_with_timeout(provider, request, output_dir, progress_cb, is_cancelled, timeout_seconds): # noqa: ANN001
+ result_holder: dict[str, object] = {}
+ error_holder: dict[str, Exception] = {}
+
+ def _run() -> None:
+ try:
+ result_holder["value"] = provider.generate(request, output_dir, progress_cb, is_cancelled)
+ except Exception as exc: # noqa: BLE001
+ error_holder["error"] = exc
+
+ worker = threading.Thread(target=_run, daemon=True)
+ worker.start()
+
+ started = time.monotonic()
+ while worker.is_alive():
+ if is_cancelled():
+ break
+ if timeout_seconds > 0 and (time.monotonic() - started) > timeout_seconds:
+ raise TimeoutError(
+ f"Generation timed out after {timeout_seconds}s. "
+ "Try model 'dummy' or reduce steps/size."
+ )
+ worker.join(timeout=0.5)
+
+ if "error" in error_holder:
+ raise error_holder["error"]
+ if "value" not in result_holder:
+ raise RuntimeError("Generation was interrupted before producing a result")
+ return result_holder["value"]
+
+ def _save_state(self) -> None:
+ rows = []
+ with self._lock:
+ for state in self._jobs.values():
+ rows.append(
+ {
+ "job_id": state.job_id,
+ "request": state.request.model_dump(),
+ "status": state.status.value,
+ "progress": state.progress,
+ "message": state.message,
+ "created_at": state.created_at.isoformat(),
+ "updated_at": state.updated_at.isoformat(),
+ "image_paths": state.image_paths,
+ "output_dir": state.output_dir,
+ "error": state.error,
+ "cancel_requested": state.cancel_requested,
+ }
+ )
+ self._state_file.parent.mkdir(parents=True, exist_ok=True)
+ payload = json.dumps(rows, indent=2)
+
+ with self._state_write_lock:
+ tmp_path = self._state_file.with_suffix(f"{self._state_file.suffix}.{uuid.uuid4().hex}.tmp")
+ try:
+ tmp_path.write_text(payload, encoding="utf-8")
+ tmp_path.replace(self._state_file)
+ except OSError as exc:
+ LOGGER.warning("State persistence failed (%s). Continuing without crash.", exc)
+ try:
+ self._state_file.write_text(payload, encoding="utf-8")
+ except OSError as fallback_exc:
+ LOGGER.warning("Direct state write failed (%s).", fallback_exc)
+ finally:
+ tmp_path.unlink(missing_ok=True)
+
+ def _load_state(self) -> None:
+ if not self._state_file.exists():
+ return
+ try:
+ rows = json.loads(self._state_file.read_text(encoding="utf-8"))
+ except json.JSONDecodeError:
+ return
+ if not isinstance(rows, list):
+ return
+ for row in rows:
+ try:
+ req = GenerateRequest(**row["request"])
+ status = JobStatus(row.get("status", "error"))
+ # Recover queued/running jobs as error on restart to avoid silent loss.
+ if status in {JobStatus.QUEUED, JobStatus.RUNNING}:
+ status = JobStatus.ERROR
+ row["error"] = "Recovered after restart during unfinished execution"
+ state = JobState(
+ job_id=row["job_id"],
+ request=req,
+ status=status,
+ progress=int(row.get("progress", 0)),
+ message=row.get("message", ""),
+ created_at=datetime.fromisoformat(row["created_at"]),
+ updated_at=datetime.fromisoformat(row["updated_at"]),
+ image_paths=list(row.get("image_paths", [])),
+ output_dir=row.get("output_dir"),
+ error=row.get("error"),
+ cancel_requested=bool(row.get("cancel_requested", False)),
+ )
+ self._jobs[state.job_id] = state
+ except Exception: # noqa: BLE001
+ continue
diff --git a/imageforge/backend/app/local_ai/__init__.py b/imageforge/backend/app/local_ai/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/app/local_ai/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/local_ai/engine.py b/imageforge/backend/app/local_ai/engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..007a97f887472d15ca62136b19abd4bea857722b
--- /dev/null
+++ b/imageforge/backend/app/local_ai/engine.py
@@ -0,0 +1,129 @@
+from __future__ import annotations
+
+import logging
+import os
+from dataclasses import dataclass
+
+LOGGER = logging.getLogger(__name__)
+
+# Lazy imports to avoid torch loading issues on Windows
+torch = None
+StableDiffusionImg2ImgPipeline = None
+StableDiffusionPipeline = None
+
+def _ensure_imports():
+ global torch, StableDiffusionImg2ImgPipeline, StableDiffusionPipeline
+ if torch is not None:
+ return
+ try:
+ import torch as _torch
+ from diffusers import StableDiffusionImg2ImgPipeline as _Img2Img
+ from diffusers import StableDiffusionPipeline as _Pipeline
+ torch = _torch
+ StableDiffusionImg2ImgPipeline = _Img2Img
+ StableDiffusionPipeline = _Pipeline
+ LOGGER.info("✓ torch and diffusers imported successfully")
+ except Exception as exc: # pragma: no cover - optional dependency
+ LOGGER.error("✗ Failed to import torch/diffusers: %s", exc, exc_info=True)
+ pass
+
+
+@dataclass(slots=True)
+class LocalAIRequest:
+ prompt: str
+ negative_prompt: str
+ width: int
+ height: int
+ steps: int
+ guidance: float
+ seed: int
+ init_image_path: str | None = None
+ strength: float = 0.45
+ model_variant: str | None = None
+
+
+class LocalAIEngine:
+ """Self-hosted local generation engine; no external API calls required."""
+
+ def __init__(self) -> None:
+ self.model_id = os.getenv("IMAGEFORGE_LOCALAI_MODEL", "segmind/tiny-sd")
+ self._pipe_t2i = None
+ self._pipe_i2i = None
+
+ def is_available(self) -> bool:
+ _ensure_imports()
+ return StableDiffusionPipeline is not None and torch is not None
+
+ def _ensure(self):
+ _ensure_imports()
+ if not self.is_available():
+ raise RuntimeError(
+ "LocalAI dependencies missing. Install diffusers, torch, transformers, accelerate."
+ )
+ if self._pipe_t2i is None:
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+ dtype = torch.float16 if device == "cuda" else torch.float32
+ local_only = os.getenv("IMAGEFORGE_LOCALAI_LOCAL_ONLY", "0") == "1"
+ LOGGER.info("Loading LocalAI model '%s' on %s", self.model_id, device)
+ try:
+ # FORCE local_files_only=False to allow download if needed
+ pipe = StableDiffusionPipeline.from_pretrained(
+ self.model_id,
+ torch_dtype=dtype,
+ local_files_only=False, # Always allow download
+ use_safetensors=True if "safetensors" in self.model_id else None,
+ )
+ except Exception as exc: # noqa: BLE001
+ LOGGER.error("Failed to load model '%s': %s", self.model_id, exc)
+ raise RuntimeError(
+ f"LocalAI model '{self.model_id}' could not be loaded. Error: {exc}"
+ ) from exc
+ if device == "cuda":
+ pipe = pipe.to(device)
+ if os.getenv("IMAGEFORGE_ENABLE_ATTENTION_SLICING", "1") == "1":
+ pipe.enable_attention_slicing()
+ self._pipe_t2i = pipe
+ if StableDiffusionImg2ImgPipeline is not None:
+ pipe_i2i = StableDiffusionImg2ImgPipeline.from_pretrained(
+ self.model_id,
+ torch_dtype=dtype,
+ local_files_only=local_only,
+ )
+ if device == "cuda":
+ pipe_i2i = pipe_i2i.to(device)
+ if os.getenv("IMAGEFORGE_ENABLE_ATTENTION_SLICING", "1") == "1":
+ pipe_i2i.enable_attention_slicing()
+ self._pipe_i2i = pipe_i2i
+ return self._pipe_t2i
+
+ def generate(self, req: LocalAIRequest):
+ from PIL import Image
+
+ if getattr(req, "model_variant", None) and req.model_variant != self.model_id:
+ self.model_id = req.model_variant
+ self._pipe_t2i = None
+ self._pipe_i2i = None
+ pipe = self._ensure()
+ generator = torch.Generator(device=pipe.device).manual_seed(req.seed)
+ if req.init_image_path and self._pipe_i2i is not None:
+ init_img = Image.open(req.init_image_path).convert("RGB").resize((req.width, req.height))
+ out = self._pipe_i2i(
+ prompt=req.prompt,
+ negative_prompt=req.negative_prompt or None,
+ image=init_img,
+ guidance_scale=req.guidance,
+ num_inference_steps=req.steps,
+ strength=max(0.0, min(1.0, req.strength)),
+ generator=generator,
+ )
+ else:
+ out = pipe(
+ prompt=req.prompt,
+ negative_prompt=req.negative_prompt or None,
+ width=req.width,
+ height=req.height,
+ guidance_scale=req.guidance,
+ num_inference_steps=req.steps,
+ generator=generator,
+ )
+ return out.images[0]
diff --git a/imageforge/backend/app/main.py b/imageforge/backend/app/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..12160a2a0782714ea8cf73e39f9790d35218ce89
--- /dev/null
+++ b/imageforge/backend/app/main.py
@@ -0,0 +1,437 @@
+from __future__ import annotations
+
+import logging
+import time
+from dataclasses import asdict
+from datetime import datetime, timezone
+from pathlib import Path
+
+from fastapi import FastAPI, Header, HTTPException, Query, Request
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import FileResponse, PlainTextResponse
+from PIL import Image
+import uvicorn
+
+from .api.schemas import (
+ AdminSettings,
+ CancelResponse,
+ DashboardStats,
+ ExportRequest,
+ ExportResponse,
+ GenerateRequest,
+ GenerateResponse,
+ HealthResponse,
+ HistoryItem,
+ JobInfoResponse,
+ MetricsResponse,
+ ModelInfo,
+ PresetPayload,
+ PresetResponse,
+ RetryResponse,
+)
+from .core.config import (
+ ADMIN_TOKEN,
+ CONTENT_PROFILE,
+ CORS_ORIGINS,
+ DEFAULT_BACKEND_HOST,
+ DEFAULT_BACKEND_PORT,
+ OUTPUT_DIR,
+ OUTPUT_RETENTION_DAYS,
+ REQUEST_MAX_BYTES,
+)
+from .core.logging import setup_logging
+from .core.observability import MetricsStore
+from .core.policy import ContentPolicy, PolicyAuditStore
+from .core.security import ApiSecurity, Principal
+from .jobs.manager import JobManager
+from .providers.factory import ProviderRegistry
+from .storage.history import PromptHistoryStore
+from .storage.maintenance import cleanup_outputs
+from .storage.presets import PresetStore
+from .storage.settings import SettingsStore
+
+setup_logging()
+LOGGER = logging.getLogger(__name__)
+
+provider_registry = ProviderRegistry()
+history_store = PromptHistoryStore()
+job_manager = JobManager(provider_registry, history_store)
+policy_audit = PolicyAuditStore()
+api_security = ApiSecurity()
+preset_store = PresetStore()
+settings_store = SettingsStore()
+metrics = MetricsStore()
+
+cleanup_removed = cleanup_outputs(OUTPUT_RETENTION_DAYS)
+if cleanup_removed:
+ LOGGER.info("Startup cleanup removed %s old output day folder(s)", cleanup_removed)
+
+app = FastAPI(title="ImageForge Backend", version="0.3.0")
+allow_origins = [item.strip() for item in CORS_ORIGINS.split(",")] if CORS_ORIGINS != "*" else ["*"]
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=allow_origins,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.middleware("http")
+async def request_limits_middleware(request: Request, call_next):
+ length = request.headers.get("content-length")
+ if length and int(length) > REQUEST_MAX_BYTES:
+ raise HTTPException(status_code=413, detail="Request too large")
+ start = time.perf_counter()
+ response = await call_next(request)
+ elapsed_ms = (time.perf_counter() - start) * 1000.0
+ metrics.incr("http_requests_total")
+ metrics.observe_ms("http_request", elapsed_ms)
+ response.headers["X-Content-Type-Options"] = "nosniff"
+ response.headers["X-Frame-Options"] = "DENY"
+ response.headers["Referrer-Policy"] = "no-referrer"
+ response.headers["Content-Security-Policy"] = "default-src 'self'"
+ return response
+
+
+def _principal(
+ http_request: Request,
+ x_imageforge_api_key: str | None,
+ minimum_role: str,
+) -> Principal:
+ api_security.limit = int(settings_store.get().get("rate_limit_per_minute", api_security.limit))
+ principal = api_security.authenticate(
+ api_key=x_imageforge_api_key,
+ client_id=http_request.client.host if http_request.client else "unknown",
+ )
+ api_security.require_role(principal, minimum_role)
+ return principal
+
+
+@app.get("/health", response_model=HealthResponse)
+def health() -> HealthResponse:
+ return HealthResponse(status="ok", timestamp=datetime.now(timezone.utc))
+
+
+@app.get("/ready", response_model=HealthResponse)
+def ready() -> HealthResponse:
+ try:
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+ probe = OUTPUT_DIR / ".ready_probe"
+ probe.write_text("ok", encoding="utf-8")
+ probe.unlink(missing_ok=True)
+ except Exception as exc: # noqa: BLE001
+ raise HTTPException(status_code=503, detail=f"Output directory not writable: {exc}") from exc
+ return HealthResponse(status="ready", timestamp=datetime.now(timezone.utc))
+
+
+@app.get("/metrics", response_model=MetricsResponse)
+def metrics_json(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> MetricsResponse:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ return MetricsResponse(metrics=metrics.snapshot())
+
+
+@app.get("/metrics/prometheus")
+def metrics_prom(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> PlainTextResponse:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ return PlainTextResponse(metrics.to_prometheus())
+
+
+@app.get("/models", response_model=list[ModelInfo])
+def models(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> list[ModelInfo]:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ runtime = settings_store.get()
+ adult_enabled = bool(runtime.get("adult_enabled", False))
+ return [
+ ModelInfo(
+ id=provider.id,
+ name=provider.name,
+ description=provider.description,
+ available=provider.is_available() if provider.id != "zimageturbo" else (adult_enabled and provider.is_available()),
+ )
+ for provider in provider_registry.list()
+ ]
+
+
+@app.post("/generate", response_model=GenerateResponse)
+def generate(
+ request: GenerateRequest,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+ x_imageforge_admin_token: str | None = Header(default=None, alias="X-ImageForge-Admin-Token"),
+) -> GenerateResponse:
+ _principal(http_request, x_imageforge_api_key, "operator")
+ try:
+ provider_registry.get(request.model)
+ except KeyError as exc:
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
+
+ runtime = settings_store.get()
+ if request.model == "zimageturbo" and not bool(runtime.get("adult_enabled", False)):
+ raise HTTPException(status_code=400, detail="Adult provider is disabled in admin settings")
+ policy = ContentPolicy(runtime.get("content_profile", CONTENT_PROFILE))
+
+ admin_override_applied = bool(
+ request.admin_override and ADMIN_TOKEN and x_imageforge_admin_token == ADMIN_TOKEN
+ )
+ decision = policy.evaluate(
+ f"{request.prompt}\n{request.negative_prompt}",
+ admin_override=admin_override_applied,
+ )
+ policy_audit.write(
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ profile=runtime.get("content_profile", CONTENT_PROFILE),
+ decision=decision,
+ client_ip=http_request.client.host if http_request.client else "unknown",
+ model=request.model,
+ admin_override_requested=request.admin_override,
+ admin_override_applied=admin_override_applied,
+ )
+ if not decision.allowed:
+ raise HTTPException(status_code=400, detail=f"Blocked by policy: {decision.reason}")
+
+ metrics.incr("jobs_submitted_total")
+ job_id = job_manager.submit(request)
+ return GenerateResponse(job_id=job_id)
+
+
+@app.get("/jobs/{job_id}", response_model=JobInfoResponse)
+def job_status(
+ job_id: str,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> JobInfoResponse:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ state = job_manager.get(job_id)
+ if state is None:
+ raise HTTPException(status_code=404, detail="Job not found")
+ return _to_job_response(state)
+
+
+@app.get("/jobs", response_model=list[JobInfoResponse])
+def list_jobs(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> list[JobInfoResponse]:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ return [_to_job_response(state) for state in job_manager.list()]
+
+
+@app.get("/image")
+def get_image(path: str = Query(...)) -> FileResponse:
+ try:
+ source = Path(path).resolve(strict=True)
+ except FileNotFoundError as exc:
+ raise HTTPException(status_code=404, detail="Image not found") from exc
+
+ output_root = OUTPUT_DIR.resolve()
+ if source != output_root and output_root not in source.parents:
+ raise HTTPException(status_code=403, detail="Forbidden source path")
+ if not source.is_file():
+ raise HTTPException(status_code=404, detail="Image not found")
+ return FileResponse(source)
+
+
+@app.post("/jobs/{job_id}/retry", response_model=RetryResponse)
+def retry_job(
+ job_id: str,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> RetryResponse:
+ _principal(http_request, x_imageforge_api_key, "operator")
+ new_id = job_manager.retry(job_id)
+ if not new_id:
+ raise HTTPException(status_code=404, detail="Job not found")
+ metrics.incr("jobs_retry_total")
+ return RetryResponse(old_job_id=job_id, new_job_id=new_id)
+
+
+def _to_job_response(state) -> JobInfoResponse: # noqa: ANN001
+ return JobInfoResponse(
+ job_id=state.job_id,
+ status=state.status,
+ progress=state.progress,
+ message=state.message,
+ created_at=state.created_at,
+ updated_at=state.updated_at,
+ image_paths=state.image_paths,
+ output_dir=state.output_dir,
+ error=state.error,
+ )
+
+
+@app.post("/jobs/{job_id}/cancel", response_model=CancelResponse)
+def cancel(
+ job_id: str,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> CancelResponse:
+ _principal(http_request, x_imageforge_api_key, "operator")
+ success = job_manager.cancel(job_id)
+ if not success:
+ raise HTTPException(status_code=404, detail="Job not found")
+ metrics.incr("jobs_cancel_total")
+ return CancelResponse(success=True)
+
+
+@app.get("/history", response_model=list[HistoryItem])
+def history(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> list[HistoryItem]:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ items = history_store.list()
+ parsed: list[HistoryItem] = []
+ for item in items:
+ try:
+ parsed.append(
+ HistoryItem(
+ prompt=item.get("prompt", ""),
+ negative_prompt=item.get("negative_prompt", ""),
+ timestamp=datetime.fromisoformat(item.get("timestamp", "")),
+ )
+ )
+ except Exception: # noqa: BLE001
+ continue
+ return parsed
+
+
+@app.get("/dashboard/stats", response_model=DashboardStats)
+def dashboard_stats(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> DashboardStats:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ return DashboardStats(**job_manager.stats())
+
+
+@app.get("/presets", response_model=list[PresetResponse])
+def list_presets(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> list[PresetResponse]:
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ return [PresetResponse(**asdict(preset)) for preset in preset_store.list()]
+
+
+@app.post("/presets", response_model=PresetResponse)
+def upsert_preset(
+ payload: PresetPayload,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> PresetResponse:
+ _principal(http_request, x_imageforge_api_key, "operator")
+ preset = preset_store.upsert(payload.model_dump())
+ return PresetResponse(**asdict(preset))
+
+
+@app.delete("/presets/{name}", response_model=CancelResponse)
+def delete_preset(
+ name: str,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> CancelResponse:
+ _principal(http_request, x_imageforge_api_key, "admin")
+ ok = preset_store.delete(name)
+ if not ok:
+ raise HTTPException(status_code=404, detail="Preset not found")
+ return CancelResponse(success=True)
+
+
+@app.post("/export", response_model=ExportResponse)
+def export_image(
+ payload: ExportRequest,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> ExportResponse:
+ _principal(http_request, x_imageforge_api_key, "operator")
+ source = Path(payload.source_path).resolve()
+ output_root = OUTPUT_DIR.resolve()
+ if output_root not in source.parents:
+ raise HTTPException(status_code=403, detail="Forbidden source path")
+ if not source.exists() or not source.is_file():
+ raise HTTPException(status_code=404, detail="Source image not found")
+ img = Image.open(source).convert("RGB")
+ if payload.max_width or payload.max_height:
+ max_w = payload.max_width or img.width
+ max_h = payload.max_height or img.height
+ img.thumbnail((max_w, max_h))
+ out_path = source.with_name(f"{source.stem}_export.{payload.format}")
+ save_kwargs: dict[str, int] = {}
+ if payload.format in {"jpg", "webp"}:
+ save_kwargs["quality"] = payload.quality
+ img.save(out_path, format=payload.format.upper(), **save_kwargs)
+ return ExportResponse(output_path=str(out_path.resolve()))
+
+
+@app.get("/admin/settings", response_model=AdminSettings)
+def get_admin_settings(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> AdminSettings:
+ principal = _principal(http_request, x_imageforge_api_key, "admin")
+ current = settings_store.get()
+ metrics.incr(f"admin_settings_read_by_{principal.role}")
+ return AdminSettings(**current)
+
+
+@app.put("/admin/settings", response_model=AdminSettings)
+def put_admin_settings(
+ payload: AdminSettings,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> AdminSettings:
+ principal = _principal(http_request, x_imageforge_api_key, "admin")
+ current = settings_store.update(payload.model_dump(), actor=principal.client_id)
+ return AdminSettings(**current)
+
+
+@app.post("/admin/cleanup", response_model=DashboardStats)
+def cleanup_endpoint(
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+) -> DashboardStats:
+ _principal(http_request, x_imageforge_api_key, "admin")
+ runtime = settings_store.get()
+ cleanup_outputs(int(runtime.get("output_retention_days", OUTPUT_RETENTION_DAYS)))
+ return DashboardStats(**job_manager.stats())
+
+
+@app.get("/files/{relative_path:path}")
+def output_file(
+ relative_path: str,
+ http_request: Request,
+ x_imageforge_api_key: str | None = Header(default=None, alias="X-ImageForge-Api-Key"),
+):
+ _principal(http_request, x_imageforge_api_key, "viewer")
+ path = (Path.cwd() / relative_path).resolve()
+ output_root = OUTPUT_DIR.resolve()
+ if output_root not in path.parents and path != output_root:
+ raise HTTPException(status_code=403, detail="Forbidden")
+ if not path.exists() or not path.is_file():
+ raise HTTPException(status_code=404, detail="File not found")
+ return FileResponse(path)
+
+
+def run() -> None:
+ uvicorn.run(
+ "backend.app.main:app",
+ host=DEFAULT_BACKEND_HOST,
+ port=DEFAULT_BACKEND_PORT,
+ reload=False,
+ log_level="info",
+ )
+
+
+if __name__ == "__main__":
+ LOGGER.info("Starting ImageForge backend")
+ run()
diff --git a/imageforge/backend/app/providers/__init__.py b/imageforge/backend/app/providers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/app/providers/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/providers/a1111_provider.py b/imageforge/backend/app/providers/a1111_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..843d869e61776c7abce66a913c41613ac29b527e
--- /dev/null
+++ b/imageforge/backend/app/providers/a1111_provider.py
@@ -0,0 +1,166 @@
+from __future__ import annotations
+
+import base64
+import json
+from pathlib import Path
+import time
+from urllib import error, request
+
+from ..core.config import (
+ A1111_API_AUTH,
+ A1111_API_PASSWORD,
+ A1111_API_USER,
+ A1111_BASE_URL,
+ A1111_HEALTH_ENDPOINT,
+ A1111_RETRY_BACKOFF_SECONDS,
+ A1111_RETRY_COUNT,
+ A1111_TIMEOUT_SECONDS,
+)
+from .interface import ProviderRequest, ProviderResult, ProviderUnavailableError
+
+
+class A1111Provider:
+ id = "a1111"
+ name = "AUTOMATIC1111"
+ description = "Uses AUTOMATIC1111 Stable Diffusion WebUI API"
+
+ def __init__(self) -> None:
+ self.base_url = A1111_BASE_URL.rstrip("/")
+ self.timeout = A1111_TIMEOUT_SECONDS
+ self.health_endpoint = self._normalize_endpoint(A1111_HEALTH_ENDPOINT)
+ self.retry_count = max(0, A1111_RETRY_COUNT)
+ self.retry_backoff_seconds = max(0.0, A1111_RETRY_BACKOFF_SECONDS)
+ self.auth_header = self._resolve_auth_header()
+
+ def is_available(self) -> bool:
+ try:
+ self._request_json("GET", self.health_endpoint, retries=0)
+ return True
+ except Exception:
+ return False
+
+ def generate(self, request_data: ProviderRequest, output_dir: Path, progress, is_cancelled) -> ProviderResult:
+ if not self.is_available():
+ raise ProviderUnavailableError(
+ "AUTOMATIC1111 API not reachable. Start webui with --api and verify IMAGEFORGE_A1111_BASE_URL."
+ )
+ if is_cancelled():
+ return ProviderResult(image_paths=[])
+
+ output_dir.mkdir(parents=True, exist_ok=True)
+ progress(5, "AUTOMATIC1111 request started")
+
+ payload = {
+ "prompt": request_data.prompt,
+ "negative_prompt": request_data.negative_prompt or "",
+ "steps": request_data.steps,
+ "cfg_scale": request_data.guidance,
+ "seed": request_data.seed,
+ "width": request_data.width,
+ "height": request_data.height,
+ "batch_size": request_data.count,
+ "n_iter": 1,
+ "sampler_name": "Euler a",
+ }
+
+ if request_data.model_variant:
+ self._request_json("POST", "/sdapi/v1/options", {"sd_model_checkpoint": request_data.model_variant})
+
+ endpoint = "/sdapi/v1/txt2img"
+ if request_data.init_image_path:
+ init_path = Path(request_data.init_image_path)
+ if not init_path.exists() or not init_path.is_file():
+ raise ValueError("init_image_path does not exist or is not a file")
+ payload = {
+ "prompt": request_data.prompt,
+ "negative_prompt": request_data.negative_prompt or "",
+ "steps": request_data.steps,
+ "cfg_scale": request_data.guidance,
+ "seed": request_data.seed,
+ "width": request_data.width,
+ "height": request_data.height,
+ "batch_size": request_data.count,
+ "n_iter": 1,
+ "denoising_strength": max(0.0, min(1.0, request_data.img2img_strength)),
+ "init_images": [base64.b64encode(init_path.read_bytes()).decode("ascii")],
+ "sampler_name": "Euler a",
+ }
+ endpoint = "/sdapi/v1/img2img"
+
+ response = self._request_json("POST", endpoint, payload)
+ images = response.get("images") if isinstance(response, dict) else None
+ if not isinstance(images, list) or not images:
+ raise RuntimeError("AUTOMATIC1111 returned no images")
+
+ image_paths: list[Path] = []
+ total = len(images)
+ for idx, encoded in enumerate(images, start=1):
+ if is_cancelled():
+ break
+ try:
+ raw = base64.b64decode(encoded)
+ except Exception as exc:
+ raise RuntimeError("Failed to decode image from AUTOMATIC1111") from exc
+ out_path = output_dir / f"image_{idx:02d}.png"
+ out_path.write_bytes(raw)
+ image_paths.append(out_path)
+ pct = int((idx / max(1, total)) * 100)
+ progress(pct, f"AUTOMATIC1111 image {idx}/{total} complete")
+
+ return ProviderResult(image_paths=image_paths)
+
+ def _request_json(self, method: str, endpoint: str, payload: dict | None = None, retries: int | None = None):
+ endpoint = self._normalize_endpoint(endpoint)
+ retry_budget = self.retry_count if retries is None else max(0, retries)
+ data = json.dumps(payload).encode("utf-8") if payload is not None else None
+ req = request.Request(
+ f"{self.base_url}{endpoint}",
+ data=data,
+ method=method,
+ headers=self._headers(),
+ )
+
+ attempts = retry_budget + 1
+ for attempt in range(1, attempts + 1):
+ try:
+ with request.urlopen(req, timeout=self.timeout) as resp:
+ return json.loads(resp.read().decode("utf-8"))
+ except error.HTTPError as exc:
+ detail = exc.read().decode("utf-8", errors="ignore")
+ retryable = exc.code >= 500 or exc.code == 429
+ if retryable and attempt < attempts:
+ self._sleep_before_retry(attempt)
+ continue
+ raise RuntimeError(f"AUTOMATIC1111 {method} failed ({exc.code}): {detail}") from exc
+ except Exception as exc:
+ if attempt < attempts:
+ self._sleep_before_retry(attempt)
+ continue
+ raise RuntimeError(f"AUTOMATIC1111 {method} failed: {exc}") from exc
+
+ def _headers(self) -> dict[str, str]:
+ headers = {"Content-Type": "application/json"}
+ if self.auth_header:
+ headers["Authorization"] = self.auth_header
+ return headers
+
+ @staticmethod
+ def _normalize_endpoint(endpoint: str) -> str:
+ if endpoint.startswith("/"):
+ return endpoint
+ return f"/{endpoint}"
+
+ def _resolve_auth_header(self) -> str:
+ if A1111_API_AUTH.strip():
+ token = base64.b64encode(A1111_API_AUTH.encode("utf-8")).decode("ascii")
+ return f"Basic {token}"
+ if A1111_API_USER.strip() or A1111_API_PASSWORD.strip():
+ pair = f"{A1111_API_USER}:{A1111_API_PASSWORD}"
+ token = base64.b64encode(pair.encode("utf-8")).decode("ascii")
+ return f"Basic {token}"
+ return ""
+
+ def _sleep_before_retry(self, attempt: int) -> None:
+ if self.retry_backoff_seconds <= 0:
+ return
+ time.sleep(self.retry_backoff_seconds * attempt)
diff --git a/imageforge/backend/app/providers/diffusion_provider.py b/imageforge/backend/app/providers/diffusion_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..75e34c7e3c6a2e7d337d623eb574e8f1b175b28c
--- /dev/null
+++ b/imageforge/backend/app/providers/diffusion_provider.py
@@ -0,0 +1,165 @@
+from __future__ import annotations
+
+import logging
+import os
+from pathlib import Path
+
+from .interface import ProviderRequest, ProviderResult, ProviderUnavailableError
+
+LOGGER = logging.getLogger(__name__)
+
+# Lazy imports to avoid torch loading issues on Windows
+torch = None
+StableDiffusionImg2ImgPipeline = None
+StableDiffusionPipeline = None
+
+def _ensure_imports():
+ global torch, StableDiffusionImg2ImgPipeline, StableDiffusionPipeline
+ if torch is not None:
+ return
+ try:
+ import torch as _torch
+ from diffusers import StableDiffusionImg2ImgPipeline as _Img2Img
+ from diffusers import StableDiffusionPipeline as _Pipeline
+ torch = _torch
+ StableDiffusionImg2ImgPipeline = _Img2Img
+ StableDiffusionPipeline = _Pipeline
+ except Exception: # pragma: no cover - optional dependency
+ pass
+
+
+class DiffusionProvider:
+ id = "diffusion"
+ name = "Stable Diffusion (local)"
+ description = "Uses diffusers for local Stable Diffusion generation"
+
+ def __init__(self, model_id: str = "segmind/tiny-sd") -> None:
+ self.model_id = os.getenv("IMAGEFORGE_DIFFUSION_MODEL", model_id)
+ self._pipe: StableDiffusionPipeline | None = None
+ self._img2img_pipe: StableDiffusionImg2ImgPipeline | None = None
+
+ def is_available(self) -> bool:
+ _ensure_imports()
+ return StableDiffusionPipeline is not None and torch is not None
+
+ def _ensure_pipeline(self) -> StableDiffusionPipeline:
+ _ensure_imports()
+ if StableDiffusionPipeline is None or torch is None:
+ raise ProviderUnavailableError(
+ "Diffusion dependencies missing. Install diffusers, torch, and transformers."
+ )
+ if self._pipe is None:
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+ dtype = torch.float16 if device == "cuda" else torch.float32
+ local_only = os.getenv("IMAGEFORGE_DIFFUSION_LOCAL_ONLY", "0") == "1"
+ LOGGER.info("Loading diffusion model '%s' on %s", self.model_id, device)
+ try:
+ pipe = StableDiffusionPipeline.from_pretrained(
+ self.model_id,
+ torch_dtype=dtype,
+ local_files_only=local_only,
+ )
+ except Exception as exc: # noqa: BLE001
+ mode_hint = "local cache only" if local_only else "online download"
+ raise ProviderUnavailableError(
+ f"Diffusion model '{self.model_id}' could not be loaded ({mode_hint}). "
+ "Set IMAGEFORGE_DIFFUSION_LOCAL_ONLY=0 to allow downloading models."
+ ) from exc
+ if device == "cuda":
+ pipe = pipe.to(device)
+ if os.getenv("IMAGEFORGE_ENABLE_ATTENTION_SLICING", "1") == "1":
+ pipe.enable_attention_slicing()
+ self._pipe = pipe
+ return self._pipe
+
+ def _ensure_img2img_pipeline(self) -> StableDiffusionImg2ImgPipeline | None:
+ _ensure_imports()
+ if StableDiffusionImg2ImgPipeline is None or torch is None:
+ return None
+ if self._img2img_pipe is None:
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+ dtype = torch.float16 if device == "cuda" else torch.float32
+ local_only = os.getenv("IMAGEFORGE_DIFFUSION_LOCAL_ONLY", "0") == "1"
+ LOGGER.info("Loading diffusion img2img model '%s' on %s", self.model_id, device)
+ try:
+ img2img = StableDiffusionImg2ImgPipeline.from_pretrained(
+ self.model_id,
+ torch_dtype=dtype,
+ local_files_only=local_only,
+ )
+ except Exception as exc: # noqa: BLE001
+ mode_hint = "local cache only" if local_only else "online download"
+ raise ProviderUnavailableError(
+ f"Diffusion img2img model '{self.model_id}' could not be loaded ({mode_hint}). "
+ "Set IMAGEFORGE_DIFFUSION_LOCAL_ONLY=0 to allow downloading models."
+ ) from exc
+ if device == "cuda":
+ img2img = img2img.to(device)
+ if os.getenv("IMAGEFORGE_ENABLE_ATTENTION_SLICING", "1") == "1":
+ img2img.enable_attention_slicing()
+ self._img2img_pipe = img2img
+ return self._img2img_pipe
+
+ def generate(self, request: ProviderRequest, output_dir: Path, progress, is_cancelled) -> ProviderResult:
+ if is_cancelled():
+ return ProviderResult(image_paths=[])
+ if request.model_variant and request.model_variant != self.model_id:
+ self.model_id = request.model_variant
+ self._pipe = None
+ self._img2img_pipe = None
+ progress(1, "Loading diffusion model")
+ pipe = self._ensure_pipeline()
+ output_dir.mkdir(parents=True, exist_ok=True)
+ image_paths: list[Path] = []
+
+ for idx in range(request.count):
+ if is_cancelled():
+ break
+
+ seed = request.seed + idx
+ generator = torch.Generator(device=pipe.device).manual_seed(seed)
+
+ def _callback(step: int, timestep: int, latents): # noqa: ANN001
+ if is_cancelled():
+ raise RuntimeError("Generation cancelled")
+ local_progress = int(((step + 1) / max(1, request.steps)) * 100)
+ progress(local_progress, f"Diffusion step {step + 1}/{request.steps} (image {idx + 1})")
+
+ if request.init_image_path:
+ from PIL import Image
+
+ img2img_pipe = self._ensure_img2img_pipeline()
+ if img2img_pipe is None:
+ raise ProviderUnavailableError("Img2Img requires diffusers img2img pipeline support")
+ init_image = Image.open(request.init_image_path).convert("RGB").resize((request.width, request.height))
+ result = img2img_pipe(
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt or None,
+ image=init_image,
+ num_inference_steps=request.steps,
+ guidance_scale=request.guidance,
+ strength=max(0.0, min(1.0, request.img2img_strength)),
+ generator=generator,
+ callback=_callback,
+ callback_steps=1,
+ )
+ else:
+ result = pipe(
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt or None,
+ width=request.width,
+ height=request.height,
+ num_inference_steps=request.steps,
+ guidance_scale=request.guidance,
+ generator=generator,
+ callback=_callback,
+ callback_steps=1,
+ )
+ image = result.images[0]
+ image_path = output_dir / f"image_{idx + 1:02d}.png"
+ image.save(image_path, format="PNG")
+ image_paths.append(image_path)
+ pct = int(((idx + 1) / request.count) * 100)
+ progress(pct, f"Diffusion image {idx + 1}/{request.count} complete")
+
+ return ProviderResult(image_paths=image_paths)
diff --git a/imageforge/backend/app/providers/dummy_provider.py b/imageforge/backend/app/providers/dummy_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..8281fc58ffa64002552fd025e50b46608b80fa06
--- /dev/null
+++ b/imageforge/backend/app/providers/dummy_provider.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import math
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+
+from .interface import IImageProvider, ProviderRequest, ProviderResult
+
+
+class DummyProvider(IImageProvider):
+ id = "dummy"
+ name = "Dummy Placeholder"
+ description = "Generates local placeholder images without AI dependencies"
+
+ def is_available(self) -> bool:
+ return True
+
+ def generate(
+ self,
+ request: ProviderRequest,
+ output_dir: Path,
+ progress,
+ is_cancelled,
+ ) -> ProviderResult:
+ output_dir.mkdir(parents=True, exist_ok=True)
+ image_paths: list[Path] = []
+
+ for idx in range(request.count):
+ if is_cancelled():
+ break
+
+ image = Image.new("RGB", (request.width, request.height))
+ pixels = image.load()
+ for y in range(request.height):
+ for x in range(request.width):
+ r = int(40 + 215 * (x / max(1, request.width - 1)))
+ g = int(40 + 215 * (y / max(1, request.height - 1)))
+ b = int(120 + 80 * math.sin((x + y) / 120))
+ pixels[x, y] = (r, g, max(0, min(255, b)))
+ if request.init_image_path:
+ source = Image.open(request.init_image_path).convert("RGB")
+ source = source.resize((request.width, request.height))
+ alpha = max(0.0, min(1.0, request.img2img_strength))
+ image = Image.blend(source, image, 1.0 - alpha)
+
+ draw = ImageDraw.Draw(image)
+ font = ImageFont.load_default()
+ lines = [
+ f"DummyProvider #{idx + 1}",
+ f"Prompt: {request.prompt[:80]}",
+ f"Negative: {request.negative_prompt[:80] or '-'}",
+ f"Seed: {request.seed}",
+ f"Size: {request.width}x{request.height}",
+ ]
+ y_pos = 20
+ for line in lines:
+ draw.rectangle((16, y_pos - 2, request.width - 16, y_pos + 14), fill=(0, 0, 0))
+ draw.text((20, y_pos), line, fill=(255, 255, 255), font=font)
+ y_pos += 20
+
+ image_path = output_dir / f"image_{idx + 1:02d}.png"
+ image.save(image_path, format="PNG")
+ image_paths.append(image_path)
+ pct = int(((idx + 1) / request.count) * 100)
+ progress(pct, f"Dummy image {idx + 1}/{request.count} complete")
+
+ return ProviderResult(image_paths=image_paths)
diff --git a/imageforge/backend/app/providers/factory.py b/imageforge/backend/app/providers/factory.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b11d7d052c1933ee21f22d484e3ae520474dd63
--- /dev/null
+++ b/imageforge/backend/app/providers/factory.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from .a1111_provider import A1111Provider
+from .diffusion_provider import DiffusionProvider
+from .dummy_provider import DummyProvider
+from .interface import IImageProvider
+from .localai_provider import LocalAIProvider
+from .zimageturbo_provider import ZImageTurboProvider
+
+
+class ProviderRegistry:
+ def __init__(self) -> None:
+ self._providers: dict[str, IImageProvider] = {
+ "dummy": DummyProvider(),
+ "localai": LocalAIProvider(),
+ "diffusion": DiffusionProvider(),
+ "a1111": A1111Provider(),
+ "zimageturbo": ZImageTurboProvider(),
+ }
+
+ def get(self, provider_id: str) -> IImageProvider:
+ provider = self._providers.get(provider_id)
+ if provider is None:
+ raise KeyError(f"Unknown provider: {provider_id}")
+ return provider
+
+ def list(self) -> list[IImageProvider]:
+ return list(self._providers.values())
diff --git a/imageforge/backend/app/providers/huggingface_provider.py b/imageforge/backend/app/providers/huggingface_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cc01cb5e9e12d5471c7c3009f18c19e09d4af6a
--- /dev/null
+++ b/imageforge/backend/app/providers/huggingface_provider.py
@@ -0,0 +1,328 @@
+"""
+HuggingFace Spaces Provider for PixelForge
+
+This provider allows PixelForge to call any HuggingFace Space as an image generation backend.
+Supports both Spaces with Gradio API and custom inference endpoints.
+
+Phase 2 Implementation (Future)
+"""
+
+import os
+import json
+import requests
+import base64
+import logging
+from io import BytesIO
+from pathlib import Path
+from PIL import Image
+
+logger = logging.getLogger(__name__)
+
+
+class HuggingFaceSpaceProvider:
+ """
+ Provider für HuggingFace Spaces Integration
+
+ Beispiele:
+ - Heartsync/Adult: Erwachsene Bildgenerierung (Z-Image Turbo)
+ - Andere Spaces: beliebige Custom Spaces
+
+ Umgebungsvariablen:
+ - HF_API_TOKEN: HuggingFace API Token (https://huggingface.co/settings/tokens)
+ - HF_SPACE_URL: Komplette Space URL oder nur "username/space-name"
+ - HF_REQUEST_TIMEOUT: Timeout in Sekunden (default: 300)
+ """
+
+ id = "huggingface"
+ name = "HuggingFace Spaces"
+ description = "Nutzt HuggingFace Spaces für Image Generation (erwachsene Inhalte, Anime, Custom)"
+
+ def __init__(
+ self,
+ space_id: str = None,
+ api_token: str = None,
+ timeout: int = 300,
+ ):
+ """
+ Args:
+ space_id: HuggingFace Space ID (z.B. "Heartsync/Adult" oder komplette URL)
+ api_token: HuggingFace API Token
+ timeout: Request Timeout in Sekunden
+ """
+ self.space_id = space_id or os.getenv("HF_SPACE_URL", "Heartsync/Adult")
+ self.api_token = api_token or os.getenv("HF_API_TOKEN", "")
+ self.timeout = timeout or int(os.getenv("HF_REQUEST_TIMEOUT", "300"))
+
+ # Normalize space URL
+ if not self.space_id.startswith("https://"):
+ self.space_id = f"https://huggingface.co/spaces/{self.space_id}"
+
+ self.api_url = f"{self.space_id}/call/predict"
+ self.logger = logging.getLogger(__name__)
+
+ def is_available(self) -> bool:
+ """Check if HuggingFace Space is reachable"""
+ if not self.api_token:
+ self.logger.warning("HF_API_TOKEN not set - HuggingFace Space Provider unavailable")
+ return False
+
+ try:
+ # Check Space health
+ response = requests.head(
+ self.space_id,
+ timeout=5,
+ headers={"Authorization": f"Bearer {self.api_token}"}
+ )
+ is_available = response.status_code < 500
+ if is_available:
+ self.logger.info(f"✓ HuggingFace Space verfügbar: {self.space_id}")
+ else:
+ self.logger.warning(f"HuggingFace Space Status: {response.status_code}")
+ return is_available
+ except Exception as e:
+ self.logger.error(f"HuggingFace Space Health Check failed: {e}")
+ return False
+
+ def generate(self, request: "ProviderRequest") -> "ProviderResult":
+ """
+ Generate image using HuggingFace Space
+
+ Args:
+ request: ProviderRequest mit prompt, negative_prompt, etc.
+
+ Returns:
+ ProviderResult mit generiertem Bild
+ """
+ try:
+ # Payload für Gradio Space API
+ payload = {
+ "data": [
+ request.prompt, # prompt
+ request.negative_prompt, # negative_prompt
+ int(request.seed % (2**31)), # seed
+ request.steps, # num_inference_steps
+ request.guidance_scale, # guidance_scale
+ ]
+ }
+
+ headers = {
+ "Authorization": f"Bearer {self.api_token}",
+ "Content-Type": "application/json",
+ }
+
+ self.logger.info(f"Generating image on HF Space: {self.space_id}")
+ self.logger.debug(f"Payload: {json.dumps(payload, indent=2)}")
+
+ # Call HuggingFace Space
+ response = requests.post(
+ self.api_url,
+ json=payload,
+ headers=headers,
+ timeout=self.timeout,
+ )
+
+ response.raise_for_status()
+
+ # Parse response
+ result = response.json()
+ self.logger.debug(f"HF Response: {json.dumps(result, indent=2)}")
+
+ # Erwartete Struktur: {"data": [{"name": "image.png", "data": "base64..."}]}
+ if "data" not in result or not result["data"]:
+ raise ValueError(f"Unexpected response format: {result}")
+
+ # Extract image(s) from response
+ image_data_list = result["data"]
+ if not isinstance(image_data_list, list):
+ image_data_list = [image_data_list]
+
+ image_paths = []
+
+ for idx, img_data in enumerate(image_data_list):
+ # Handle different response formats
+ if isinstance(img_data, dict) and "data" in img_data:
+ # Gradio format: {"name": "...", "data": "base64..."}
+ base64_str = img_data["data"]
+ elif isinstance(img_data, str):
+ # Direct base64 string
+ base64_str = img_data
+ else:
+ self.logger.warning(f"Unknown image format: {type(img_data)}")
+ continue
+
+ # Decode base64 image
+ if base64_str.startswith("data:image"):
+ # Data URL format: "data:image/png;base64,..."
+ base64_str = base64_str.split(",", 1)[1]
+
+ try:
+ image_bytes = base64.b64decode(base64_str)
+ image = Image.open(BytesIO(image_bytes))
+
+ # Save to disk
+ output_path = Path(__file__).parent.parent.parent / "output" / f"image_{idx:02d}.png"
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ image.save(output_path)
+
+ image_paths.append(str(output_path))
+ self.logger.info(f"✓ Image saved: {output_path}")
+
+ except Exception as e:
+ self.logger.error(f"Failed to decode/save image {idx}: {e}")
+ raise
+
+ if not image_paths:
+ raise ValueError("No images in response")
+
+ return {
+ "image_paths": image_paths,
+ "message": f"Generated {len(image_paths)} image(s) via {self.space_id}",
+ }
+
+ except requests.Timeout:
+ raise TimeoutError(
+ f"HuggingFace Space request timed out after {self.timeout}s. "
+ "Space might be busy or overloaded."
+ )
+ except requests.HTTPError as e:
+ if e.response.status_code == 401:
+ raise PermissionError(f"Invalid HF API Token")
+ elif e.response.status_code == 404:
+ raise ValueError(f"HuggingFace Space not found: {self.space_id}")
+ else:
+ raise RuntimeError(f"HF Space API error: {e}")
+ except Exception as e:
+ self.logger.error(f"HuggingFace Space generation failed: {e}")
+ raise
+
+
+# ============================================================================
+# Alternative: Verwendung von gradio_client statt REST
+# ============================================================================
+
+class HuggingFaceSpaceProviderGradio:
+ """
+ Alternative Implementation using gradio_client
+
+ Vorteil: Automatische Format-Konvertierung
+ Nachteil: Zusätzliche Dependency (gradio-client)
+
+ Usage:
+ pip install gradio_client
+ """
+
+ id = "huggingface_gradio"
+ name = "HuggingFace Spaces (Gradio)"
+ description = "HuggingFace Spaces via Gradio Client"
+
+ def __init__(self, space_id: str = None):
+ try:
+ from gradio_client import Client
+ self.Client = Client
+ except ImportError:
+ raise ImportError("gradio-client not installed. Install with: pip install gradio_client")
+
+ self.space_id = space_id or os.getenv("HF_SPACE_URL", "Heartsync/Adult")
+ self.logger = logging.getLogger(__name__)
+
+ def is_available(self) -> bool:
+ try:
+ client = self.Client(f"https://huggingface.co/spaces/{self.space_id}")
+ self.logger.info(f"✓ Gradio Client verbunden: {self.space_id}")
+ return True
+ except Exception as e:
+ self.logger.error(f"Gradio Client connection failed: {e}")
+ return False
+
+ def generate(self, request: "ProviderRequest") -> "ProviderResult":
+ from gradio_client import Client
+
+ try:
+ client = Client(f"https://huggingface.co/spaces/{self.space_id}")
+
+ result = client.predict(
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ seed=int(request.seed % (2**31)),
+ num_inference_steps=request.steps,
+ guidance_scale=request.guidance_scale,
+ api_name="/predict"
+ )
+
+ # Gradio returns file path
+ image_path = str(result)
+
+ return {
+ "image_paths": [image_path],
+ "message": f"Generated via {self.space_id}",
+ }
+
+ except Exception as e:
+ self.logger.error(f"Gradio generation failed: {e}")
+ raise
+
+
+# ============================================================================
+# Integration Instructions for later
+# ============================================================================
+
+"""
+INTEGRATION CHECKLIST (Phase 2):
+
+1. ✅ Dieses File erstellt
+2. [ ] HuggingFace API Token besorgen:
+ - https://huggingface.co/settings/tokens
+ - New Token → read role
+ - Setze env: HF_API_TOKEN=hf_xxxxx
+
+3. [ ] Adult_repo zu HF Space deployen:
+ - cd d:/VSC Codes/Bild/Adult_repo
+ - pip install huggingface-hub
+ - huggingface-cli login
+ - git push huggingface main
+
+4. [ ] Provider in Factory registrieren:
+ # in imageforge/backend/app/providers/factory.py
+ from .huggingface_provider import HuggingFaceSpaceProvider
+
+ _providers = {
+ ...
+ "huggingface": HuggingFaceSpaceProvider(),
+ }
+
+5. [ ] Testen:
+ # Im Backend
+ POST http://127.0.0.1:8008/generate
+ {
+ "model": "huggingface",
+ "prompt": "A beautiful sunset",
+ "negative_prompt": "",
+ "steps": 20,
+ "guidance_scale": 7.5
+ }
+
+6. [ ] Colab deaktivieren oder als Fallback nutzen
+
+TROUBLESHOOTING:
+
+Problem: "HF_API_TOKEN not set"
+Fix: $env:HF_API_TOKEN = "hf_xxxxx"; Restart Backend
+
+Problem: "Space not found (404)"
+Fix: Space-ID prüfen: "Heartsync/Adult" oder komplette URL?
+
+Problem: "Request timeout"
+Fix: Space ist überlastet. Später erneut versuchen.
+ Oder: $env:HF_REQUEST_TIMEOUT = "600" (10 Min)
+
+Problem: "Invalid API Token"
+Fix: Token regenerieren auf https://huggingface.co/settings/tokens
+ Ensure read permissions
+
+TESTEN MIT CURL:
+
+curl -X POST https://api-inference.huggingface.co/models/Heartsync/Adult \\
+ -H "Authorization: Bearer hf_xxxxx" \\
+ -H "Content-Type: application/json" \\
+ -d '{"inputs": "a girl in school uniform"}'
+"""
diff --git a/imageforge/backend/app/providers/interface.py b/imageforge/backend/app/providers/interface.py
new file mode 100644
index 0000000000000000000000000000000000000000..0dea0cea433dba026ebabf101a997cf62520b33c
--- /dev/null
+++ b/imageforge/backend/app/providers/interface.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Callable, Protocol
+
+
+class ProviderUnavailableError(RuntimeError):
+ pass
+
+
+@dataclass(slots=True)
+class ProviderRequest:
+ prompt: str
+ negative_prompt: str
+ count: int
+ width: int
+ height: int
+ seed: int
+ steps: int
+ guidance: float
+ init_image_path: str | None = None
+ img2img_strength: float = 0.45
+ model_variant: str | None = None
+
+
+@dataclass(slots=True)
+class ProviderResult:
+ image_paths: list[Path]
+
+
+ProgressCallback = Callable[[int, str], None]
+CancelCallback = Callable[[], bool]
+
+
+class IImageProvider(Protocol):
+ id: str
+ name: str
+ description: str
+
+ def is_available(self) -> bool:
+ ...
+
+ def generate(
+ self,
+ request: ProviderRequest,
+ output_dir: Path,
+ progress: ProgressCallback,
+ is_cancelled: CancelCallback,
+ ) -> ProviderResult:
+ ...
diff --git a/imageforge/backend/app/providers/localai_provider.py b/imageforge/backend/app/providers/localai_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..42c1102d349b1a070abf78216164a3b8343a2ab8
--- /dev/null
+++ b/imageforge/backend/app/providers/localai_provider.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+import os
+from pathlib import Path
+import threading
+import time
+
+from ..local_ai.engine import LocalAIEngine, LocalAIRequest
+from .interface import ProviderRequest, ProviderResult, ProviderUnavailableError
+
+
+class LocalAIProvider:
+ id = "localai"
+ name = "LocalAI (self-hosted)"
+ description = "Own local AI engine, no paid cloud API required"
+
+ def __init__(self) -> None:
+ self.engine = LocalAIEngine()
+ self.image_timeout_seconds = max(1, int(os.getenv("IMAGEFORGE_LOCALAI_IMAGE_TIMEOUT_SECONDS", "180")))
+
+ def is_available(self) -> bool:
+ return self.engine.is_available()
+
+ def generate(self, request: ProviderRequest, output_dir: Path, progress, is_cancelled) -> ProviderResult:
+ if not self.is_available():
+ raise ProviderUnavailableError(
+ "LocalAI dependencies missing. Install diffusers/torch packages."
+ )
+ output_dir.mkdir(parents=True, exist_ok=True)
+ image_paths: list[Path] = []
+
+ for idx in range(request.count):
+ if is_cancelled():
+ break
+ progress(5, f"LocalAI preparing image {idx + 1}/{request.count}")
+ image = self._generate_with_heartbeat(
+ LocalAIRequest(
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ width=request.width,
+ height=request.height,
+ steps=request.steps,
+ guidance=request.guidance,
+ seed=request.seed + idx,
+ init_image_path=request.init_image_path,
+ strength=request.img2img_strength,
+ model_variant=request.model_variant,
+ ),
+ progress=progress,
+ is_cancelled=is_cancelled,
+ image_index=idx + 1,
+ total=request.count,
+ )
+ image_path = output_dir / f"image_{idx + 1:02d}.png"
+ image.save(image_path, format="PNG")
+ image_paths.append(image_path)
+ pct = int(((idx + 1) / request.count) * 100)
+ progress(pct, f"LocalAI image {idx + 1}/{request.count} complete")
+
+ return ProviderResult(image_paths=image_paths)
+
+ def _generate_with_heartbeat(
+ self,
+ request: LocalAIRequest,
+ progress,
+ is_cancelled,
+ image_index: int,
+ total: int,
+ ):
+ result_holder: dict[str, object] = {}
+ error_holder: dict[str, Exception] = {}
+
+ def _run() -> None:
+ try:
+ result_holder["image"] = self.engine.generate(request)
+ except Exception as exc: # noqa: BLE001
+ error_holder["error"] = exc
+
+ worker = threading.Thread(target=_run, daemon=True)
+ worker.start()
+
+ started = time.monotonic()
+ heartbeat = 8
+ while worker.is_alive():
+ if is_cancelled():
+ raise RuntimeError("Generation cancelled")
+ elapsed = time.monotonic() - started
+ if elapsed > self.image_timeout_seconds:
+ raise TimeoutError(
+ "LocalAI generation timed out. "
+ "Try fewer steps/smaller size or use fallback model 'dummy'."
+ )
+ pct = min(95, heartbeat)
+ progress(
+ pct,
+ f"LocalAI generating image {image_index}/{total} ({pct}%, {int(elapsed)}s)",
+ )
+ heartbeat = min(95, heartbeat + 2)
+ worker.join(timeout=1.0)
+
+ if "error" in error_holder:
+ raise error_holder["error"]
+ if "image" not in result_holder:
+ raise RuntimeError("LocalAI generation ended without image result")
+ return result_holder["image"]
diff --git a/imageforge/backend/app/providers/zimageturbo_provider.py b/imageforge/backend/app/providers/zimageturbo_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..aaf7483b9c83da94c812bddb3a8e108a2f61214b
--- /dev/null
+++ b/imageforge/backend/app/providers/zimageturbo_provider.py
@@ -0,0 +1,227 @@
+from __future__ import annotations
+
+import base64
+import json
+import logging
+import os
+import time
+from io import BytesIO
+from pathlib import Path
+from typing import Any
+from urllib.parse import urljoin
+
+import requests
+from PIL import Image
+
+from .interface import (
+ CancelCallback,
+ IImageProvider,
+ ProgressCallback,
+ ProviderRequest,
+ ProviderResult,
+ ProviderUnavailableError,
+)
+
+LOGGER = logging.getLogger(__name__)
+
+
+class ZImageTurboProvider(IImageProvider):
+ id = "zimageturbo"
+ name = "Adult + Z-Image Turbo (HF Space API)"
+ description = "Nutzen von Heartsync/Adult per HuggingFace Space API"
+
+ def __init__(self) -> None:
+ self.mode = os.getenv("ZIMAGETURBO_MODE", "hf_space").strip().lower()
+ self.api_key = os.getenv("ZIMAGETURBO_API_KEY", "").strip()
+ self.timeout = int(os.getenv("ZIMAGETURBO_TIMEOUT", "300"))
+
+ self.hf_space = os.getenv("ZIMAGETURBO_HF_SPACE", "Heartsync/Adult").strip()
+ self.hf_api_name = os.getenv("ZIMAGETURBO_HF_API_NAME", "predict").strip().lstrip("/")
+
+ legacy_base = os.getenv("ZIMAGETURBO_API_URL", "").strip()
+ if legacy_base:
+ self.legacy_api_url = legacy_base.rstrip("/")
+ else:
+ self.legacy_api_url = ""
+
+ self.hf_base_url = f"https://huggingface.co/spaces/{self.hf_space}"
+ self._available: bool | None = None
+
+ def is_available(self) -> bool:
+ if self._available is not None:
+ return self._available
+ try:
+ if self.mode == "legacy":
+ self._available = self._is_legacy_available()
+ else:
+ self._available = self._is_hf_space_available()
+ except Exception: # noqa: BLE001
+ self._available = False
+ return self._available
+
+ def generate(
+ self,
+ request: ProviderRequest,
+ output_dir: Path,
+ progress: ProgressCallback,
+ is_cancelled: CancelCallback,
+ ) -> ProviderResult:
+ if not self.is_available():
+ raise ProviderUnavailableError("Adult + Z-Image Turbo API nicht verfügbar")
+
+ output_dir.mkdir(parents=True, exist_ok=True)
+ image_paths: list[Path] = []
+
+ for index in range(request.count):
+ if is_cancelled():
+ break
+
+ progress(int(10 + 80 * (index / max(1, request.count))), f"Generiere Bild {index + 1}/{request.count}...")
+ current_seed = request.seed + index if request.seed >= 0 else None
+
+ if self.mode == "legacy":
+ image = self._generate_legacy(request, current_seed)
+ else:
+ image = self._generate_hf_space(request, current_seed)
+
+ out = output_dir / f"image_{index + 1:02d}.png"
+ image.save(out)
+ image_paths.append(out)
+ progress(int(10 + 80 * ((index + 1) / max(1, request.count))), f"Bild {index + 1}/{request.count} gespeichert")
+
+ if not image_paths:
+ raise RuntimeError("Keine Bilder erzeugt")
+
+ return ProviderResult(image_paths=image_paths)
+
+ def _headers(self) -> dict[str, str]:
+ headers = {"Content-Type": "application/json"}
+ if self.api_key:
+ headers["Authorization"] = f"Bearer {self.api_key}"
+ return headers
+
+ def _is_legacy_available(self) -> bool:
+ if not self.legacy_api_url:
+ return False
+ try:
+ response = requests.get(f"{self.legacy_api_url}/health", timeout=5, headers=self._headers())
+ return response.status_code == 200
+ except Exception as exc: # noqa: BLE001
+ LOGGER.warning("Legacy ZImageTurbo health failed: %s", exc)
+ return False
+
+ def _is_hf_space_available(self) -> bool:
+ try:
+ response = requests.get(self.hf_base_url, timeout=8)
+ if response.status_code < 500:
+ return True
+ LOGGER.warning("HF Space returned status %s", response.status_code)
+ return False
+ except Exception as exc: # noqa: BLE001
+ LOGGER.warning("HF Space health failed: %s", exc)
+ return False
+
+ def _generate_legacy(self, request: ProviderRequest, seed: int | None) -> Image.Image:
+ payload: dict[str, Any] = {
+ "prompt": request.prompt,
+ "negative_prompt": request.negative_prompt,
+ "width": request.width,
+ "height": request.height,
+ "num_inference_steps": request.steps,
+ "guidance_scale": request.guidance,
+ }
+ if seed is not None:
+ payload["seed"] = seed
+
+ response = requests.post(
+ f"{self.legacy_api_url}/generate",
+ json=payload,
+ headers=self._headers(),
+ timeout=self.timeout,
+ )
+ response.raise_for_status()
+ body = response.json()
+ if not body.get("success") or not body.get("image"):
+ raise RuntimeError(body.get("error", "Ungültige Legacy API Antwort"))
+ raw = base64.b64decode(body["image"])
+ return Image.open(BytesIO(raw)).convert("RGB")
+
+ def _generate_hf_space(self, request: ProviderRequest, seed: int | None) -> Image.Image:
+ api_call_url = f"{self.hf_base_url}/gradio_api/call/{self.hf_api_name}"
+ payload = {
+ "data": [
+ request.prompt,
+ request.height,
+ request.width,
+ request.steps,
+ seed if seed is not None else 42,
+ seed is None,
+ 1,
+ ]
+ }
+
+ call_response = requests.post(
+ api_call_url,
+ json=payload,
+ headers=self._headers(),
+ timeout=self.timeout,
+ )
+ call_response.raise_for_status()
+ call_payload = call_response.json()
+ event_id = call_payload.get("event_id")
+ if not event_id:
+ raise RuntimeError(f"HF Space call failed: {call_payload}")
+
+ result = self._poll_hf_event(event_id)
+ image_url = self._extract_first_image_url(result)
+ if not image_url:
+ raise RuntimeError(f"Kein Bild in HF-Antwort: {result}")
+
+ full_url = image_url if image_url.startswith("http") else urljoin(self.hf_base_url, image_url)
+ image_response = requests.get(full_url, timeout=self.timeout)
+ image_response.raise_for_status()
+ return Image.open(BytesIO(image_response.content)).convert("RGB")
+
+ def _poll_hf_event(self, event_id: str) -> Any:
+ result_url = f"{self.hf_base_url}/gradio_api/call/{self.hf_api_name}/{event_id}"
+ deadline = time.time() + self.timeout
+
+ while time.time() < deadline:
+ response = requests.get(result_url, timeout=30)
+ response.raise_for_status()
+ lines = [line.strip() for line in response.text.splitlines() if line.strip()]
+ data_lines = [line[5:].strip() for line in lines if line.startswith("data:")]
+ if not data_lines:
+ time.sleep(1)
+ continue
+
+ for raw in reversed(data_lines):
+ if raw in {"[DONE]", "null"}:
+ continue
+ try:
+ return json.loads(raw)
+ except json.JSONDecodeError:
+ continue
+ time.sleep(1)
+
+ raise TimeoutError(f"HF Space Timeout nach {self.timeout}s")
+
+ def _extract_first_image_url(self, payload: Any) -> str | None:
+ stack = [payload]
+ while stack:
+ item = stack.pop()
+ if isinstance(item, str):
+ if item.startswith("http") and ("/gradio_api/file=" in item or item.endswith((".png", ".jpg", ".jpeg", ".webp"))):
+ return item
+ if item.startswith("/gradio_api/file="):
+ return item
+ elif isinstance(item, dict):
+ for key in ("url", "path"):
+ value = item.get(key)
+ if isinstance(value, str):
+ if value.startswith("http") or value.startswith("/gradio_api/file="):
+ return value
+ stack.extend(item.values())
+ elif isinstance(item, list):
+ stack.extend(item)
+ return None
diff --git a/imageforge/backend/app/storage/__init__.py b/imageforge/backend/app/storage/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/imageforge/backend/app/storage/__init__.py
@@ -0,0 +1 @@
+
diff --git a/imageforge/backend/app/storage/history.py b/imageforge/backend/app/storage/history.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c0777613e44c0c717d9b3b447584e2b3a717f79
--- /dev/null
+++ b/imageforge/backend/app/storage/history.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+import json
+from datetime import datetime, timezone
+from threading import Lock
+
+from ..core.config import HISTORY_FILE
+
+
+class PromptHistoryStore:
+ def __init__(self, max_entries: int = 50) -> None:
+ self.max_entries = max_entries
+ self._lock = Lock()
+
+ def add(self, prompt: str, negative_prompt: str) -> None:
+ with self._lock:
+ items = self._read()
+ items.insert(
+ 0,
+ {
+ "prompt": prompt,
+ "negative_prompt": negative_prompt,
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ },
+ )
+ HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
+ HISTORY_FILE.write_text(
+ json.dumps(items[: self.max_entries], indent=2), encoding="utf-8"
+ )
+
+ def list(self) -> list[dict[str, str]]:
+ with self._lock:
+ return self._read()
+
+ def _read(self) -> list[dict[str, str]]:
+ if not HISTORY_FILE.exists():
+ return []
+ try:
+ raw = json.loads(HISTORY_FILE.read_text(encoding="utf-8"))
+ if isinstance(raw, list):
+ return [item for item in raw if isinstance(item, dict)]
+ except json.JSONDecodeError:
+ return []
+ return []
diff --git a/imageforge/backend/app/storage/maintenance.py b/imageforge/backend/app/storage/maintenance.py
new file mode 100644
index 0000000000000000000000000000000000000000..32c87bc4158b4fa1347df92ef8ba27c0dbd1aa5b
--- /dev/null
+++ b/imageforge/backend/app/storage/maintenance.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+
+from ..core.config import OUTPUT_DIR
+
+
+def cleanup_outputs(retention_days: int) -> int:
+ if retention_days <= 0:
+ return 0
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+ cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
+ removed = 0
+ for item in OUTPUT_DIR.iterdir():
+ if not item.is_dir():
+ continue
+ dt = _parse_date(item.name)
+ if dt and dt < cutoff:
+ _remove_tree(item)
+ removed += 1
+ return removed
+
+
+def _parse_date(name: str) -> datetime | None:
+ try:
+ return datetime.strptime(name, "%Y-%m-%d").replace(tzinfo=timezone.utc)
+ except ValueError:
+ return None
+
+
+def _remove_tree(path: Path) -> None:
+ for child in path.glob("**/*"):
+ if child.is_file():
+ child.unlink(missing_ok=True)
+ for child in sorted(path.glob("**/*"), reverse=True):
+ if child.is_dir():
+ child.rmdir()
+ path.rmdir()
diff --git a/imageforge/backend/app/storage/presets.py b/imageforge/backend/app/storage/presets.py
new file mode 100644
index 0000000000000000000000000000000000000000..2ee1397a7ff4fcc0a56be1b4242dbc627ff60523
--- /dev/null
+++ b/imageforge/backend/app/storage/presets.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+import json
+from dataclasses import asdict, dataclass
+from datetime import datetime, timezone
+from threading import Lock
+
+from ..core.config import PRESETS_FILE
+
+
+@dataclass(slots=True)
+class Preset:
+ name: str
+ prompt: str
+ negative_prompt: str
+ model: str
+ size: str
+ count: int
+ steps: int
+ guidance: float
+ image_type: str
+ style_preset: str
+ style_strength: int
+ updated_at: str
+
+
+class PresetStore:
+ def __init__(self) -> None:
+ self._lock = Lock()
+
+ def list(self) -> list[Preset]:
+ with self._lock:
+ return [Preset(**item) for item in self._read()]
+
+ def upsert(self, payload: dict) -> Preset:
+ with self._lock:
+ rows = self._read()
+ now = datetime.now(timezone.utc).isoformat()
+ preset = Preset(
+ name=payload["name"],
+ prompt=payload.get("prompt", ""),
+ negative_prompt=payload.get("negative_prompt", ""),
+ model=payload.get("model", "dummy"),
+ size=payload.get("size", "1024x1024"),
+ count=int(payload.get("count", 1)),
+ steps=int(payload.get("steps", 30)),
+ guidance=float(payload.get("guidance", 7.5)),
+ image_type=payload.get("image_type", "general"),
+ style_preset=payload.get("style_preset", "auto"),
+ style_strength=int(payload.get("style_strength", 60)),
+ updated_at=now,
+ )
+ rows = [row for row in rows if row.get("name") != preset.name]
+ rows.insert(0, asdict(preset))
+ self._write(rows)
+ return preset
+
+ def delete(self, name: str) -> bool:
+ with self._lock:
+ rows = self._read()
+ after = [row for row in rows if row.get("name") != name]
+ if len(after) == len(rows):
+ return False
+ self._write(after)
+ return True
+
+ def _read(self) -> list[dict]:
+ if not PRESETS_FILE.exists():
+ return []
+ try:
+ content = json.loads(PRESETS_FILE.read_text(encoding="utf-8"))
+ if isinstance(content, list):
+ return [row for row in content if isinstance(row, dict)]
+ except json.JSONDecodeError:
+ return []
+ return []
+
+ def _write(self, rows: list[dict]) -> None:
+ PRESETS_FILE.parent.mkdir(parents=True, exist_ok=True)
+ PRESETS_FILE.write_text(json.dumps(rows[:200], indent=2), encoding="utf-8")
diff --git a/imageforge/backend/app/storage/settings.py b/imageforge/backend/app/storage/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..afee53c412cecb9ffe7ef4cc468ec3211f009990
--- /dev/null
+++ b/imageforge/backend/app/storage/settings.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import json
+from datetime import datetime, timezone
+from threading import Lock
+
+from ..core.config import (
+ ADMIN_AUDIT_FILE,
+ CONTENT_PROFILE,
+ OUTPUT_RETENTION_DAYS,
+ RATE_LIMIT_PER_MINUTE,
+ SETTINGS_FILE,
+)
+
+
+DEFAULT_SETTINGS = {
+ "content_profile": CONTENT_PROFILE,
+ "rate_limit_per_minute": RATE_LIMIT_PER_MINUTE,
+ "output_retention_days": OUTPUT_RETENTION_DAYS,
+ "adult_enabled": False,
+}
+
+
+class SettingsStore:
+ def __init__(self) -> None:
+ self._lock = Lock()
+
+ def get(self) -> dict:
+ with self._lock:
+ return self._merged()
+
+ def update(self, patch: dict, actor: str) -> dict:
+ with self._lock:
+ current = self._merged()
+ current.update({k: v for k, v in patch.items() if v is not None})
+ self._write(current)
+ self._audit(actor=actor, patch=patch, current=current)
+ return current
+
+ def _merged(self) -> dict:
+ current = DEFAULT_SETTINGS.copy()
+ current.update(self._read())
+ return current
+
+ def _read(self) -> dict:
+ if not SETTINGS_FILE.exists():
+ return {}
+ try:
+ parsed = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
+ return parsed if isinstance(parsed, dict) else {}
+ except json.JSONDecodeError:
+ return {}
+
+ def _write(self, data: dict) -> None:
+ SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
+ SETTINGS_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
+
+ def _audit(self, actor: str, patch: dict, current: dict) -> None:
+ entry = {
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "actor": actor,
+ "patch": patch,
+ "current": current,
+ }
+ ADMIN_AUDIT_FILE.parent.mkdir(parents=True, exist_ok=True)
+ with ADMIN_AUDIT_FILE.open("a", encoding="utf-8") as fp:
+ fp.write(json.dumps(entry, ensure_ascii=True) + "\n")
diff --git a/imageforge/backend/tests/test_a1111_provider.py b/imageforge/backend/tests/test_a1111_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..873cba40589de4eb4b7a0c8600d6becea05317ca
--- /dev/null
+++ b/imageforge/backend/tests/test_a1111_provider.py
@@ -0,0 +1,51 @@
+import base64
+
+from backend.app.providers.a1111_provider import A1111Provider
+
+
+class _FakeResponse:
+ def __init__(self, payload: bytes) -> None:
+ self._payload = payload
+
+ def read(self) -> bytes:
+ return self._payload
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc, tb):
+ return False
+
+
+def test_a1111_provider_builds_auth_header_from_compact_auth(monkeypatch) -> None:
+ monkeypatch.setattr("backend.app.providers.a1111_provider.A1111_API_AUTH", "user:secret")
+ monkeypatch.setattr("backend.app.providers.a1111_provider.A1111_API_USER", "")
+ monkeypatch.setattr("backend.app.providers.a1111_provider.A1111_API_PASSWORD", "")
+
+ provider = A1111Provider()
+
+ assert provider.auth_header.startswith("Basic ")
+ encoded = provider.auth_header.split(" ", maxsplit=1)[1]
+ decoded = base64.b64decode(encoded.encode("ascii")).decode("utf-8")
+ assert decoded == "user:secret"
+
+
+def test_a1111_provider_retries_transient_failure(monkeypatch) -> None:
+ monkeypatch.setattr("backend.app.providers.a1111_provider.A1111_RETRY_COUNT", 2)
+ monkeypatch.setattr("backend.app.providers.a1111_provider.A1111_RETRY_BACKOFF_SECONDS", 0.0)
+
+ attempts = {"count": 0}
+
+ def fake_urlopen(req, timeout): # noqa: ANN001
+ attempts["count"] += 1
+ if attempts["count"] == 1:
+ raise OSError("temporary network issue")
+ return _FakeResponse(b'{"ok": true}')
+
+ monkeypatch.setattr("backend.app.providers.a1111_provider.request.urlopen", fake_urlopen)
+
+ provider = A1111Provider()
+ result = provider._request_json("GET", "/sdapi/v1/sd-models")
+
+ assert result == {"ok": True}
+ assert attempts["count"] == 2
diff --git a/imageforge/backend/tests/test_dummy_provider.py b/imageforge/backend/tests/test_dummy_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..d43bfd5974a1875c90475488477db4b30dd718c3
--- /dev/null
+++ b/imageforge/backend/tests/test_dummy_provider.py
@@ -0,0 +1,32 @@
+from pathlib import Path
+
+from backend.app.providers.dummy_provider import DummyProvider
+from backend.app.providers.interface import ProviderRequest
+
+
+def test_dummy_provider_generates_images(tmp_path: Path) -> None:
+ provider = DummyProvider()
+ req = ProviderRequest(
+ prompt="test prompt",
+ negative_prompt="",
+ count=2,
+ width=256,
+ height=256,
+ seed=123,
+ steps=10,
+ guidance=7.5,
+ )
+
+ events: list[tuple[int, str]] = []
+
+ result = provider.generate(
+ req,
+ tmp_path,
+ lambda p, m: events.append((p, m)),
+ lambda: False,
+ )
+
+ assert len(result.image_paths) == 2
+ assert all(path.exists() for path in result.image_paths)
+ assert events
+ assert events[-1][0] == 100
diff --git a/imageforge/backend/tests/test_img2img.py b/imageforge/backend/tests/test_img2img.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3d14e61c5b6612f55222adc62779ae708c29aae
--- /dev/null
+++ b/imageforge/backend/tests/test_img2img.py
@@ -0,0 +1,29 @@
+from pathlib import Path
+
+from PIL import Image
+
+from backend.app.providers.dummy_provider import DummyProvider
+from backend.app.providers.interface import ProviderRequest
+
+
+def test_dummy_provider_image_to_image_blend(tmp_path: Path) -> None:
+ source = tmp_path / "source.png"
+ Image.new("RGB", (64, 64), color=(255, 0, 0)).save(source)
+
+ provider = DummyProvider()
+ req = ProviderRequest(
+ prompt="transform this",
+ negative_prompt="",
+ count=1,
+ width=128,
+ height=128,
+ seed=123,
+ steps=10,
+ guidance=7.5,
+ init_image_path=str(source),
+ img2img_strength=0.5,
+ )
+
+ result = provider.generate(req, tmp_path, lambda *_: None, lambda: False)
+ assert len(result.image_paths) == 1
+ assert result.image_paths[0].exists()
diff --git a/imageforge/backend/tests/test_job_manager.py b/imageforge/backend/tests/test_job_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5b6e7c8cd90f1566ce526dc4ff60bac4c96460d
--- /dev/null
+++ b/imageforge/backend/tests/test_job_manager.py
@@ -0,0 +1,143 @@
+import time
+from pathlib import Path
+
+from backend.app.api.schemas import GenerateRequest, JobStatus
+from backend.app.jobs.manager import JobManager
+from backend.app.providers.factory import ProviderRegistry
+from backend.app.storage.history import PromptHistoryStore
+
+
+def test_registry_contains_localai() -> None:
+ registry = ProviderRegistry()
+ provider = registry.get("localai")
+ assert provider.id == "localai"
+
+
+def test_job_lifecycle(tmp_path: Path, monkeypatch) -> None:
+ monkeypatch.setattr("backend.app.jobs.manager.OUTPUT_DIR", tmp_path)
+ manager = JobManager(ProviderRegistry(), PromptHistoryStore(), state_file=tmp_path / "jobs_state.json")
+
+ req = GenerateRequest(
+ prompt="a mountain",
+ negative_prompt="",
+ model="dummy",
+ size="1024x1024",
+ count=1,
+ random_seed=True,
+ steps=10,
+ guidance=7.5,
+ )
+ job_id = manager.submit(req)
+
+ deadline = time.time() + 10
+ while time.time() < deadline:
+ state = manager.get(job_id)
+ assert state is not None
+ if state.status in {JobStatus.DONE, JobStatus.ERROR}:
+ break
+ time.sleep(0.1)
+
+ state = manager.get(job_id)
+ assert state is not None
+ assert state.status == JobStatus.DONE
+ assert state.progress == 100
+ assert state.image_paths
+
+
+def test_job_cancel_from_queue(tmp_path: Path) -> None:
+ manager = JobManager(ProviderRegistry(), PromptHistoryStore(), state_file=tmp_path / "jobs_state.json")
+ req = GenerateRequest(prompt="cancel me", model="dummy")
+ job_id = manager.submit(req)
+
+ assert manager.cancel(job_id)
+ state = manager.get(job_id)
+ assert state is not None
+ assert state.cancel_requested
+
+
+def test_job_stats_shape(tmp_path: Path) -> None:
+ manager = JobManager(ProviderRegistry(), PromptHistoryStore(), state_file=tmp_path / "jobs_state.json")
+ stats = manager.stats()
+ assert set(stats.keys()) == {
+ "queued",
+ "running",
+ "done",
+ "error",
+ "cancelled",
+ "total",
+ "last_24h",
+ }
+
+
+def test_job_falls_back_when_primary_provider_fails(tmp_path: Path, monkeypatch) -> None:
+ monkeypatch.setattr("backend.app.jobs.manager.OUTPUT_DIR", tmp_path)
+ monkeypatch.setenv("IMAGEFORGE_ENABLE_AUTO_FALLBACK", "1")
+ monkeypatch.setenv("IMAGEFORGE_FALLBACK_MODELS", "localai,dummy")
+ monkeypatch.setenv("IMAGEFORGE_FALLBACK_MAX_STEPS", "12")
+
+ class FailingProvider:
+ id = "a1111"
+ name = "Failing A1111"
+ description = ""
+
+ def is_available(self) -> bool:
+ return True
+
+ def generate(self, request, output_dir, progress, is_cancelled): # noqa: ANN001
+ raise RuntimeError("primary failure")
+
+ class WorkingProvider:
+ id = "localai"
+ name = "Working LocalAI"
+ description = ""
+
+ def __init__(self) -> None:
+ self.last_steps = None
+
+ def is_available(self) -> bool:
+ return True
+
+ def generate(self, request, output_dir, progress, is_cancelled): # noqa: ANN001
+ self.last_steps = request.steps
+ output_dir.mkdir(parents=True, exist_ok=True)
+ path = output_dir / "image_01.png"
+ path.write_bytes(b"fallback")
+ progress(100, "ok")
+ from backend.app.providers.interface import ProviderResult
+
+ return ProviderResult(image_paths=[path])
+
+ class FakeRegistry:
+ def __init__(self) -> None:
+ self.working = WorkingProvider()
+ self.providers = {
+ "a1111": FailingProvider(),
+ "localai": self.working,
+ "dummy": self.working,
+ }
+
+ def get(self, provider_id: str):
+ return self.providers[provider_id]
+
+ def list(self):
+ return list(self.providers.values())
+
+ registry = FakeRegistry()
+ manager = JobManager(registry, PromptHistoryStore(), state_file=tmp_path / "jobs_state.json")
+
+ req = GenerateRequest(prompt="fallback", model="a1111", steps=30, guidance=9.0)
+ job_id = manager.submit(req)
+
+ deadline = time.time() + 10
+ while time.time() < deadline:
+ state = manager.get(job_id)
+ assert state is not None
+ if state.status in {JobStatus.DONE, JobStatus.ERROR}:
+ break
+ time.sleep(0.1)
+
+ state = manager.get(job_id)
+ assert state is not None
+ assert state.status == JobStatus.DONE
+ assert state.image_paths
+ assert registry.working.last_steps == 12
diff --git a/imageforge/backend/tests/test_localai_provider_timeout.py b/imageforge/backend/tests/test_localai_provider_timeout.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b34147483f3763c7012bb1af90f15365387d7d4
--- /dev/null
+++ b/imageforge/backend/tests/test_localai_provider_timeout.py
@@ -0,0 +1,37 @@
+import time
+from pathlib import Path
+
+import pytest
+
+from backend.app.providers.interface import ProviderRequest
+from backend.app.providers.localai_provider import LocalAIProvider
+
+
+class _SlowEngine:
+ def is_available(self) -> bool:
+ return True
+
+ def generate(self, request): # noqa: ANN001
+ time.sleep(2)
+ return None
+
+
+def test_localai_provider_times_out(monkeypatch, tmp_path: Path) -> None:
+ monkeypatch.setenv("IMAGEFORGE_LOCALAI_IMAGE_TIMEOUT_SECONDS", "1")
+
+ provider = LocalAIProvider()
+ provider.engine = _SlowEngine()
+
+ req = ProviderRequest(
+ prompt="test",
+ negative_prompt="",
+ count=1,
+ width=1024,
+ height=1024,
+ seed=42,
+ steps=30,
+ guidance=7.5,
+ )
+
+ with pytest.raises(TimeoutError):
+ provider.generate(req, tmp_path, lambda *_: None, lambda: False)
diff --git a/imageforge/backend/tests/test_maintenance.py b/imageforge/backend/tests/test_maintenance.py
new file mode 100644
index 0000000000000000000000000000000000000000..c47acb5769e62e0f6b41ed1912f13db0e77e8227
--- /dev/null
+++ b/imageforge/backend/tests/test_maintenance.py
@@ -0,0 +1,12 @@
+from pathlib import Path
+
+from backend.app.storage.maintenance import cleanup_outputs
+
+
+def test_cleanup_outputs_removes_old(tmp_path: Path, monkeypatch) -> None:
+ old = tmp_path / "2000-01-01"
+ old.mkdir(parents=True)
+ (old / "x.txt").write_text("x", encoding="utf-8")
+ monkeypatch.setattr("backend.app.storage.maintenance.OUTPUT_DIR", tmp_path)
+ removed = cleanup_outputs(30)
+ assert removed >= 1
diff --git a/imageforge/backend/tests/test_policy.py b/imageforge/backend/tests/test_policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..dfa04f5150040599a4da303d91848cbcff544c09
--- /dev/null
+++ b/imageforge/backend/tests/test_policy.py
@@ -0,0 +1,28 @@
+from backend.app.core.policy import ContentPolicy
+
+
+def test_hard_block_always_denied() -> None:
+ policy = ContentPolicy("internal-relaxed")
+ decision = policy.evaluate("underage explicit scene", admin_override=True)
+ assert not decision.allowed
+ assert decision.reason == "blocked_illegal_content"
+
+
+def test_strict_blocks_sexual_without_override() -> None:
+ policy = ContentPolicy("strict")
+ decision = policy.evaluate("erotic portrait", admin_override=False)
+ assert not decision.allowed
+
+
+def test_relaxed_allows_erotic_non_explicit() -> None:
+ policy = ContentPolicy("internal-relaxed")
+ decision = policy.evaluate("sensual adult portrait", admin_override=False)
+ assert decision.allowed
+
+
+def test_relaxed_explicit_requires_override() -> None:
+ policy = ContentPolicy("internal-relaxed")
+ denied = policy.evaluate("hardcore porn scene", admin_override=False)
+ allowed = policy.evaluate("hardcore porn scene", admin_override=True)
+ assert not denied.allowed
+ assert allowed.allowed
diff --git a/imageforge/backend/tests/test_presets.py b/imageforge/backend/tests/test_presets.py
new file mode 100644
index 0000000000000000000000000000000000000000..94e41498afdcabf7a37a4736b52670a161234a8a
--- /dev/null
+++ b/imageforge/backend/tests/test_presets.py
@@ -0,0 +1,20 @@
+from pathlib import Path
+
+from backend.app.storage.presets import PresetStore
+
+
+def test_preset_store_upsert_list_delete(tmp_path: Path, monkeypatch) -> None:
+ target = tmp_path / "presets.json"
+ monkeypatch.setattr("backend.app.storage.presets.PRESETS_FILE", target)
+ store = PresetStore()
+
+ created = store.upsert({
+ "name": "portrait_fast",
+ "prompt": "portrait",
+ "model": "dummy",
+ "size": "1024x1024",
+ })
+ assert created.name == "portrait_fast"
+ listed = store.list()
+ assert listed and listed[0].name == "portrait_fast"
+ assert store.delete("portrait_fast")
diff --git a/imageforge/backend/tests/test_prompting.py b/imageforge/backend/tests/test_prompting.py
new file mode 100644
index 0000000000000000000000000000000000000000..12f0f29dab8068c1522415b14b7a108674e758ad
--- /dev/null
+++ b/imageforge/backend/tests/test_prompting.py
@@ -0,0 +1,25 @@
+from backend.app.core.prompting import compile_prompts
+
+
+def test_compile_prompts_applies_type_and_style() -> None:
+ result = compile_prompts(
+ prompt="a dragon over city",
+ negative_prompt="",
+ image_type="poster",
+ style_preset="cinematic",
+ style_strength=80,
+ )
+ assert "Target:" in result.prompt
+ assert "style influence 80%" in result.prompt
+ assert "watermark" in result.negative_prompt
+
+
+def test_compile_prompts_merges_negative_prompt() -> None:
+ result = compile_prompts(
+ prompt="logo for bakery",
+ negative_prompt="busy background",
+ image_type="logo",
+ style_preset="minimal",
+ style_strength=50,
+ )
+ assert result.negative_prompt.startswith("busy background")
diff --git a/imageforge/backend/tests/test_recovery.py b/imageforge/backend/tests/test_recovery.py
new file mode 100644
index 0000000000000000000000000000000000000000..084d80bbb515eb498457c803081c3f24c6e2f6cb
--- /dev/null
+++ b/imageforge/backend/tests/test_recovery.py
@@ -0,0 +1,16 @@
+from pathlib import Path
+
+from backend.app.api.schemas import GenerateRequest
+from backend.app.jobs.manager import JobManager
+from backend.app.providers.factory import ProviderRegistry
+from backend.app.storage.history import PromptHistoryStore
+
+
+def test_job_recovery_loads_state(tmp_path: Path) -> None:
+ state_file = tmp_path / "jobs_state.json"
+ mgr1 = JobManager(ProviderRegistry(), PromptHistoryStore(), state_file=state_file)
+ job_id = mgr1.submit(GenerateRequest(prompt="recover", model="dummy"))
+
+ mgr2 = JobManager(ProviderRegistry(), PromptHistoryStore(), state_file=state_file)
+ loaded = mgr2.get(job_id)
+ assert loaded is not None
diff --git a/imageforge/backend/tests/test_security.py b/imageforge/backend/tests/test_security.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b5e652b1c4eea6fc2420c65573bde92dc49d1d0
--- /dev/null
+++ b/imageforge/backend/tests/test_security.py
@@ -0,0 +1,26 @@
+import pytest
+
+from backend.app.core.security import ApiSecurity
+
+
+def test_security_allows_without_required_key(monkeypatch) -> None:
+ monkeypatch.setattr("backend.app.core.security.API_KEYS", "")
+ sec = ApiSecurity()
+ principal = sec.authenticate(api_key=None, client_id="local")
+ assert principal.role == "operator"
+
+
+def test_security_rejects_invalid_key(monkeypatch) -> None:
+ monkeypatch.setattr("backend.app.core.security.API_KEYS", "abc")
+ sec = ApiSecurity()
+ with pytest.raises(Exception):
+ sec.authenticate(api_key="wrong", client_id="local")
+
+
+def test_security_role_mapping(monkeypatch) -> None:
+ monkeypatch.setattr("backend.app.core.security.API_KEYS", "k1:viewer,k2:admin")
+ sec = ApiSecurity()
+ viewer = sec.authenticate(api_key="k1", client_id="local")
+ admin = sec.authenticate(api_key="k2", client_id="local")
+ assert viewer.role == "viewer"
+ assert admin.role == "admin"
diff --git a/imageforge/check_provider_priority.py b/imageforge/check_provider_priority.py
new file mode 100644
index 0000000000000000000000000000000000000000..032c74279e29d9322f571b9a10ebcacad6d55e56
--- /dev/null
+++ b/imageforge/check_provider_priority.py
@@ -0,0 +1,43 @@
+"""Check which providers are available"""
+import os
+import sys
+
+os.environ['HF_HOME'] = 'd:/VSC Codes/Bild/.cache/hf'
+os.environ['TRANSFORMERS_CACHE'] = 'd:/VSC Codes/Bild/.cache/hf'
+
+sys.path.insert(0, 'd:/VSC Codes/Bild/imageforge')
+
+from backend.app.providers.factory import ProviderRegistry
+
+registry = ProviderRegistry()
+
+print("Provider Availability Check:")
+print("=" * 50)
+
+for name, provider in registry._providers.items():
+ try:
+ available = provider.is_available()
+ status = "✓ AVAILABLE" if available else "✗ NOT AVAILABLE"
+ print(f"{name:12s}: {status}")
+
+ if not available and name != "dummy":
+ print(f" ^ This provider should work but isn't available!")
+ except Exception as e:
+ print(f"{name:12s}: ✗ ERROR - {e}")
+
+print("=" * 50)
+
+# Check which provider would be used
+print("\nFallback order from environment:")
+fallback_order = os.getenv("IMAGEFORGE_FALLBACK_MODELS", "localai,diffusion,a1111,dummy")
+print(f" {fallback_order}")
+
+print("\nFirst available provider will be:")
+for provider_name in fallback_order.split(","):
+ provider_name = provider_name.strip()
+ provider = registry.get(provider_name)
+ if provider.is_available():
+ print(f" ✓ {provider_name} (WILL BE USED)")
+ break
+ else:
+ print(f" ✗ {provider_name} (skipped - not available)")
diff --git a/imageforge/check_providers.py b/imageforge/check_providers.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f79b584a01ed48d8ac7de1a6e8fcb3860caaac2
--- /dev/null
+++ b/imageforge/check_providers.py
@@ -0,0 +1,60 @@
+"""Check which providers are available"""
+import os
+import sys
+
+# Set HuggingFace cache
+os.environ['HF_HOME'] = 'd:/VSC Codes/Bild/.cache/hf'
+os.environ['TRANSFORMERS_CACHE'] = 'd:/VSC Codes/Bild/.cache/hf'
+
+print("Checking providers...")
+print()
+
+# Check LocalAI
+try:
+ from backend.app.local_ai.engine import LocalAIEngine
+ engine = LocalAIEngine()
+ available = engine.is_available()
+ print(f"LocalAI Engine:")
+ print(f" Available: {available}")
+ print(f" Model ID: {engine.model_id}")
+
+ if available:
+ try:
+ print(f" Attempting to load model...")
+ pipe = engine._ensure()
+ print(f" ✓ Model loaded successfully!")
+ print(f" Device: {pipe.device}")
+ except Exception as e:
+ print(f" ✗ Failed to load: {e}")
+except Exception as e:
+ print(f"LocalAI: ✗ Error: {e}")
+
+print()
+
+# Check Diffusion Provider
+try:
+ from backend.app.providers.diffusion_provider import DiffusionProvider
+ provider = DiffusionProvider()
+ available = provider.is_available()
+ print(f"Diffusion Provider:")
+ print(f" Available: {available}")
+
+ if available:
+ try:
+ print(f" Testing generation capability...")
+ # Don't actually generate, just check if provider thinks it can
+ print(f" ✓ Provider ready")
+ except Exception as e:
+ print(f" ✗ Error: {e}")
+except Exception as e:
+ print(f"Diffusion Provider: ✗ Error: {e}")
+
+print()
+
+# List all registered providers
+try:
+ from backend.app.providers.registry import list_available_providers
+ providers = list_available_providers()
+ print(f"Registered available providers: {', '.join(providers) if providers else 'NONE (will use dummy!)'}")
+except Exception as e:
+ print(f"Registry check failed: {e}")
diff --git a/imageforge/frontend/.npmrc b/imageforge/frontend/.npmrc
new file mode 100644
index 0000000000000000000000000000000000000000..b6dcc65762c989794f4386f796991d2aaa7d04f3
--- /dev/null
+++ b/imageforge/frontend/.npmrc
@@ -0,0 +1,2 @@
+cache=D:/AI/npm-cache
+logs-dir=D:/AI/npm-cache/_logs
diff --git a/imageforge/frontend/electron/main.js b/imageforge/frontend/electron/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..eb9e14ff2ec77f9837aa7e7545734c1e33d60e69
--- /dev/null
+++ b/imageforge/frontend/electron/main.js
@@ -0,0 +1,191 @@
+const { app, BrowserWindow, dialog, ipcMain, shell, nativeImage } = require("electron");
+const path = require("path");
+const fs = require("fs");
+const { spawn } = require("child_process");
+
+const BACKEND_URL = "http://127.0.0.1:8008/health";
+let backendProcess = null;
+
+function projectRoot() {
+ return path.resolve(__dirname, "..", "..");
+}
+
+function isDev() {
+ return process.env.IMAGEFORGE_DEV === "1";
+}
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+async function waitForBackend(timeoutMs = 20000) {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const response = await fetch(BACKEND_URL);
+ if (response.ok) {
+ return true;
+ }
+ } catch (_) {}
+ await sleep(500);
+ }
+ return false;
+}
+
+function startBackend() {
+ const root = projectRoot();
+ const backendModule = "backend.app.main";
+ const pythonCandidates = process.platform === "win32"
+ ? [
+ path.join(root, ".venv", "Scripts", "python.exe"),
+ path.join(root, "..", ".venv", "Scripts", "python.exe"),
+ "py",
+ "python",
+ ]
+ : [
+ path.join(root, ".venv", "bin", "python"),
+ path.join(root, "..", ".venv", "bin", "python"),
+ "python3",
+ "python",
+ ];
+
+ for (const command of pythonCandidates) {
+ if (command.includes(path.sep) && !fs.existsSync(command)) {
+ continue;
+ }
+ try {
+ const args = command === "py" ? ["-3", "-m", backendModule] : ["-m", backendModule];
+ const child = spawn(command, args, {
+ cwd: root,
+ env: {
+ ...process.env,
+ IMAGEFORGE_HOST: process.env.IMAGEFORGE_HOST || "127.0.0.1",
+ IMAGEFORGE_PORT: process.env.IMAGEFORGE_PORT || "8008",
+ IMAGEFORGE_CORS_ORIGINS: process.env.IMAGEFORGE_CORS_ORIGINS || "http://localhost:5173",
+ IMAGEFORGE_CACHE_ROOT: process.env.IMAGEFORGE_CACHE_ROOT || path.join(root, ".cache"),
+ IMAGEFORGE_TMP_ROOT: process.env.IMAGEFORGE_TMP_ROOT || path.join(root, ".cache", "tmp"),
+ },
+ stdio: "ignore",
+ detached: false,
+ });
+ child.once("error", () => {});
+ backendProcess = child;
+ return;
+ } catch (_) {}
+ }
+}
+
+async function createWindow() {
+ startBackend();
+ const healthy = await waitForBackend();
+ if (!healthy) {
+ dialog.showErrorBox(
+ "Backend not reachable",
+ "ImageForge konnte das lokale Backend nicht starten. Bitte Python und requirements installieren."
+ );
+ }
+
+ const win = new BrowserWindow({
+ width: 1600,
+ height: 920,
+ minWidth: 1100,
+ minHeight: 700,
+ webPreferences: {
+ contextIsolation: true,
+ preload: path.join(__dirname, "preload.js"),
+ nodeIntegration: false,
+ },
+ });
+
+ const devServerUrl = process.env.VITE_DEV_SERVER_URL;
+ if (devServerUrl) {
+ await win.loadURL(devServerUrl);
+ } else {
+ await win.loadFile(path.join(__dirname, "..", "dist", "index.html"));
+ }
+}
+
+ipcMain.handle("open-folder", async (_event, folderPath) => {
+ await shell.openPath(folderPath);
+});
+
+ipcMain.handle("save-image", async (_event, sourcePath, defaultName) => {
+ const result = await dialog.showSaveDialog({
+ title: "Save image",
+ defaultPath: defaultName,
+ filters: [
+ { name: "PNG", extensions: ["png"] },
+ { name: "JPEG", extensions: ["jpg", "jpeg"] },
+ ],
+ });
+ if (result.canceled || !result.filePath) {
+ return false;
+ }
+
+ const ext = path.extname(result.filePath).toLowerCase();
+ if (ext === ".jpg" || ext === ".jpeg") {
+ const img = nativeImage.createFromPath(sourcePath);
+ fs.writeFileSync(result.filePath, img.toJPEG(92));
+ } else {
+ fs.copyFileSync(sourcePath, result.filePath);
+ }
+ return true;
+});
+
+ipcMain.handle("show-error", async (_event, title, message) => {
+ await dialog.showMessageBox({
+ type: "error",
+ title,
+ message,
+ });
+});
+
+ipcMain.handle("pick-image", async () => {
+ const result = await dialog.showOpenDialog({
+ title: "Select source image",
+ properties: ["openFile"],
+ filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "bmp"] }],
+ });
+ if (result.canceled || result.filePaths.length === 0) {
+ return null;
+ }
+ return result.filePaths[0];
+});
+
+ipcMain.handle("read-image-data-url", async (_event, sourcePath) => {
+ try {
+ const root = projectRoot();
+ const outputRoot = path.resolve(root, "output");
+ const resolved = path.resolve(String(sourcePath || ""));
+ if (resolved !== outputRoot && !resolved.startsWith(`${outputRoot}${path.sep}`)) {
+ return null;
+ }
+ if (!fs.existsSync(resolved)) {
+ return null;
+ }
+ const ext = path.extname(resolved).toLowerCase();
+ const mimeMap = {
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".webp": "image/webp",
+ ".bmp": "image/bmp",
+ };
+ const mime = mimeMap[ext] || "application/octet-stream";
+ const data = fs.readFileSync(resolved);
+ return `data:${mime};base64,${data.toString("base64")}`;
+ } catch {
+ return null;
+ }
+});
+
+app.whenReady().then(createWindow);
+
+app.on("window-all-closed", () => {
+ if (backendProcess && !backendProcess.killed) {
+ backendProcess.kill();
+ }
+ if (process.platform !== "darwin") {
+ app.quit();
+ }
+});
diff --git a/imageforge/frontend/electron/preload.js b/imageforge/frontend/electron/preload.js
new file mode 100644
index 0000000000000000000000000000000000000000..a31fa58f0fd8c621aa61a3fd3c5ee43b57c2139c
--- /dev/null
+++ b/imageforge/frontend/electron/preload.js
@@ -0,0 +1,9 @@
+const { contextBridge, ipcRenderer } = require("electron");
+
+contextBridge.exposeInMainWorld("imageForge", {
+ openFolder: (folderPath) => ipcRenderer.invoke("open-folder", folderPath),
+ saveImage: (sourcePath, defaultName) => ipcRenderer.invoke("save-image", sourcePath, defaultName),
+ showError: (title, message) => ipcRenderer.invoke("show-error", title, message),
+ pickImage: () => ipcRenderer.invoke("pick-image"),
+ readImageDataUrl: (sourcePath) => ipcRenderer.invoke("read-image-data-url", sourcePath),
+});
diff --git a/imageforge/frontend/index.html b/imageforge/frontend/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..956df6506131034bb994ebc0dbddc87b9fd9ff70
--- /dev/null
+++ b/imageforge/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ ImageForge
+
+
+
+
+
+
diff --git a/imageforge/frontend/package-lock.json b/imageforge/frontend/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..09bc6d3ff404d4a134872c1c78f2c9268daccae0
--- /dev/null
+++ b/imageforge/frontend/package-lock.json
@@ -0,0 +1,7342 @@
+{
+ "name": "imageforge-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "imageforge-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.0",
+ "@types/node": "^22.13.4",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "@vitejs/plugin-react": "^4.3.4",
+ "concurrently": "^9.1.2",
+ "cross-env": "^7.0.3",
+ "electron": "^34.0.2",
+ "electron-builder": "^25.1.8",
+ "typescript": "^5.7.3",
+ "vite": "^6.1.0",
+ "wait-on": "^8.0.2"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@develar/schema-utils": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
+ "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.0",
+ "ajv-keywords": "^3.4.1"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/@electron/asar": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
+ "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^5.0.0",
+ "glob": "^7.1.6",
+ "minimatch": "^3.0.4"
+ },
+ "bin": {
+ "asar": "bin/asar.js"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/@electron/asar/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@electron/asar/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@electron/asar/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@electron/get": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
+ "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "env-paths": "^2.2.0",
+ "fs-extra": "^8.1.0",
+ "got": "^11.8.5",
+ "progress": "^2.0.3",
+ "semver": "^6.2.0",
+ "sumchecker": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "global-agent": "^3.0.0"
+ }
+ },
+ "node_modules/@electron/notarize": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
+ "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.1",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/osx-sign": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz",
+ "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "compare-version": "^0.1.2",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.0.0",
+ "isbinaryfile": "^4.0.8",
+ "minimist": "^1.2.6",
+ "plist": "^3.0.5"
+ },
+ "bin": {
+ "electron-osx-flat": "bin/electron-osx-flat.js",
+ "electron-osx-sign": "bin/electron-osx-sign.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/isbinaryfile": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
+ "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/rebuild": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz",
+ "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@malept/cross-spawn-promise": "^2.0.0",
+ "chalk": "^4.0.0",
+ "debug": "^4.1.1",
+ "detect-libc": "^2.0.1",
+ "fs-extra": "^10.0.0",
+ "got": "^11.7.0",
+ "node-abi": "^3.45.0",
+ "node-api-version": "^0.2.0",
+ "node-gyp": "^9.0.0",
+ "ora": "^5.1.0",
+ "read-binary-file-arch": "^1.0.6",
+ "semver": "^7.3.5",
+ "tar": "^6.0.5",
+ "yargs": "^17.0.1"
+ },
+ "bin": {
+ "electron-rebuild": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/universal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
+ "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/asar": "^3.2.7",
+ "@malept/cross-spawn-promise": "^2.0.0",
+ "debug": "^4.3.1",
+ "dir-compare": "^4.2.0",
+ "fs-extra": "^11.1.1",
+ "minimatch": "^9.0.3",
+ "plist": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=16.4"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@electron/universal/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/fs-extra": {
+ "version": "11.3.3",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
+ "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@gar/promisify": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
+ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@hapi/address": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
+ "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/formula": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
+ "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/hoek": {
+ "version": "11.0.7",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
+ "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/pinpoint": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
+ "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/tlds": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz",
+ "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/topo": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
+ "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@malept/cross-spawn-promise": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
+ "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/malept"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
+ }
+ ],
+ "license": "Apache-2.0",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
+ "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.0",
+ "lodash": "^4.17.15",
+ "tmp-promise": "^3.0.2"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
+ "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promisify": "^1.1.3",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@npmcli/fs/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@npmcli/move-file": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
+ "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
+ "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.11",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
+ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/plist": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
+ "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*",
+ "xmlbuilder": ">=11.0.1"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/verror": {
+ "version": "1.10.11",
+ "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
+ "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.11",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/7zip-bin": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
+ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "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"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/app-builder-bin": {
+ "version": "5.0.0-alpha.10",
+ "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz",
+ "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/app-builder-lib": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz",
+ "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@develar/schema-utils": "~2.6.5",
+ "@electron/notarize": "2.5.0",
+ "@electron/osx-sign": "1.3.1",
+ "@electron/rebuild": "3.6.1",
+ "@electron/universal": "2.0.1",
+ "@malept/flatpak-bundler": "^0.4.0",
+ "@types/fs-extra": "9.0.13",
+ "async-exit-hook": "^2.0.1",
+ "bluebird-lst": "^1.0.9",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chromium-pickle-js": "^0.2.0",
+ "config-file-ts": "0.2.8-rc1",
+ "debug": "^4.3.4",
+ "dotenv": "^16.4.5",
+ "dotenv-expand": "^11.0.6",
+ "ejs": "^3.1.8",
+ "electron-publish": "25.1.7",
+ "form-data": "^4.0.0",
+ "fs-extra": "^10.1.0",
+ "hosted-git-info": "^4.1.0",
+ "is-ci": "^3.0.0",
+ "isbinaryfile": "^5.0.0",
+ "js-yaml": "^4.1.0",
+ "json5": "^2.2.3",
+ "lazy-val": "^1.0.5",
+ "minimatch": "^10.0.0",
+ "resedit": "^1.7.0",
+ "sanitize-filename": "^1.6.3",
+ "semver": "^7.3.8",
+ "tar": "^6.1.12",
+ "temp-file": "^3.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "dmg-builder": "25.1.8",
+ "electron-builder-squirrel-windows": "25.1.8"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/archiver": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
+ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "async": "^3.2.4",
+ "buffer-crc32": "^0.2.1",
+ "readable-stream": "^3.6.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^2.2.0",
+ "zip-stream": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/archiver-utils/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+ "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-exit-hook": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
+ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
+ "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bluebird-lst": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz",
+ "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "^3.5.5"
+ }
+ },
+ "node_modules/boolean": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
+ "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/builder-util": {
+ "version": "25.1.7",
+ "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz",
+ "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.1.6",
+ "7zip-bin": "~5.2.0",
+ "app-builder-bin": "5.0.0-alpha.10",
+ "bluebird-lst": "^1.0.9",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "cross-spawn": "^7.0.3",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.0",
+ "is-ci": "^3.0.0",
+ "js-yaml": "^4.1.0",
+ "source-map-support": "^0.5.19",
+ "stat-mode": "^1.0.0",
+ "temp-file": "^3.4.0"
+ }
+ },
+ "node_modules/builder-util-runtime": {
+ "version": "9.2.10",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz",
+ "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "sax": "^1.2.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/builder-util/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/builder-util/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/builder-util/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "16.1.3",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
+ "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/fs": "^2.1.0",
+ "@npmcli/move-file": "^2.0.0",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.1.0",
+ "glob": "^8.0.1",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "mkdirp": "^1.0.4",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^9.0.0",
+ "tar": "^6.1.11",
+ "unique-filename": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cacache/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cacache/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001770",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
+ "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chromium-pickle-js": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
+ "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/compare-version": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
+ "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/compress-commons": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
+ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "^0.2.13",
+ "crc32-stream": "^4.0.2",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concurrently": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "4.1.2",
+ "rxjs": "7.8.2",
+ "shell-quote": "1.8.3",
+ "supports-color": "8.1.1",
+ "tree-kill": "1.2.2",
+ "yargs": "17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/config-file-ts": {
+ "version": "0.2.8-rc1",
+ "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz",
+ "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^10.3.12",
+ "typescript": "^5.4.3"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/config-file-ts/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/crc": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+ "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "buffer": "^5.1.0"
+ }
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
+ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/dir-compare": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
+ "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimatch": "^3.0.5",
+ "p-limit": "^3.1.0 "
+ }
+ },
+ "node_modules/dir-compare/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dir-compare/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/dir-compare/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/dmg-builder": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz",
+ "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "fs-extra": "^10.1.0",
+ "iconv-lite": "^0.6.2",
+ "js-yaml": "^4.1.0"
+ },
+ "optionalDependencies": {
+ "dmg-license": "^1.0.11"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/dmg-license": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
+ "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "@types/plist": "^3.0.1",
+ "@types/verror": "^1.10.3",
+ "ajv": "^6.10.0",
+ "crc": "^3.8.0",
+ "iconv-corefoundation": "^1.1.7",
+ "plist": "^3.0.4",
+ "smart-buffer": "^4.0.2",
+ "verror": "^1.10.0"
+ },
+ "bin": {
+ "dmg-license": "bin/dmg-license.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dotenv-expand": {
+ "version": "11.0.7",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
+ "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dotenv": "^16.4.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron": {
+ "version": "34.5.8",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-34.5.8.tgz",
+ "integrity": "sha512-vxLD65mabTzYmEVa9KceMHM0+zO+vqgrhcyNVlmTd0IGV5J7XZ8v/qElm0o4YQ4wPeq7olZkUjZkBQQEdr23/g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/get": "^2.0.0",
+ "@types/node": "^20.9.0",
+ "extract-zip": "^2.0.1"
+ },
+ "bin": {
+ "electron": "cli.js"
+ },
+ "engines": {
+ "node": ">= 12.20.55"
+ }
+ },
+ "node_modules/electron-builder": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz",
+ "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "dmg-builder": "25.1.8",
+ "fs-extra": "^10.1.0",
+ "is-ci": "^3.0.0",
+ "lazy-val": "^1.0.5",
+ "simple-update-notifier": "2.0.0",
+ "yargs": "^17.6.2"
+ },
+ "bin": {
+ "electron-builder": "cli.js",
+ "install-app-deps": "install-app-deps.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz",
+ "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "archiver": "^5.3.1",
+ "builder-util": "25.1.7",
+ "fs-extra": "^10.1.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-publish": {
+ "version": "25.1.7",
+ "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
+ "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/fs-extra": "^9.0.11",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "fs-extra": "^10.1.0",
+ "lazy-val": "^1.0.5",
+ "mime": "^2.5.2"
+ }
+ },
+ "node_modules/electron-publish/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-publish/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-publish/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.286",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+ "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/electron/node_modules/@types/node": {
+ "version": "20.19.33",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
+ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es6-error": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
+ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extsprintf": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
+ "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+ "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/filelist/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gauge": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+ "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/global-agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "es6-error": "^4.1.1",
+ "matcher": "^3.0.0",
+ "roarr": "^2.15.3",
+ "semver": "^7.3.2",
+ "serialize-error": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/global-agent/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/got": {
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-corefoundation": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
+ "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "node-addon-api": "^1.6.3"
+ },
+ "engines": {
+ "node": "^8.11.2 || >=10"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/infer-owner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
+ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+ "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ci-info": "^3.2.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isbinaryfile": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
+ "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/joi": {
+ "version": "18.0.2",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz",
+ "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/address": "^5.1.1",
+ "@hapi/formula": "^3.0.2",
+ "@hapi/hoek": "^11.0.7",
+ "@hapi/pinpoint": "^2.0.1",
+ "@hapi/tlds": "^1.1.1",
+ "@hapi/topo": "^6.0.2",
+ "@standard-schema/spec": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/lazy-val": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
+ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.union": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
+ "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "agentkeepalive": "^4.2.1",
+ "cacache": "^16.1.0",
+ "http-cache-semantics": "^4.1.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-fetch": "^2.0.3",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.3",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^7.0.0",
+ "ssri": "^9.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/matcher": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz",
+ "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz",
+ "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.1.6",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.1.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.87.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
+ "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
+ "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/node-api-version": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
+ "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/node-api-version/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz",
+ "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^10.0.3",
+ "nopt": "^6.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^12.13 || ^14.13 || >=16"
+ }
+ },
+ "node_modules/node-gyp/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
+ "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^1.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+ "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/pe-library": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
+ "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jet2jet"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/plist": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
+ "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
+ "base64-js": "^1.5.1",
+ "xmlbuilder": "^15.1.1"
+ },
+ "engines": {
+ "node": ">=10.4.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-binary-file-arch": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
+ "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "bin": {
+ "read-binary-file-arch": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readdir-glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resedit": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz",
+ "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pe-library": "^0.4.1"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jet2jet"
+ }
+ },
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/responselike": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/roarr": {
+ "version": "2.15.4",
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "detect-node": "^2.0.4",
+ "globalthis": "^1.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "semver-compare": "^1.0.0",
+ "sprintf-js": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sanitize-filename": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+ "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+ "dev": true,
+ "license": "WTFPL OR ISC",
+ "dependencies": {
+ "truncate-utf8-bytes": "^1.0.0"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
+ "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/serialize-error": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "type-fest": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+ "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/ssri": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
+ "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/stat-mode": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
+ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sumchecker": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
+ "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/temp-file": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
+ "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-exit-hook": "^2.0.1",
+ "fs-extra": "^10.0.0"
+ }
+ },
+ "node_modules/temp-file/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/temp-file/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/temp-file/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/tmp-promise": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
+ "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tmp": "^0.2.0"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/truncate-utf8-bytes": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+ "dev": true,
+ "license": "WTFPL",
+ "dependencies": {
+ "utf8-byte-length": "^1.0.1"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-fest": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unique-filename": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
+ "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "unique-slug": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz",
+ "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/utf8-byte-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)"
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/verror": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
+ "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wait-on": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz",
+ "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.12.1",
+ "joi": "^18.0.1",
+ "lodash": "^4.17.21",
+ "minimist": "^1.2.8",
+ "rxjs": "^7.8.2"
+ },
+ "bin": {
+ "wait-on": "bin/wait-on"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/xmlbuilder": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+ "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zip-stream": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
+ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^3.0.4",
+ "compress-commons": "^4.1.2",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/zip-stream/node_modules/archiver-utils": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
+ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^7.2.3",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ }
+ }
+}
diff --git a/imageforge/frontend/package.json b/imageforge/frontend/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..a113333ecc199ae3421df6f34908f1968100b378
--- /dev/null
+++ b/imageforge/frontend/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "imageforge-frontend",
+ "version": "0.1.0",
+ "private": true,
+ "main": "electron/main.js",
+ "scripts": {
+ "dev": "cross-env npm_config_cache=D:/AI/npm-cache TMP=D:/AI/tmp TEMP=D:/AI/tmp concurrently -k \"vite\" \"wait-on tcp:5173 && cross-env ELECTRON_RUN_AS_NODE=0 IMAGEFORGE_DEV=1 VITE_DEV_SERVER_URL=http://localhost:5173 electron ./electron/main.js\"",
+ "dev:web": "vite --host 127.0.0.1 --port 5173",
+ "build": "vite build && electron-builder",
+ "build:renderer": "vite build",
+ "typecheck": "tsc --noEmit",
+ "test:e2e": "playwright test"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/node": "^22.13.4",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "@vitejs/plugin-react": "^4.3.4",
+ "concurrently": "^9.1.2",
+ "cross-env": "^7.0.3",
+ "electron": "^34.0.2",
+ "electron-builder": "^25.1.8",
+ "@playwright/test": "^1.50.0",
+ "typescript": "^5.7.3",
+ "vite": "^6.1.0",
+ "wait-on": "^8.0.2"
+ },
+ "build": {
+ "appId": "local.imageforge.app",
+ "productName": "ImageForge",
+ "directories": {
+ "output": "dist-electron"
+ },
+ "files": [
+ "dist/**/*",
+ "electron/**/*",
+ "../backend/**/*",
+ "../requirements.txt"
+ ],
+ "win": {
+ "target": "nsis"
+ }
+ }
+}
diff --git a/imageforge/frontend/playwright.config.ts b/imageforge/frontend/playwright.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..51c7d28742dba4b1606ae768dcc36eade2a16025
--- /dev/null
+++ b/imageforge/frontend/playwright.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig({
+ testDir: "./tests",
+ retries: 1,
+ use: {
+ baseURL: "http://127.0.0.1:5173",
+ headless: true,
+ },
+ webServer: {
+ command: "npm run dev:web",
+ port: 5173,
+ timeout: 120000,
+ reuseExistingServer: true,
+ },
+});
diff --git a/imageforge/frontend/src/App.tsx b/imageforge/frontend/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..70ee2e39b444ad875819e9b441dbc8d3f4ce85fa
--- /dev/null
+++ b/imageforge/frontend/src/App.tsx
@@ -0,0 +1,770 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+
+type JobStatus = "queued" | "running" | "done" | "error" | "cancelled";
+
+type ModelInfo = {
+ id: string;
+ name: string;
+ available: boolean;
+ description: string;
+};
+
+type JobInfo = {
+ job_id: string;
+ status: JobStatus;
+ progress: number;
+ message: string;
+ image_paths: string[];
+ output_dir: string | null;
+ error: string | null;
+};
+
+type HistoryItem = {
+ prompt: string;
+ negative_prompt: string;
+ timestamp: string;
+};
+
+type Preset = {
+ name: string;
+ prompt: string;
+ negative_prompt: string;
+ model: string;
+ size: string;
+ count: number;
+ steps: number;
+ guidance: number;
+ image_type: string;
+ style_preset: string;
+ style_strength: number;
+ updated_at: string;
+};
+
+type DashboardStats = {
+ queued: number;
+ running: number;
+ done: number;
+ error: number;
+ cancelled: number;
+ total: number;
+ last_24h: number;
+};
+
+type AdminSettings = {
+ content_profile: string;
+ rate_limit_per_minute: number;
+ output_retention_days: number;
+ adult_enabled: boolean;
+};
+
+const API_BASE = "http://127.0.0.1:8008";
+
+function toImageUrl(path: string): string {
+ return `${API_BASE}/image?path=${encodeURIComponent(path)}`;
+}
+
+type ResolvedImageProps = {
+ path: string;
+ alt: string;
+};
+
+function ResolvedImage({ path, alt }: ResolvedImageProps) {
+ const [src, setSrc] = useState(() => toImageUrl(path));
+
+ useEffect(() => {
+ let active = true;
+ setSrc(toImageUrl(path));
+ void window.imageForge.readImageDataUrl(path).then((dataUrl) => {
+ if (active && dataUrl) {
+ setSrc(dataUrl);
+ }
+ });
+ return () => {
+ active = false;
+ };
+ }, [path]);
+
+ return ;
+}
+
+async function api(path: string, init?: RequestInit): Promise {
+ const apiKey = localStorage.getItem("imageforge_api_key") || "";
+ const response = await fetch(`${API_BASE}${path}`, {
+ ...init,
+ headers: {
+ "Content-Type": "application/json",
+ "X-ImageForge-Api-Key": apiKey,
+ ...(init?.headers || {}),
+ },
+ });
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(text || `HTTP ${response.status}`);
+ }
+ return (await response.json()) as T;
+}
+
+function App() {
+ const [activeTab, setActiveTab] = useState<"studio" | "dashboard" | "presets" | "settings">("studio");
+ const [apiKeyInput, setApiKeyInput] = useState(localStorage.getItem("imageforge_api_key") || "");
+ const [adminSettings, setAdminSettings] = useState(null);
+ const lastJobLogSignatureRef = useRef("");
+ const [models, setModels] = useState([]);
+ const [history, setHistory] = useState([]);
+ const [presets, setPresets] = useState([]);
+ const [stats, setStats] = useState(null);
+
+ const [prompt, setPrompt] = useState("");
+ const [batchPrompts, setBatchPrompts] = useState("");
+ const [negativePrompt, setNegativePrompt] = useState("");
+ const [model, setModel] = useState("dummy");
+ const [modelVariant, setModelVariant] = useState("");
+ const [size, setSize] = useState("512x512");
+ const [count, setCount] = useState(1);
+ const [randomSeed, setRandomSeed] = useState(true);
+ const [seed, setSeed] = useState(42);
+ const [steps, setSteps] = useState(18);
+ const [guidance, setGuidance] = useState(6.5);
+ const [imageType, setImageType] = useState("general");
+ const [stylePreset, setStylePreset] = useState("auto");
+ const [styleStrength, setStyleStrength] = useState(60);
+ const [initImagePath, setInitImagePath] = useState(null);
+ const [img2imgStrength, setImg2imgStrength] = useState(0.45);
+
+ const [currentJobId, setCurrentJobId] = useState(null);
+ const [jobs, setJobs] = useState([]);
+ const [jobInfo, setJobInfo] = useState(null);
+ const [logs, setLogs] = useState([]);
+ const [selectedImage, setSelectedImage] = useState(null);
+ const [compareImages, setCompareImages] = useState([]);
+
+ async function refreshModels() {
+ const response = await api<{ value: ModelInfo[]; Count: number }>("/models");
+ const data = Array.isArray(response) ? response : response.value || [];
+ setModels(data);
+ const preferred = ["a1111", "localai", "diffusion", "dummy"];
+ const available =
+ preferred
+ .map((id) => data.find((m) => m.id === id && m.available))
+ .find(Boolean) ||
+ data.find((m) => m.available) ||
+ data[0];
+ if (available) {
+ setModel((prev) => {
+ const current = data.find((m) => m.id === prev);
+ if (!current || !current.available || prev === "dummy") {
+ return available.id;
+ }
+ return prev;
+ });
+ }
+ }
+
+ async function refreshHistory() {
+ const data = await api("/history");
+ setHistory(data);
+ }
+
+ async function refreshJobs() {
+ const data = await api("/jobs");
+ setJobs(data);
+ }
+
+ async function refreshPresets() {
+ const data = await api("/presets");
+ setPresets(data);
+ }
+
+ async function refreshStats() {
+ const data = await api("/dashboard/stats");
+ setStats(data);
+ }
+
+ async function refreshAdminSettings() {
+ try {
+ const data = await api("/admin/settings");
+ setAdminSettings(data);
+ } catch {
+ setAdminSettings(null);
+ }
+ }
+
+ useEffect(() => {
+ void Promise.all([refreshModels(), refreshHistory(), refreshJobs(), refreshPresets(), refreshStats(), refreshAdminSettings()]).catch((err: Error) => {
+ void window.imageForge.showError("Startup Error", err.message);
+ });
+ }, []);
+
+ useEffect(() => {
+ const timer = window.setInterval(() => {
+ void refreshJobs().catch(() => {});
+ void refreshStats().catch(() => {});
+ }, 2000);
+ return () => window.clearInterval(timer);
+ }, []);
+
+ useEffect(() => {
+ if (!currentJobId) {
+ return;
+ }
+ const timer = window.setInterval(() => {
+ void api(`/jobs/${currentJobId}`)
+ .then((info) => {
+ setJobInfo(info);
+ const signature = `${info.status}|${info.progress}|${info.message}`;
+ if (lastJobLogSignatureRef.current !== signature) {
+ lastJobLogSignatureRef.current = signature;
+ setLogs((prev) => {
+ const next = [...prev, `${new Date().toLocaleTimeString()} | ${info.message}`];
+ return next.slice(-200);
+ });
+ }
+ if (info.status === "done") {
+ setSelectedImage(info.image_paths[0] || null);
+ setCurrentJobId(null);
+ void refreshHistory().catch(() => {});
+ void refreshJobs().catch(() => {});
+ }
+ if (info.status === "error" || info.status === "cancelled") {
+ setCurrentJobId(null);
+ if (info.error) {
+ void window.imageForge.showError("Generation Error", info.error);
+ }
+ }
+ })
+ .catch((err: Error) => {
+ setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Poll failed: ${err.message}`]);
+ });
+ }, 1000);
+
+ return () => window.clearInterval(timer);
+ }, [currentJobId]);
+
+ const isGenerating = useMemo(
+ () => Boolean(currentJobId) || jobInfo?.status === "queued" || jobInfo?.status === "running",
+ [currentJobId, jobInfo?.status]
+ );
+
+ function buildPayload(basePrompt: string) {
+ return {
+ prompt: basePrompt,
+ negative_prompt: negativePrompt,
+ model,
+ model_variant: modelVariant.trim() || null,
+ size,
+ count,
+ random_seed: randomSeed,
+ seed: randomSeed ? null : seed,
+ steps,
+ guidance,
+ image_type: imageType,
+ style_preset: stylePreset,
+ style_strength: styleStrength,
+ init_image_path: initImagePath,
+ img2img_strength: img2imgStrength,
+ };
+ }
+
+ async function submitOne(basePrompt: string) {
+ const response = await api<{ job_id: string }>("/generate", {
+ method: "POST",
+ body: JSON.stringify(buildPayload(basePrompt)),
+ });
+ return response.job_id;
+ }
+
+ async function handleGenerate() {
+ if (!prompt.trim()) {
+ await window.imageForge.showError("Validation", "Prompt darf nicht leer sein.");
+ return;
+ }
+ try {
+ setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Submit generation`]);
+ setJobInfo(null);
+ lastJobLogSignatureRef.current = "";
+ setSelectedImage(null);
+ const jobId = await submitOne(prompt.trim());
+ setCurrentJobId(jobId);
+ await refreshJobs();
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ await window.imageForge.showError("Generate Failed", message);
+ }
+ }
+
+ async function handleBatchGenerate() {
+ const lines = batchPrompts
+ .split("\n")
+ .map((line) => line.trim())
+ .filter(Boolean);
+ if (lines.length === 0) {
+ await window.imageForge.showError("Validation", "Batch-Prompts sind leer.");
+ return;
+ }
+ try {
+ for (const line of lines) {
+ await submitOne(line);
+ }
+ setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Batch queued (${lines.length})`]);
+ await refreshJobs();
+ await refreshStats();
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ await window.imageForge.showError("Batch Failed", message);
+ }
+ }
+
+ async function handleCancel() {
+ if (!currentJobId) {
+ return;
+ }
+ await api(`/jobs/${currentJobId}/cancel`, { method: "POST" });
+ setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Cancel requested`]);
+ await refreshJobs();
+ }
+
+ async function handleCancelById(jobId: string) {
+ await api(`/jobs/${jobId}/cancel`, { method: "POST" });
+ await refreshJobs();
+ }
+
+ async function handleRetry(jobId: string) {
+ const out = await api<{ new_job_id: string }>(`/jobs/${jobId}/retry`, { method: "POST" });
+ setCurrentJobId(out.new_job_id);
+ await refreshJobs();
+ }
+
+ async function handleSaveImage() {
+ if (!selectedImage) {
+ return;
+ }
+ const ok = await window.imageForge.saveImage(selectedImage, "imageforge_output.png");
+ if (ok) {
+ setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Image saved`]);
+ }
+ }
+
+ async function handleExportImage(format: "png" | "jpg" | "webp") {
+ if (!selectedImage) {
+ return;
+ }
+ const out = await api<{ output_path: string }>("/export", {
+ method: "POST",
+ body: JSON.stringify({ source_path: selectedImage, format, quality: 92 }),
+ });
+ await window.imageForge.saveImage(out.output_path, `imageforge_export.${format}`);
+ }
+
+ async function handleOpenFolder() {
+ if (jobInfo?.output_dir) {
+ await window.imageForge.openFolder(jobInfo.output_dir);
+ }
+ }
+
+ async function handlePickInitImage() {
+ const path = await window.imageForge.pickImage();
+ if (path) {
+ setInitImagePath(path);
+ }
+ }
+
+ function toggleCompare(path: string) {
+ setCompareImages((prev) => (prev.includes(path) ? prev.filter((p) => p !== path) : [...prev.slice(-3), path]));
+ }
+
+ async function savePreset() {
+ const name = prompt.slice(0, 40) || `preset-${Date.now()}`;
+ const payload = {
+ name,
+ prompt,
+ negative_prompt: negativePrompt,
+ model,
+ size,
+ count,
+ steps,
+ guidance,
+ image_type: imageType,
+ style_preset: stylePreset,
+ style_strength: styleStrength,
+ };
+ await api("/presets", { method: "POST", body: JSON.stringify(payload) });
+ await refreshPresets();
+ }
+
+ function applyHyperrealPortraitPreset() {
+ setImageType("portrait");
+ setStylePreset("photorealistic");
+ setStyleStrength(90);
+ setSteps(48);
+ setGuidance(7.0);
+ setModel((prev) => prev || "localai");
+ setNegativePrompt(
+ "low quality, blurry, bad anatomy, extra fingers, waxy skin, overprocessed face, watermark"
+ );
+ }
+
+ function applyHyperrealProductPreset() {
+ setImageType("product");
+ setStylePreset("photorealistic");
+ setStyleStrength(85);
+ setSteps(42);
+ setGuidance(6.5);
+ setModel((prev) => prev || "localai");
+ setNegativePrompt(
+ "low quality, blurry, noisy, distorted geometry, warped label, watermark, text clutter"
+ );
+ }
+
+ async function applyPreset(p: Preset) {
+ setPrompt(p.prompt);
+ setNegativePrompt(p.negative_prompt);
+ setModel(p.model);
+ setSize(p.size);
+ setCount(p.count);
+ setSteps(p.steps);
+ setGuidance(p.guidance);
+ setImageType(p.image_type);
+ setStylePreset(p.style_preset);
+ setStyleStrength(p.style_strength);
+ setActiveTab("studio");
+ }
+
+ async function deletePreset(name: string) {
+ await api(`/presets/${encodeURIComponent(name)}`, { method: "DELETE" });
+ await refreshPresets();
+ }
+
+ async function saveAdminSettings() {
+ if (!adminSettings) {
+ return;
+ }
+ const out = await api("/admin/settings", {
+ method: "PUT",
+ body: JSON.stringify(adminSettings),
+ });
+ setAdminSettings(out);
+ setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Admin settings updated`]);
+ }
+
+ return (
+
+
+
+
+ {activeTab === "studio" && (
+ <>
+ ImageForge Studio
+
+ Prompt
+
+
+ Negative Prompt
+
+
+ {history.slice(0, 6).map((item, idx) => (
+ setPrompt(item.prompt)}>
+ {item.prompt}
+
+ ))}
+
+
+
+ Batch Queue (ein Prompt pro Zeile)
+
+
+
+ Generate
+ Queue Batch
+ Cancel
+ Hyperreal Portrait
+ Hyperreal Product
+ Save Preset
+ Save As
+ Open Folder
+ void handleExportImage("webp")} disabled={!selectedImage}>Export WEBP
+
+ >
+ )}
+
+ {activeTab === "dashboard" && (
+ <>
+ Operations Dashboard
+ {adminSettings && (
+
+
+ Adult Repo (HF Space) aktivieren
+
+ setAdminSettings({ ...adminSettings, adult_enabled: e.target.checked })
+ }
+ />
+
+
+ void saveAdminSettings()}>Schalter speichern
+
+
+ )}
+
+
Total {stats?.total ?? 0}
+
Queued {stats?.queued ?? 0}
+
Running {stats?.running ?? 0}
+
Done {stats?.done ?? 0}
+
Error {stats?.error ?? 0}
+
24h {stats?.last_24h ?? 0}
+
+
+ {jobs.slice(0, 30).map((j) => (
+
+
{j.job_id}
+
{j.status}
+
{j.progress}%
+
{j.message}
+
+ void handleRetry(j.job_id)}>Retry
+ void handleCancelById(j.job_id)}>Cancel
+
+
+ ))}
+
+ >
+ )}
+
+ {activeTab === "presets" && (
+ <>
+ Preset Library
+
+ {presets.map((preset) => (
+
+
+
{preset.name}
+
{preset.model} | {preset.size} | steps {preset.steps}
+
+
+ void applyPreset(preset)}>Apply
+ void deletePreset(preset.name)}>Delete
+
+
+ ))}
+
+ >
+ )}
+
+ {activeTab === "settings" && (
+ <>
+ Admin Settings
+ {adminSettings ? (
+
+ ) : (
+ No admin permissions for settings.
+ )}
+ >
+ )}
+
+
+
+ {jobInfo?.status ?? "idle"}
+ {jobInfo?.progress ?? 0}%
+
+
+
+ {logs.map((line, idx) => (
+
{line}
+ ))}
+
+
+
+
+
+ Gallery & Compare
+
+ {(jobInfo?.image_paths || []).map((path) => (
+
+ setSelectedImage(path)}>
+
+
+ toggleCompare(path)} /> Compare
+
+ ))}
+
+
+ {selectedImage ?
:
No preview
}
+
+ {compareImages.length > 0 && (
+
+ {compareImages.map((path) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export default App;
diff --git a/imageforge/frontend/src/main.tsx b/imageforge/frontend/src/main.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e94e19936d6ee53380f1f353e47d9f356da683a6
--- /dev/null
+++ b/imageforge/frontend/src/main.tsx
@@ -0,0 +1,25 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./styles.css";
+
+if (!window.imageForge) {
+ window.imageForge = {
+ openFolder: async () => {},
+ saveImage: async () => {
+ window.alert("Save is only available in the desktop app.");
+ return false;
+ },
+ showError: async (title: string, message: string) => {
+ window.alert(`${title}: ${message}`);
+ },
+ pickImage: async () => null,
+ readImageDataUrl: async () => null,
+ };
+}
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+);
diff --git a/imageforge/frontend/src/styles.css b/imageforge/frontend/src/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..5c5749b95b14ad44fc57d11c1fb59bc541060719
--- /dev/null
+++ b/imageforge/frontend/src/styles.css
@@ -0,0 +1,362 @@
+:root {
+ --bg: #eef2ea;
+ --panel: #fcf6eb;
+ --panel-strong: #f3e9d5;
+ --ink: #14281d;
+ --accent: #3a7d44;
+ --accent-2: #b65f28;
+ --line: #d4c8b2;
+ --surface: #fffdf8;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: "Trebuchet MS", "Segoe UI", sans-serif;
+ color: var(--ink);
+ background: radial-gradient(circle at 20% 20%, #f7f2e9, #dde6d8 65%, #ced9c8);
+}
+
+.app-grid {
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: 320px 1fr 360px;
+ gap: 16px;
+ padding: 16px;
+ align-items: stretch;
+}
+
+.sidebar,
+.main-area,
+.gallery {
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ padding: 16px;
+}
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-height: calc(100vh - 32px);
+ overflow: auto;
+ position: sticky;
+ top: 16px;
+}
+
+.sidebar label,
+.main-area label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+textarea,
+input,
+select,
+button {
+ font: inherit;
+}
+
+textarea,
+input,
+select {
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 9px 10px;
+ background: var(--surface);
+ color: var(--ink);
+}
+
+textarea:focus,
+input:focus,
+select:focus,
+button:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 1px;
+}
+
+.row {
+ flex-direction: row !important;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+}
+
+.row-actions {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.main-area {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ padding-bottom: 2px;
+}
+
+.tabs {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 8px;
+}
+
+.tabs button {
+ background: var(--surface);
+ color: var(--ink);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ padding: 8px 10px;
+}
+
+.tab-active {
+ background: var(--accent) !important;
+ color: var(--surface) !important;
+ border-color: var(--accent) !important;
+}
+
+.history-box {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.history-item {
+ background: var(--accent);
+ color: var(--surface);
+ font-size: 0.8rem;
+ padding: 6px 10px;
+ border-radius: 999px;
+}
+
+button {
+ border: 1px solid transparent;
+ border-radius: 10px;
+ padding: 9px 13px;
+ background: var(--accent);
+ color: var(--surface);
+ cursor: pointer;
+ transition: filter 120ms ease;
+}
+
+button:hover {
+ filter: brightness(0.95);
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.actions button:nth-child(2),
+.row-actions button:nth-child(2) {
+ background: var(--accent-2);
+}
+
+h1,
+h2 {
+ margin: 2px 0 8px;
+ line-height: 1.15;
+}
+
+small {
+ color: var(--ink);
+ opacity: 0.8;
+}
+
+.progress-box {
+ background: var(--panel-strong);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ padding: 12px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 280px;
+}
+
+.progress-label {
+ display: flex;
+ justify-content: space-between;
+ font-weight: 700;
+}
+
+progress {
+ width: 100%;
+ height: 14px;
+ margin: 8px 0;
+}
+
+.log-box {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 10px;
+ overflow: auto;
+ flex: 1;
+ font-family: Consolas, monospace;
+ font-size: 0.8rem;
+ line-height: 1.4;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.stat-card {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ padding: 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.job-table {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-height: 320px;
+ overflow: auto;
+}
+
+.job-row {
+ display: grid;
+ grid-template-columns: 140px 90px 70px 1fr auto;
+ gap: 8px;
+ align-items: center;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 8px;
+ background: var(--surface);
+}
+
+.preset-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.preset-item {
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ padding: 10px;
+ background: var(--surface);
+ display: flex;
+ justify-content: space-between;
+ gap: 10px;
+ align-items: center;
+}
+
+.gallery {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-height: calc(100vh - 32px);
+ overflow: auto;
+ position: sticky;
+ top: 16px;
+}
+
+.thumbs {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.thumb {
+ padding: 0;
+ overflow: hidden;
+ height: 120px;
+ border: 1px solid var(--line);
+ background: var(--surface);
+}
+
+.thumb img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.preview {
+ flex: 1;
+ border: 1px dashed var(--line);
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 240px;
+ background: var(--surface);
+ padding: 8px;
+}
+
+.preview img {
+ max-width: 100%;
+ max-height: 100%;
+ border-radius: 8px;
+}
+
+.compare-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.compare-grid img {
+ width: 100%;
+ border-radius: 8px;
+ border: 1px solid var(--line);
+}
+
+.init-image-preview {
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ overflow: hidden;
+ max-height: 180px;
+ background: var(--surface);
+}
+
+.init-image-preview img {
+ width: 100%;
+ height: 100%;
+ max-height: 180px;
+ object-fit: cover;
+}
+
+@media (max-width: 1200px) {
+ .app-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar,
+ .gallery {
+ max-height: none;
+ position: static;
+ overflow: visible;
+ }
+
+ .job-row {
+ grid-template-columns: 1fr;
+ gap: 6px;
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
diff --git a/imageforge/frontend/src/types.d.ts b/imageforge/frontend/src/types.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba3be6c36325e9e3f43b54d5f83e04f0d53be098
--- /dev/null
+++ b/imageforge/frontend/src/types.d.ts
@@ -0,0 +1,13 @@
+export {};
+
+declare global {
+ interface Window {
+ imageForge: {
+ openFolder: (folderPath: string) => Promise;
+ saveImage: (sourcePath: string, defaultName: string) => Promise;
+ showError: (title: string, message: string) => Promise;
+ pickImage: () => Promise;
+ readImageDataUrl: (sourcePath: string) => Promise;
+ };
+ }
+}
diff --git a/imageforge/frontend/tests/e2e-smoke.spec.ts b/imageforge/frontend/tests/e2e-smoke.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..030969708b2826aba676788c14623bfb4e471525
--- /dev/null
+++ b/imageforge/frontend/tests/e2e-smoke.spec.ts
@@ -0,0 +1,17 @@
+import { test, expect } from "@playwright/test";
+
+test("loads dashboard shell", async ({ page }) => {
+ await page.addInitScript(() => {
+ // Shim Electron bridge for browser-only E2E run in CI.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any).imageForge = {
+ openFolder: async () => {},
+ saveImage: async () => true,
+ showError: async () => {},
+ pickImage: async () => null,
+ };
+ });
+ await page.goto("/");
+ await expect(page.getByText("ImageForge Studio")).toBeVisible();
+ await expect(page.getByText("Dashboard")).toBeVisible();
+});
diff --git a/imageforge/frontend/tsconfig.json b/imageforge/frontend/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..25cf52c184ace3e4aa5047c96b1b6a2f8be2cc8f
--- /dev/null
+++ b/imageforge/frontend/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["DOM", "ES2020"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "jsx": "react-jsx",
+ "resolveJsonModule": true,
+ "noEmit": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src"]
+}
diff --git a/imageforge/frontend/vite.config.ts b/imageforge/frontend/vite.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc2727306ae4b8dab7db98566ef214dcdfcccea6
--- /dev/null
+++ b/imageforge/frontend/vite.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ strictPort: true,
+ },
+});
diff --git a/imageforge/package.json b/imageforge/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..c31daf224cc166e4a1ae8da494c64f664bc2d16d
--- /dev/null
+++ b/imageforge/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "imageforge",
+ "private": true,
+ "version": "0.1.0",
+ "scripts": {
+ "dev": "npm --prefix frontend run dev",
+ "dev:stack": "powershell -ExecutionPolicy Bypass -File scripts/start-prod.ps1 -FrontendMode web",
+ "build": "npm --prefix frontend run build",
+ "backend": "python -m backend.app.main",
+ "backend:server": "set IMAGEFORGE_HOST=0.0.0.0&& python -m backend.app.main",
+ "test": "pytest",
+ "test:e2e": "npm --prefix frontend run test:e2e",
+ "start:prod": "powershell -ExecutionPolicy Bypass -File scripts/start-prod.ps1",
+ "backup": "powershell -ExecutionPolicy Bypass -File scripts/backup.ps1",
+ "restore": "powershell -ExecutionPolicy Bypass -File scripts/restore.ps1"
+ }
+}
diff --git a/imageforge/presets.json b/imageforge/presets.json
new file mode 100644
index 0000000000000000000000000000000000000000..86042104da2e95b0e3974d6c9296c5aab526b13a
--- /dev/null
+++ b/imageforge/presets.json
@@ -0,0 +1,30 @@
+[
+ {
+ "name": "Erstelle mir ein Bild einer realistische",
+ "prompt": "Erstelle mir ein Bild einer realistischen Frau",
+ "negative_prompt": "low quality, blurry, bad anatomy, extra fingers, waxy skin, overprocessed face, watermark",
+ "model": "diffusion",
+ "size": "1024x1024",
+ "count": 1,
+ "steps": 48,
+ "guidance": 7.0,
+ "image_type": "portrait",
+ "style_preset": "photorealistic",
+ "style_strength": 90,
+ "updated_at": "2026-02-18T21:27:53.127126+00:00"
+ },
+ {
+ "name": "Eine Realistische Frau\n",
+ "prompt": "Eine Realistische Frau\n",
+ "negative_prompt": "low quality, blurry, bad anatomy, extra fingers, waxy skin, overprocessed face, watermark",
+ "model": "dummy",
+ "size": "1024x1024",
+ "count": 1,
+ "steps": 48,
+ "guidance": 7.0,
+ "image_type": "portrait",
+ "style_preset": "photorealistic",
+ "style_strength": 90,
+ "updated_at": "2026-02-18T19:30:48.195696+00:00"
+ }
+]
\ No newline at end of file
diff --git a/imageforge/prewarm_model.py b/imageforge/prewarm_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbea92c4a3d432ab16e9115068548c1285f8cb15
--- /dev/null
+++ b/imageforge/prewarm_model.py
@@ -0,0 +1,48 @@
+"""Pre-warm the LocalAI model before starting the backend"""
+import os
+import sys
+
+os.environ['HF_HOME'] = 'd:/VSC Codes/Bild/.cache/hf'
+os.environ['TRANSFORMERS_CACHE'] = 'd:/VSC Codes/Bild/.cache/hf'
+
+sys.path.insert(0, 'd:/VSC Codes/Bild/imageforge')
+
+print("Pre-warming LocalAI model...")
+print("This will take 30-60 seconds the first time.\n")
+
+try:
+ from backend.app.local_ai.engine import LocalAIEngine, LocalAIRequest
+
+ print("✓ Imports successful")
+ print("Creating engine...")
+
+ engine = LocalAIEngine()
+
+ if not engine.is_available():
+ print("✗ Engine not available!")
+ sys.exit(1)
+
+ print("✓ Engine available")
+ print(f" Model: {engine.model_id}")
+ print("\nLoading model (this is the slow part)...")
+
+ # Force model load
+ image = engine.generate(LocalAIRequest(
+ prompt="test",
+ negative_prompt="",
+ width=512,
+ height=512,
+ steps=1, # Just 1 step to test loading
+ guidance=7.5,
+ seed=42,
+ ))
+
+ print("\n✓✓✓ Model loaded successfully!")
+ print(f"Generated test image: {image.size}")
+ print("\nThe backend should now start quickly.\n")
+
+except Exception as e:
+ print(f"\n✗ Failed to pre-warm model: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
diff --git a/imageforge/pytest.ini b/imageforge/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..ec78a6b49e47f088f1fef1f2dea4753cdca51648
--- /dev/null
+++ b/imageforge/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+pythonpath = .
+testpaths = backend/tests
diff --git a/imageforge/requirements.txt b/imageforge/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..40289a6f9de036f2716c7e5d1b535b7ff4344d68
--- /dev/null
+++ b/imageforge/requirements.txt
@@ -0,0 +1,11 @@
+fastapi==0.115.8
+uvicorn==0.34.0
+pydantic==2.10.6
+pillow==11.1.0
+pytest==8.3.4
+
+# Optional local AI / diffusion support
+# diffusers>=0.32.0
+# torch>=2.5.0
+# transformers>=4.47.0
+# accelerate>=1.2.0
diff --git a/imageforge/scripts/backup.ps1 b/imageforge/scripts/backup.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..1ad410ec0970376df1c14b9f651377106fdc8a0f
--- /dev/null
+++ b/imageforge/scripts/backup.ps1
@@ -0,0 +1,11 @@
+param(
+ [string]$Target = "backups"
+)
+$ErrorActionPreference = "Stop"
+New-Item -ItemType Directory -Force -Path $Target | Out-Null
+$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
+$dest = Join-Path $Target "imageforge_backup_$stamp"
+New-Item -ItemType Directory -Force -Path $dest | Out-Null
+Copy-Item -Recurse -Force -Path "output" -Destination (Join-Path $dest "output") -ErrorAction SilentlyContinue
+Copy-Item -Force -Path "presets.json","prompt_history.json","settings.json","jobs_state.json","policy_audit.log","app.log" -Destination $dest -ErrorAction SilentlyContinue
+Write-Host "Backup created: $dest"
diff --git a/imageforge/scripts/healthcheck-stack.ps1 b/imageforge/scripts/healthcheck-stack.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..017a3e9c8c9003c5c165d340fc5bf143e5285c91
--- /dev/null
+++ b/imageforge/scripts/healthcheck-stack.ps1
@@ -0,0 +1,48 @@
+param(
+ [string]$BackendUrl = "http://127.0.0.1:8008",
+ [string]$A1111Url = "http://127.0.0.1:7860",
+ [int]$TimeoutSeconds = 60,
+ [switch]$RequireA1111
+)
+
+$ErrorActionPreference = "Stop"
+
+function Wait-Endpoint {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url,
+ [Parameter(Mandatory = $true)][string]$Name,
+ [Parameter(Mandatory = $true)][int]$Timeout
+ )
+
+ $deadline = (Get-Date).AddSeconds($Timeout)
+ do {
+ try {
+ $resp = Invoke-RestMethod -Uri $Url -Method Get -TimeoutSec 5
+ Write-Host "OK $Name -> $Url"
+ return $resp
+ }
+ catch {
+ Start-Sleep -Milliseconds 750
+ }
+ } while ((Get-Date) -lt $deadline)
+
+ throw "$Name not reachable within ${Timeout}s: $Url"
+}
+
+$null = Wait-Endpoint -Url "$BackendUrl/health" -Name "Backend health" -Timeout $TimeoutSeconds
+$null = Wait-Endpoint -Url "$BackendUrl/ready" -Name "Backend readiness" -Timeout $TimeoutSeconds
+
+$models = Wait-Endpoint -Url "$BackendUrl/models" -Name "Backend models" -Timeout $TimeoutSeconds
+$a1111Model = $models | Where-Object { $_.id -eq "a1111" } | Select-Object -First 1
+if ($null -eq $a1111Model) {
+ throw "Model 'a1111' is not registered in backend /models response"
+}
+
+if ($RequireA1111.IsPresent) {
+ $null = Wait-Endpoint -Url "$A1111Url/sdapi/v1/sd-models" -Name "A1111 API" -Timeout $TimeoutSeconds
+ if (-not $a1111Model.available) {
+ throw "A1111 API reachable but backend still reports a1111.available=false"
+ }
+}
+
+Write-Host "Stack readiness check passed."
diff --git a/imageforge/scripts/restore.ps1 b/imageforge/scripts/restore.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..d96402d9e4508ec85671cbc6077ae956b07d3ca4
--- /dev/null
+++ b/imageforge/scripts/restore.ps1
@@ -0,0 +1,8 @@
+param(
+ [Parameter(Mandatory=$true)][string]$Source
+)
+$ErrorActionPreference = "Stop"
+if (!(Test-Path $Source)) { throw "Backup source not found: $Source" }
+Copy-Item -Recurse -Force -Path (Join-Path $Source "output") -Destination "output" -ErrorAction SilentlyContinue
+Copy-Item -Force -Path (Join-Path $Source "presets.json"),(Join-Path $Source "prompt_history.json"),(Join-Path $Source "settings.json"),(Join-Path $Source "jobs_state.json") -Destination "." -ErrorAction SilentlyContinue
+Write-Host "Restore completed from: $Source"
diff --git a/imageforge/scripts/start-prod.ps1 b/imageforge/scripts/start-prod.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..7b6e121ad89f58d60d2f8f5b857759d887f7879b
--- /dev/null
+++ b/imageforge/scripts/start-prod.ps1
@@ -0,0 +1,149 @@
+param(
+ [string]$BackendHost = "127.0.0.1",
+ [int]$BackendPort = 8008,
+ [int]$FrontendPort = 5173,
+ [int]$HealthTimeoutSeconds = 90,
+ [string]$PythonExe = "",
+ [ValidateSet("web", "electron")][string]$FrontendMode = "web",
+ [switch]$SkipFrontend,
+ [switch]$RequireA1111
+)
+
+$ErrorActionPreference = "Stop"
+
+$projectRoot = Split-Path -Parent $PSScriptRoot
+Set-Location $projectRoot
+
+function Resolve-PythonExe {
+ param([string]$Override)
+ if ($Override -and (Test-Path $Override)) {
+ return (Resolve-Path $Override).Path
+ }
+
+ $candidates = @(
+ (Join-Path $projectRoot ".venv\Scripts\python.exe"),
+ (Join-Path (Split-Path -Parent $projectRoot) ".venv\Scripts\python.exe")
+ )
+
+ foreach ($candidate in $candidates) {
+ if (Test-Path $candidate) {
+ return (Resolve-Path $candidate).Path
+ }
+ }
+
+ throw "Python executable not found. Use -PythonExe ."
+}
+
+function Stop-PortListeners {
+ param([int[]]$Ports)
+ foreach ($port in $Ports) {
+ $listeners = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
+ foreach ($listener in $listeners) {
+ try {
+ Stop-Process -Id $listener.OwningProcess -Force -ErrorAction Stop
+ Write-Host "Stopped PID $($listener.OwningProcess) on port $port"
+ }
+ catch {
+ Write-Host "Could not stop PID $($listener.OwningProcess) on port $port"
+ }
+ }
+ }
+}
+
+function Wait-Http {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url,
+ [Parameter(Mandatory = $true)][string]$Name,
+ [Parameter(Mandatory = $true)][int]$TimeoutSeconds
+ )
+
+ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
+ do {
+ try {
+ $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5
+ if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
+ Write-Host "OK $Name -> $Url"
+ return
+ }
+ }
+ catch {
+ Start-Sleep -Milliseconds 800
+ }
+ } while ((Get-Date) -lt $deadline)
+
+ throw "$Name not reachable within ${TimeoutSeconds}s: $Url"
+}
+
+$python = Resolve-PythonExe -Override $PythonExe
+$logsDir = Join-Path $projectRoot "logs"
+New-Item -ItemType Directory -Force -Path $logsDir | Out-Null
+
+$backendLog = Join-Path $logsDir "backend.log"
+$backendErrLog = Join-Path $logsDir "backend.err.log"
+$frontendLog = Join-Path $logsDir "frontend.log"
+$frontendErrLog = Join-Path $logsDir "frontend.err.log"
+
+Stop-PortListeners -Ports @($BackendPort, $FrontendPort)
+
+$backendCmd = @(
+ "Set-Location '$projectRoot'",
+ "`$env:IMAGEFORGE_HOST='$BackendHost'",
+ "`$env:IMAGEFORGE_PORT='$BackendPort'",
+ "`$env:IMAGEFORGE_CORS_ORIGINS='http://localhost:$FrontendPort,http://127.0.0.1:$FrontendPort'",
+ "& '$python' -m backend.app.main"
+) -join "; "
+
+$backendProc = Start-Process -FilePath "powershell.exe" -ArgumentList @(
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "-Command", $backendCmd
+) -RedirectStandardOutput $backendLog -RedirectStandardError $backendErrLog -PassThru
+
+Write-Host "Backend started (PID $($backendProc.Id))."
+
+$frontendProc = $null
+if (-not $SkipFrontend.IsPresent) {
+ $frontendScript = if ($FrontendMode -eq "electron") { "dev" } else { "dev:web" }
+ $frontendCheckUrl = if ($FrontendMode -eq "electron") { "http://localhost:$FrontendPort" } else { "http://127.0.0.1:$FrontendPort" }
+ $frontendCmd = @(
+ "Set-Location '$projectRoot'",
+ "npm --prefix frontend run $frontendScript"
+ ) -join "; "
+
+ $frontendProc = Start-Process -FilePath "powershell.exe" -ArgumentList @(
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "-Command", $frontendCmd
+ ) -RedirectStandardOutput $frontendLog -RedirectStandardError $frontendErrLog -PassThru
+
+ Write-Host "Frontend started (PID $($frontendProc.Id))."
+ Wait-Http -Url $frontendCheckUrl -Name "Frontend" -TimeoutSeconds $HealthTimeoutSeconds
+}
+
+$healthArgs = @(
+ "-BackendUrl", "http://$BackendHost`:$BackendPort",
+ "-TimeoutSeconds", "$HealthTimeoutSeconds"
+)
+if ($RequireA1111.IsPresent) {
+ $healthArgs += "-RequireA1111"
+}
+
+& (Join-Path $PSScriptRoot "healthcheck-stack.ps1") @healthArgs
+
+Write-Host ""
+Write-Host "Ready:"
+Write-Host "- Backend: http://$BackendHost`:$BackendPort"
+if (-not $SkipFrontend.IsPresent) {
+ if ($FrontendMode -eq "electron") {
+ Write-Host "- Frontend mode: electron (with embedded Vite on $FrontendPort)"
+ }
+ else {
+ Write-Host "- Frontend (Web): http://localhost:$FrontendPort"
+ }
+}
+Write-Host "- Backend log: $backendLog"
+Write-Host "- Backend error log: $backendErrLog"
+if (-not $SkipFrontend.IsPresent) {
+ Write-Host "- Frontend log: $frontendLog"
+ Write-Host "- Frontend error log: $frontendErrLog"
+}
\ No newline at end of file
diff --git a/imageforge/setup_model_cache.py b/imageforge/setup_model_cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcd1250e28a65e1c271d73f556b06321faf9cfeb
--- /dev/null
+++ b/imageforge/setup_model_cache.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+"""Download and cache tiny-sd model for offline use."""
+
+import os
+import sys
+from pathlib import Path
+
+# Set cache directory
+cache_dir = Path("d:/VSC Codes/Bild/.cache/hf")
+cache_dir.mkdir(parents=True, exist_ok=True)
+os.environ['HF_HOME'] = str(cache_dir)
+
+print(f"Cache directory: {cache_dir}")
+print()
+
+try:
+ import torch
+ from diffusers import StableDiffusionPipeline
+
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
+ print(f'PyTorch: {torch.__version__}')
+ print(f'CUDA available: {torch.cuda.is_available()}')
+ print(f'Device: {device}')
+ print()
+
+ model_id = 'segmind/tiny-sd'
+ print(f'Downloading {model_id}...')
+ print('This may take a few minutes...')
+ print()
+
+ pipe = StableDiffusionPipeline.from_pretrained(
+ model_id,
+ torch_dtype=torch.float16 if device == 'cuda' else torch.float32,
+ local_files_only=False
+ )
+ pipe.to(device)
+
+ print('✓ SUCCESS: Model cached successfully!')
+ print(f'Pipeline: {type(pipe).__name__}')
+ print(f'Model cache location: {cache_dir}')
+ print()
+ print('Backend can now run offline with local cache.')
+ sys.exit(0)
+
+except Exception as e:
+ print(f'✗ FAILED: {type(e).__name__}: {e}')
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
diff --git a/pixelforge_colab_test.ipynb b/pixelforge_colab_test.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..81345ff119d47580a62f078fe2372eb297547543
--- /dev/null
+++ b/pixelforge_colab_test.ipynb
@@ -0,0 +1,156 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "37de3483",
+ "metadata": {},
+ "source": [
+ "# PixelForge with Z-Image Turbo (Colab Test)\n",
+ "Test stärkere Modelle mit kostenloser GPU"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f0d963b3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# 1. Install dependencies\n",
+ "!pip install -q torch diffusers transformers accelerate numpy pillow requests -U\n",
+ "!pip install -q flask flask-cors python-dotenv\n",
+ "print(\"✓ Dependencies installed\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9f93fdb6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# 2. Check GPU\n",
+ "import torch\n",
+ "print(f\"GPU Available: {torch.cuda.is_available()}\")\n",
+ "if torch.cuda.is_available():\n",
+ " print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n",
+ " print(f\"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e1d69f6e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# 3. Download & Load Model (beispiel: FLUX oder ähnlich)\n",
+ "# Optional: Ersetze mit deinem Z-Image Turbo Modell\n",
+ "\n",
+ "from diffusers import StableDiffusionPipeline\n",
+ "import torch\n",
+ "\n",
+ "MODEL_ID = \"segmind/tiny-sd\" # oder dein Z-Image Turbo\n",
+ "print(f\"Loading {MODEL_ID}...\")\n",
+ "\n",
+ "pipe = StableDiffusionPipeline.from_pretrained(\n",
+ " MODEL_ID,\n",
+ " torch_dtype=torch.float16,\n",
+ " safety_checker=None,\n",
+ " requires_safety_checker=False\n",
+ ")\n",
+ "pipe = pipe.to(\"cuda\")\n",
+ "print(\"✓ Model loaded\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a7575df6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# 4. Test Generation\n",
+ "prompt = \"A beautiful futuristic city at sunset\"\n",
+ "print(f\"Generating: {prompt}\")\n",
+ "\n",
+ "image = pipe(\n",
+ " prompt,\n",
+ " height=512,\n",
+ " width=512,\n",
+ " num_inference_steps=20,\n",
+ " guidance_scale=7.5\n",
+ ").images[0]\n",
+ "\n",
+ "image.save(\"/tmp/test_output.png\")\n",
+ "print(\"✓ Image generated: /tmp/test_output.png\")\n",
+ "image"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4c84cc93",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# 5. Start API Server (optional: expose to PixelForge)\n",
+ "from flask import Flask, request, jsonify\n",
+ "import json\n",
+ "import base64\n",
+ "from io import BytesIO\n",
+ "\n",
+ "app = Flask(__name__)\n",
+ "\n",
+ "@app.route('/health', methods=['GET'])\n",
+ "def health():\n",
+ " return jsonify({\"status\": \"ok\", \"model\": MODEL_ID, \"gpu\": torch.cuda.get_device_name(0)})\n",
+ "\n",
+ "@app.route('/generate', methods=['POST'])\n",
+ "def generate():\n",
+ " data = request.json\n",
+ " prompt = data.get('prompt', 'a test image')\n",
+ " steps = data.get('steps', 20)\n",
+ " guidance = data.get('guidance', 7.5)\n",
+ " \n",
+ " print(f\"Generating: {prompt}\")\n",
+ " image = pipe(\n",
+ " prompt,\n",
+ " height=512,\n",
+ " width=512,\n",
+ " num_inference_steps=steps,\n",
+ " guidance_scale=guidance\n",
+ " ).images[0]\n",
+ " \n",
+ " # Convert to base64\n",
+ " buffered = BytesIO()\n",
+ " image.save(buffered, format=\"PNG\")\n",
+ " img_base64 = base64.b64encode(buffered.getvalue()).decode()\n",
+ " \n",
+ " return jsonify({\n",
+ " \"success\": True,\n",
+ " \"image\": img_base64,\n",
+ " \"prompt\": prompt\n",
+ " })\n",
+ "\n",
+ "# Ngrok tunnel (kostenlos)\n",
+ "from pyngrok import ngrok\n",
+ "ngrok.set_auth_token(\"DEIN_NGROK_TOKEN\") # Gratis auf ngrok.com registrieren\n",
+ "\n",
+ "print(\"Starting API server...\")\n",
+ "public_url = ngrok.connect(5000)\n",
+ "print(f\"Public URL: {public_url}\")\n",
+ "print(\"Use this URL in PixelForge config!\")\n",
+ "\n",
+ "app.run(port=5000)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/run_imageforge_dev.bat b/run_imageforge_dev.bat
new file mode 100644
index 0000000000000000000000000000000000000000..9873bcf46a1f6e2142376893e3d6069d2d2150f6
--- /dev/null
+++ b/run_imageforge_dev.bat
@@ -0,0 +1,3 @@
+@echo off
+cd /d "d:\VSC Codes\Bild\imageforge"
+npm run dev:stack > "d:\VSC Codes\Bild\imageforge\logs\dev_start.log" 2>&1
diff --git a/stable-diffusion-webui b/stable-diffusion-webui
new file mode 160000
index 0000000000000000000000000000000000000000..82a973c04367123ae98bd9abdf80d9eda9b910e2
--- /dev/null
+++ b/stable-diffusion-webui
@@ -0,0 +1 @@
+Subproject commit 82a973c04367123ae98bd9abdf80d9eda9b910e2
diff --git a/start_zimageturbo.bat b/start_zimageturbo.bat
new file mode 100644
index 0000000000000000000000000000000000000000..fc72cc3dfcd8bc251559aec95d166462f3588cce
--- /dev/null
+++ b/start_zimageturbo.bat
@@ -0,0 +1,54 @@
+@echo off
+REM PixelForge mit Z-Image Turbo Start-Skript
+REM Verwendung: start_zimageturbo.bat
+
+setlocal enabledelayedexpansion
+
+echo.
+echo ====================================
+echo PixelForge + Z-Image Turbo Start
+echo ====================================
+echo.
+
+REM 1. Ngrok-URL abfragen
+set /p colab_url="Geben Sie Colab-Ngrok-URL ein (z.B. https://abc123.ngrok.io): "
+
+if "!colab_url!"=="" (
+ echo Fehler: URL ist erforderlich
+ exit /b 1
+)
+
+REM 2. Umgebungsvariablen setzen
+set ZIMAGETURBO_API_URL=!colab_url!
+set ZIMAGETURBO_TIMEOUT=300
+
+echo.
+echo Konfiguriert:
+echo - Z-Image Turbo API: !colab_url!
+echo - Timeout: 300 Sekunden
+echo.
+
+REM 3. Backend starten
+cd /d "d:\VSC Codes\Bild\imageforge"
+echo Starte Backend...
+start "PixelForge Backend" "D:\VSC Codes\Bild\.venv\Scripts\python.exe" -m backend.app.main
+
+ping 127.0.0.1 -n 4 > nul
+
+REM 4. Frontend starten (optional)
+cd /d "d:\VSC Codes\Bild\imageforge\frontend"
+echo Starte Frontend...
+start "PixelForge Frontend" cmd /k npm run dev:web
+
+echo.
+echo ====================================
+echo System bereit!
+echo ====================================
+echo.
+echo Frontend: http://127.0.0.1:5173/
+echo Backend: http://127.0.0.1:8008/
+echo Colab API: !colab_url!
+echo.
+echo Druck Ctrl+C im Backend-Fenster zum Beenden
+echo.
+pause
diff --git a/start_zimageturbo.ps1 b/start_zimageturbo.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..f9ce9aafe6e4ee56bbf910d2214e62f0a5a6ccc6
--- /dev/null
+++ b/start_zimageturbo.ps1
@@ -0,0 +1,72 @@
+#!/usr/bin/env powershell
+# PixelForge mit Z-Image Turbo Start-Skript
+# Verwendung: .\start_zimageturbo.ps1
+
+Write-Host "╔════════════════════════════════════╗" -ForegroundColor Cyan
+Write-Host "║ PixelForge + Z-Image Turbo Start ║" -ForegroundColor Cyan
+Write-Host "╚════════════════════════════════════╝`n" -ForegroundColor Cyan
+
+# 1. Konfiguration abfragen
+Write-Host "Bitte Colab-Ngrok-URL eingeben:" -ForegroundColor Yellow
+Write-Host "(z.B.: https://abc123def456.ngrok.io)" -ForegroundColor Gray
+$colab_url = Read-Host "Ngrok-URL"
+
+if (-not $colab_url -or -not $colab_url.StartsWith("https://")) {
+ Write-Host "✗ Ungültige URL" -ForegroundColor Red
+ exit 1
+}
+
+# 2. Umgebungsvariablen setzen
+$env:ZIMAGETURBO_API_URL = $colab_url
+$env:ZIMAGETURBO_TIMEOUT = "300"
+Write-Host "✓ Z-Image Turbo konfiguriert: $colab_url" -ForegroundColor Green
+
+# 3. Backend starten
+Write-Host "`nStarte Backend..." -ForegroundColor Yellow
+cd "d:/VSC Codes/Bild/imageforge"
+
+$backend_running = $false
+Start-Process -NoNewWindow -FilePath "D:/VSC Codes/Bild/.venv/Scripts/python.exe" `
+ -ArgumentList "-m backend.app.main" `
+ -PassThru | ForEach-Object {
+ Write-Host "Backend gestartet (PID: $($_.Id))" -ForegroundColor Green
+ $backend_running = $true
+ Start-Sleep -Seconds 3
+ }
+
+# 4. Frontend starten (optional)
+Write-Host "Starte Frontend..." -ForegroundColor Yellow
+cd "d:/VSC Codes/Bild/imageforge/frontend"
+Start-Process -NoNewWindow -FilePath "npm" `
+ -ArgumentList "run dev:web" `
+ -PassThru | Out-Null
+
+Start-Sleep -Seconds 2
+
+# 5. Prüfung
+Write-Host "`nPrüfe Services..." -ForegroundColor Yellow
+
+try {
+ $health = Invoke-RestMethod "http://127.0.0.1:8008/health" -TimeoutSec 3
+ Write-Host "✓ Backend OK" -ForegroundColor Green
+} catch {
+ Write-Host "✗ Backend nicht erreichbar" -ForegroundColor Red
+}
+
+try {
+ $fe = Invoke-WebRequest "http://127.0.0.1:5173" -TimeoutSec 3
+ Write-Host "✓ Frontend OK" -ForegroundColor Green
+} catch {
+ Write-Host "⚠ Frontend wird noch gestartet..." -ForegroundColor Yellow
+}
+
+# 6. Finale Informationen
+Write-Host "`n╔════════════════════════════════════╗" -ForegroundColor Green
+Write-Host "║ System bereit zum Testen! ║" -ForegroundColor Green
+Write-Host "╚════════════════════════════════════╝`n" -ForegroundColor Green
+
+Write-Host "Öffne im Browser: http://127.0.0.1:5173/" -ForegroundColor Cyan
+Write-Host "Backend API: http://127.0.0.1:8008/" -ForegroundColor Cyan
+Write-Host "Colab Modell: $colab_url" -ForegroundColor Cyan
+
+Write-Host "`nDrücke Ctrl+C zum Beenden" -ForegroundColor Yellow
diff --git a/test_generation.ps1 b/test_generation.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..d4cc2aad94b9bf5520e7cfb7ec1f29fe6d7afe81
--- /dev/null
+++ b/test_generation.ps1
@@ -0,0 +1,32 @@
+$reqBody = @{
+ prompt = "stunning landscape with mountains, cinematic lighting, 8k, beautiful"
+ negative_prompt = "blurry, low quality, white background"
+ steps = 30
+ provider = "localai"
+} | ConvertTo-Json
+
+Write-Host "Submitting generation request..."
+$job = Invoke-RestMethod -Uri "http://127.0.0.1:8008/generate" -Method POST -Body $reqBody -ContentType "application/json"
+Write-Host "Job submitted: $($job.job_id)"
+$jobId = $job.job_id
+
+# Poll for completion
+for ($i = 0; $i -lt 240; $i++) {
+ $status = Invoke-RestMethod -Uri "http://127.0.0.1:8008/jobs/$jobId" -Method GET
+ Write-Host "[$([DateTime]::Now.ToString('HH:mm:ss'))] Status: $($status.status) | Progress: $($status.progress)%"
+
+ if ($status.status -eq "done") {
+ Write-Host "`nGenerated images: $($status.output_images.Count)"
+ if ($status.output_images.Count -gt 0) {
+ Write-Host "Image path: $($status.output_images[0])"
+
+ # Check file size
+ $file = Get-Item $status.output_images[0] -ErrorAction SilentlyContinue
+ if ($file) {
+ Write-Host "File size: $($file.Length) bytes"
+ }
+ }
+ break
+ }
+ Start-Sleep -Seconds 3
+}
diff --git a/test_imports.py b/test_imports.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4e952ffb05f949b1c38ac79b120f31b4b7cd480
--- /dev/null
+++ b/test_imports.py
@@ -0,0 +1,52 @@
+"""Test if torch and diffusers can be imported"""
+import sys
+import os
+
+os.environ['HF_HOME'] = 'd:/VSC Codes/Bild/.cache/hf'
+os.environ['TRANSFORMERS_CACHE'] = 'd:/VSC Codes/Bild/.cache/hf'
+
+print("Testing torch import...")
+try:
+ import torch
+ print(f"✓ torch imported successfully")
+ print(f" Version: {torch.__version__}")
+ print(f" CUDA available: {torch.cuda.is_available()}")
+ if hasattr(torch.cuda, 'get_device_name') and torch.cuda.is_available():
+ print(f" CUDA device: {torch.cuda.get_device_name(0)}")
+except Exception as e:
+ print(f"✗ Failed to import torch: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+print("\nTesting diffusers import...")
+try:
+ from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline
+ print(f"✓ diffusers imported successfully")
+except Exception as e:
+ print(f"✗ Failed to import diffusers: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+print("\n✓✓✓ All imports successful! Provider should be available.")
+print("\nTesting provider availability...")
+sys.path.insert(0, 'd:/VSC Codes/Bild/imageforge')
+try:
+ from backend.app.providers.localai_provider import LocalAIProvider
+ provider = LocalAIProvider()
+ available = provider.is_available()
+ print(f"LocalAI Provider available: {available}")
+
+ if not available:
+ print("✗ Provider reports as unavailable despite successful imports!")
+ print("This is unexpected - checking engine...")
+ from backend.app.local_ai.engine import LocalAIEngine
+ engine = LocalAIEngine()
+ print(f" Engine available: {engine.is_available()}")
+ else:
+ print("✓ Provider is available!")
+except Exception as e:
+ print(f"✗ Failed to check provider: {e}")
+ import traceback
+ traceback.print_exc()
diff --git a/test_localai.py b/test_localai.py
new file mode 100644
index 0000000000000000000000000000000000000000..f27f3e48466292a6440c707626695dce2aa92ce3
--- /dev/null
+++ b/test_localai.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+"""Test if LocalAI can load tiny-sd model"""
+import os
+import sys
+
+os.environ['HF_HOME'] = 'd:/VSC Codes/Bild/.cache/hf'
+os.environ['TRANSFORMERS_CACHE'] = 'd:/VSC Codes/Bild/.cache/hf/transformers'
+os.chdir('d:/VSC Codes/Bild/imageforge')
+
+print("Testing LocalAI engine...")
+print()
+
+try:
+ from backend.app.local_ai.engine import LocalAIEngine
+
+ engine = LocalAIEngine()
+ print(f"Model ID: {engine.model_id}")
+ print(f"Available: {engine.is_available()}")
+ print()
+
+ if engine.is_available():
+ print("Attempting to load model...")
+ pipe = engine._ensure()
+ print(f"✓ Model loaded successfully!")
+ print(f" Device: {pipe.device}")
+ print(f" Pipeline: {type(pipe).__name__}")
+ sys.exit(0)
+ else:
+ print("✗ LocalAI not available (torch/diffusers not loaded)")
+ sys.exit(1)
+
+except Exception as e:
+ print(f"✗ Error: {type(e).__name__}: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)