diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..aa5c341bd19fcffe7fdaa46e50d06275029d6633 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..440d47393e10ab9cb7bbc01971a715077d7d7afc --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +node_modules/ +dist/ +.venv/ +__pycache__/ +*.pyc +*.pyo +**/artifacts/* +!**/artifacts/.gitkeep +!**/artifacts/runs/ +!**/artifacts/runs/.gitkeep +!**/artifacts/logs/ +!**/artifacts/logs/.gitkeep +.env +.env.* +.vite/ diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c933a303dea51919ab10ac738e668763bc3c957e --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +--- +title: NaturalCAD +emoji: 🧱 +colorFrom: slate +colorTo: blue +sdk: gradio +sdk_version: 4.44.1 +app_file: app.py +pinned: false +--- + +# NaturalCAD + +NaturalCAD is a public prompt-to-CAD demo built around build123d. + +The immediate goal is simple: get it in front of people fast, learn from real prompts, and improve from actual usage instead of guessing in a vacuum. + +## Current app path + +- `app.py` - Hugging Face Space entrypoint +- `requirements.txt` - Space runtime dependencies +- `apps/gradio-demo` - primary MVP app + +## Other repo areas + +- `apps/backend-api` - later-phase backend scaffold if we outgrow a Space-only MVP +- `apps/web-visualizer` - earlier React/Vite prototype +- `docs/` - product and deployment planning +- `archive/` - older or superseded material kept for reference + +## Local run + +```bash +pip install -r requirements.txt +python app.py +``` + +## Deployment posture + +Right now the priority is a lean Hugging Face Space MVP. +If the CAD dependency stack or runtime limits become painful, the frontend can stay on Hugging Face while execution moves to a container or VM later. + +## Key docs + +- `docs/hf-space-mvp.md` +- `docs/hf-space-deploy-checklist.md` +- `docs/publish-checklist.md` +- `docs/backend-v0.md` +- `docs/security-policy-v0.md` diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..969cdc229ef1d920f1331c4a4b26b4478a9bf0ab --- /dev/null +++ b/app.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +SOURCE = ROOT / 'apps' / 'gradio-demo' / 'app' / 'main.py' + +spec = importlib.util.spec_from_file_location('naturalcad_gradio_main', SOURCE) +if spec is None or spec.loader is None: + raise RuntimeError(f'Could not load NaturalCAD app from {SOURCE}') + +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + +demo = module.build_ui() + +if __name__ == '__main__': + demo.launch( + server_name='0.0.0.0', + server_port=7860, + css=""" + #model-viewer {height: 620px !important; border-radius: 18px; overflow: hidden;} + .log-box textarea {font-family: 'JetBrains Mono', monospace; font-size: 13px;} + .gradio-container {max-width: 1380px !important;} + button.primary {font-weight: 700;} + """, + ) diff --git a/apps/backend-api/README.md b/apps/backend-api/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2e25a79311b7c7557c2adf9e6778005bcd4a0ca3 --- /dev/null +++ b/apps/backend-api/README.md @@ -0,0 +1,39 @@ +# NaturalCAD Backend API + +FastAPI backend for NaturalCAD. + +## Purpose +- keep secrets off the Hugging Face Space +- validate and rate-limit public requests +- create jobs and track status +- generate structured CAD specs +- provide a clean place for worker, DB, and storage integration + +## Run locally + +```bash +cd apps/backend-api +python3 -m venv .venv +.venv/bin/pip install -r requirements.txt +.venv/bin/uvicorn app.main:app --reload --port 8010 +``` + +## Initial endpoints +- `GET /v1/health` +- `POST /v1/jobs` +- `GET /v1/jobs/{job_id}` +- `POST /v1/generate-spec` + +## Current integration state +- `apps/gradio-demo` now creates backend jobs through `POST /v1/jobs` +- the backend currently returns a validated in-memory spec +- the Gradio app still performs local build123d execution for now +- next step is moving execution from the Gradio app into a real worker + +## Notes +This is the v0 scaffold. It currently uses in-memory storage by default, but now includes a Postgres schema and a repository layer that can switch to `DATABASE_URL` when Supabase is ready. + +## Supabase readiness +- schema file: `db/schema.sql` +- env placeholders added for `DATABASE_URL` and Supabase keys +- repository layer falls back to memory until the database is configured diff --git a/apps/backend-api/app/__init__.py b/apps/backend-api/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e6eff991d513999ffd09b7fc6b8bd4bb4cf35d8b --- /dev/null +++ b/apps/backend-api/app/__init__.py @@ -0,0 +1 @@ +# NaturalCAD backend package diff --git a/apps/backend-api/app/config.py b/apps/backend-api/app/config.py new file mode 100644 index 0000000000000000000000000000000000000000..8cba811d91b29242bbb7d623b4c79664f1dcd292 --- /dev/null +++ b/apps/backend-api/app/config.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass + +from dotenv import load_dotenv + +load_dotenv() + + +@dataclass(frozen=True) +class Settings: + app_name: str = "NaturalCAD Backend" + app_env: str = os.getenv("APP_ENV", "development") + api_shared_secret: str = os.getenv("API_SHARED_SECRET", "") + rate_limit_per_hour: int = int(os.getenv("RATE_LIMIT_PER_HOUR", "20")) + max_prompt_length: int = int(os.getenv("MAX_PROMPT_LENGTH", "1000")) + database_url: str = os.getenv("DATABASE_URL", "") + supabase_url: str = os.getenv("SUPABASE_URL", "") + supabase_service_role_key: str = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "") + supabase_anon_key: str = os.getenv("SUPABASE_ANON_KEY", "") + + +settings = Settings() diff --git a/apps/backend-api/app/db.py b/apps/backend-api/app/db.py new file mode 100644 index 0000000000000000000000000000000000000000..bd3a9c30c18af5c3ac6002bad0aa65457ec7ee60 --- /dev/null +++ b/apps/backend-api/app/db.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any + +from .config import settings + +try: + import psycopg +except Exception: # noqa: BLE001 + psycopg = None + + +@dataclass +class DatabaseState: + enabled: bool + reason: str | None = None + + +def get_database_state() -> DatabaseState: + if not settings.database_url: + return DatabaseState(enabled=False, reason="DATABASE_URL not configured") + if psycopg is None: + return DatabaseState(enabled=False, reason="psycopg not installed") + return DatabaseState(enabled=True) + + +def connect(): + state = get_database_state() + if not state.enabled: + raise RuntimeError(state.reason or "Database unavailable") + assert psycopg is not None + return psycopg.connect(settings.database_url) + + +def serialize_json(value: Any) -> str: + return json.dumps(value) diff --git a/apps/backend-api/app/main.py b/apps/backend-api/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..4dcff94bd6b87e9729ab6cb79950ba9d05912a05 --- /dev/null +++ b/apps/backend-api/app/main.py @@ -0,0 +1,336 @@ +from __future__ import annotations + +import hashlib +import re +import time +from typing import cast + +from fastapi import FastAPI, Header, HTTPException, Request + +from .config import settings +from .models import ( + CadSpec, + CadStyle, + CreateJobRequest, + GenerateSpecRequest, + GenerateSpecResponse, + HealthResponse, + JobRecord, + ModeType, + OutputType, +) +from .repository import get_job as repo_get_job, save_job +from .store import _CACHE, _JOBS, _REQUESTS + +app = FastAPI(title=settings.app_name, version="0.4.0") + + +def _check_auth(header_value: str | None) -> None: + if settings.api_shared_secret and header_value != settings.api_shared_secret: + raise HTTPException(status_code=401, detail="Invalid shared secret") + + +def _rate_limit_key(request: Request, session_id: str | None) -> str: + client_ip = request.client.host if request.client else "unknown" + return session_id or client_ip + + +def _enforce_rate_limit(key: str) -> None: + now = time.time() + cutoff = now - 3600 + bucket = _REQUESTS[key] + while bucket and bucket[0] < cutoff: + bucket.popleft() + if len(bucket) >= settings.rate_limit_per_hour: + raise HTTPException(status_code=429, detail="Rate limit exceeded") + bucket.append(now) + + +def _normalize_prompt(prompt: str) -> str: + return " ".join(prompt.lower().strip().split()) + + +def _assess_prompt(prompt: str) -> tuple[bool, list[str]]: + reasons: list[str] = [] + suspicious_patterns = [ + r"```", + r"\bimport\b", + r"\bexec\b", + r"\beval\b", + r"__import__", + r"subprocess", + r"os\.system", + r"rm\s+-rf", + r"curl\s+", + r"wget\s+", + ] + for pattern in suspicious_patterns: + if re.search(pattern, prompt): + reasons.append(f"matched:{pattern}") + + if len(prompt) > settings.max_prompt_length: + reasons.append("too_long") + + if prompt.count("\n") > 20: + reasons.append("too_many_lines") + + return bool(reasons), reasons + + +def _prompt_hash(prompt: str, mode: str, output_type: str) -> str: + digest = hashlib.sha256(f"{mode}|{output_type}|{prompt}".encode()).hexdigest() + return digest[:16] + + +def _extract_number(prompt: str, keywords: list[str], default: float) -> float: + for keyword in keywords: + pattern = rf"{keyword}\s*(?:of|=|:)?\s*(\d+(?:\.\d+)?)" + match = re.search(pattern, prompt) + if match: + return float(match.group(1)) + return default + + +def _extract_count(prompt: str, nouns: list[str], default: int) -> int: + word_map = { + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "ten": 10, + } + for noun in nouns: + digit_match = re.search(rf"(\d+)\s+{noun}", prompt) + if digit_match: + return int(digit_match.group(1)) + for word, value in word_map.items(): + if re.search(rf"{word}\s+{noun}", prompt): + return value + return default + + +def _style_from_prompt(prompt: str, default_family: str) -> CadStyle: + heaviness = 0.6 + family = default_family + if any(word in prompt for word in ["heavy", "massive", "thick", "brutal"]): + heaviness = 0.85 + elif any(word in prompt for word in ["light", "slim", "thin", "delicate"]): + heaviness = 0.35 + + if any(word in prompt for word in ["industrial", "steel", "metal"]): + family = "industrial" + elif any(word in prompt for word in ["structural", "truss", "frame"]): + family = "structural" + elif any(word in prompt for word in ["smooth", "soft", "shell", "canopy"]): + family = "smooth" + elif any(word in prompt for word in ["diagram", "profile", "elevation", "line"]): + family = "diagrammatic" + + return CadStyle(family=family, heaviness=heaviness) + + +def _infer_spec(prompt: str, mode: ModeType, output_type: OutputType) -> CadSpec: + p = prompt.lower() + + if output_type == "2d_vector" or mode == "sketch": + family = "truss_elevation" if any(word in p for word in ["truss", "beam", "frame", "elevation"]) else "plate_profile" + if family == "truss_elevation": + params = { + "span": _extract_number(p, ["span", "length", "width"], 140), + "height": _extract_number(p, ["height", "rise"], 24), + "panel_count": _extract_count(p, ["panels", "bays", "segments"], 7), + "member_size": _extract_number(p, ["member", "thickness", "depth"], 3), + "preview_thickness": 1, + } + else: + params = { + "width": _extract_number(p, ["width", "span"], 80), + "height": _extract_number(p, ["height"], 50), + "hole_count": _extract_count(p, ["holes", "bolt holes", "openings"], 4), + "hole_diameter": _extract_number(p, ["hole diameter", "hole", "diameter"], 10), + "preview_thickness": 1, + } + return CadSpec( + output_type="2d_vector", + geometry_family=family, + parameters=params, + style=_style_from_prompt(p, "diagrammatic"), + ) + + if output_type == "surface": + family = "canopy_surface" if any(word in p for word in ["roof", "canopy", "shell", "surface"]) else "lofted_panel" + if family == "canopy_surface": + params = { + "span": _extract_number(p, ["span", "width"], 160), + "depth": _extract_number(p, ["depth", "length"], 90), + "peak_height": _extract_number(p, ["peak", "height", "rise"], 38), + "thickness": _extract_number(p, ["thickness"], 2), + } + else: + params = { + "width": _extract_number(p, ["width", "span"], 80), + "depth": _extract_number(p, ["depth", "length"], 50), + "rise": _extract_number(p, ["rise", "height"], 18), + "thickness": _extract_number(p, ["thickness"], 2), + } + return CadSpec( + output_type="surface", + geometry_family=family, + parameters=params, + style=_style_from_prompt(p, "smooth"), + ) + + if any(word in p for word in ["truss", "beam", "frame", "girder"]): + return CadSpec( + output_type="3d_solid", + geometry_family="truss_beam", + parameters={ + "span": _extract_number(p, ["span", "length"], 140), + "height": _extract_number(p, ["height", "rise"], 24), + "panel_count": _extract_count(p, ["panels", "bays", "segments"], 7), + "member_size": _extract_number(p, ["member", "thickness", "depth"], 3), + }, + style=_style_from_prompt(p, "structural"), + ) + + if any(word in p for word in ["tower", "block", "monolith"]): + return CadSpec( + output_type="3d_solid", + geometry_family="tower_block", + parameters={ + "width": _extract_number(p, ["width"], 30), + "length": _extract_number(p, ["length", "depth"], 30), + "height": _extract_number(p, ["height"], 120), + "notch": _extract_number(p, ["notch", "cut"], 10), + }, + style=_style_from_prompt(p, "industrial"), + ) + + return CadSpec( + output_type="3d_solid", + geometry_family="bracket_plate", + parameters={ + "width": _extract_number(p, ["width", "span"], 80), + "height": _extract_number(p, ["height"], 50), + "thickness": _extract_number(p, ["thickness"], 6), + "hole_count": _extract_count(p, ["holes", "bolt holes", "openings"], 4), + "hole_diameter": _extract_number(p, ["hole diameter", "hole", "diameter"], 10), + }, + style=_style_from_prompt(p, "industrial"), + ) + + +def _generate_spec(payload: GenerateSpecRequest) -> GenerateSpecResponse: + normalized = _normalize_prompt(payload.prompt) + suspicious_input, suspicious_reasons = _assess_prompt(payload.prompt) + key = _prompt_hash(normalized, payload.mode, payload.output_type) + + if key in _CACHE: + cached = dict(_CACHE[key]) + cached["cached"] = True + return GenerateSpecResponse(**cached) + + safe_prompt = normalized + fallback_level = "normal" + notes = [ + f"Mode: {payload.mode}", + f"Output type: {payload.output_type}", + ] + + if suspicious_input: + safe_prompt = "simple industrial bracket plate with 4 holes" + fallback_level = "guardrailed" + notes.extend([ + "Input looked more like code or hostile instructions than a CAD prompt.", + "Using a safe fallback prompt for MVP robustness.", + *[f"Guardrail: {reason}" for reason in suspicious_reasons], + ]) + elif len(normalized.split()) < 3: + fallback_level = "underspecified" + notes.extend([ + "Prompt was underspecified.", + "Using conservative defaults and a simple geometry family.", + ]) + + spec = _infer_spec(safe_prompt, payload.mode, payload.output_type) + response = GenerateSpecResponse( + prompt_hash=key, + spec=spec, + notes=notes + [ + "Prompt mapped into a structured CAD spec.", + "Replace the stub router with a real HF endpoint later.", + ], + suspicious_input=suspicious_input, + fallback_level=fallback_level, + ) + _CACHE[key] = response.model_dump() + return response + + +@app.get("/v1/health", response_model=HealthResponse) +def health() -> HealthResponse: + return HealthResponse( + status="ok", + environment=settings.app_env, + rate_limit_per_hour=settings.rate_limit_per_hour, + cache_entries=len(_CACHE), + jobs_in_memory=len(_JOBS), + ) + + +@app.post("/v1/generate-spec", response_model=GenerateSpecResponse) +def generate_spec(payload: GenerateSpecRequest, request: Request, x_api_key: str | None = Header(default=None)) -> GenerateSpecResponse: + _check_auth(x_api_key) + _enforce_rate_limit(_rate_limit_key(request, payload.session_id)) + return _generate_spec(payload) + + +@app.post("/v1/jobs", response_model=JobRecord) +def create_job(payload: CreateJobRequest, request: Request, x_api_key: str | None = Header(default=None)) -> JobRecord: + _check_auth(x_api_key) + _enforce_rate_limit(_rate_limit_key(request, payload.session_id)) + + if len(payload.prompt.strip()) > settings.max_prompt_length: + raise HTTPException(status_code=400, detail="Prompt too long") + + spec_response = _generate_spec(GenerateSpecRequest(**payload.model_dump())) + job = JobRecord( + status="validated", + prompt=payload.prompt, + mode=payload.mode, + output_type=payload.output_type, + session_id=payload.session_id, + prompt_hash=spec_response.prompt_hash, + spec=spec_response.spec, + notes=[ + "Job created in backend scaffold.", + "Next step: persist to Supabase and enqueue worker execution.", + ], + ) + job.status = cast(str, "queued") + save_job(job) + return job + + +@app.get("/v1/jobs/{job_id}", response_model=JobRecord) +def get_job(job_id: str, x_api_key: str | None = Header(default=None)) -> JobRecord: + _check_auth(x_api_key) + job = repo_get_job(job_id) + if not job: + raise HTTPException(status_code=404, detail="Job not found") + return JobRecord(**job) + + +@app.get("/") +def root() -> dict[str, str]: + return { + "message": settings.app_name, + "docs": "/docs", + "health": "/v1/health", + "jobs": "/v1/jobs", + } diff --git a/apps/backend-api/app/models.py b/apps/backend-api/app/models.py new file mode 100644 index 0000000000000000000000000000000000000000..dd30d1a85e25e98c8693e0a604cb5dde6e55252d --- /dev/null +++ b/apps/backend-api/app/models.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import Literal +from uuid import uuid4 + +from pydantic import BaseModel, Field + +ModeType = Literal["part", "assembly", "sketch"] +OutputType = Literal["2d_vector", "surface", "3d_solid"] +JobStatus = Literal["submitted", "validated", "queued", "running", "completed", "failed"] + + +class GenerateSpecRequest(BaseModel): + prompt: str = Field(min_length=3, max_length=1000) + mode: ModeType = "part" + output_type: OutputType = "3d_solid" + session_id: str | None = None + + +class CreateJobRequest(BaseModel): + prompt: str = Field(min_length=3, max_length=1000) + mode: ModeType = "part" + output_type: OutputType = "3d_solid" + session_id: str | None = None + + +class CadStyle(BaseModel): + family: str = "industrial" + heaviness: float = 0.6 + + +class CadSpec(BaseModel): + output_type: OutputType + geometry_family: str + units: str = "mm" + parameters: dict[str, int | float | str] + style: CadStyle + + +class GenerateSpecResponse(BaseModel): + ok: bool = True + cached: bool = False + prompt_hash: str + spec: CadSpec + notes: list[str] = [] + model: str = "stub/template-router" + suspicious_input: bool = False + fallback_level: str = "normal" + + +class JobRecord(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + status: JobStatus = "submitted" + prompt: str + mode: ModeType = "part" + output_type: OutputType = "3d_solid" + session_id: str | None = None + prompt_hash: str | None = None + spec: CadSpec | None = None + notes: list[str] = [] + error: str | None = None + + +class HealthResponse(BaseModel): + status: str + environment: str + rate_limit_per_hour: int + cache_entries: int + jobs_in_memory: int diff --git a/apps/backend-api/app/repository.py b/apps/backend-api/app/repository.py new file mode 100644 index 0000000000000000000000000000000000000000..fe2b9242202eb0900fd7efe8571555836e192cee --- /dev/null +++ b/apps/backend-api/app/repository.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import Any + +from .db import connect, get_database_state, serialize_json +from .models import JobRecord +from .store import _JOBS + + +def save_job(job: JobRecord) -> None: + db_state = get_database_state() + if not db_state.enabled: + _JOBS[job.id] = job.model_dump() + return + + with connect() as conn: + with conn.cursor() as cur: + cur.execute( + """ + insert into jobs ( + id, status, prompt, mode, output_type, client_session_id, + prompt_hash, spec_json, notes_json, error_text + ) + values (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, %s) + on conflict (id) do update set + status = excluded.status, + prompt = excluded.prompt, + mode = excluded.mode, + output_type = excluded.output_type, + client_session_id = excluded.client_session_id, + prompt_hash = excluded.prompt_hash, + spec_json = excluded.spec_json, + notes_json = excluded.notes_json, + error_text = excluded.error_text, + updated_at = now() + """, + ( + job.id, + job.status, + job.prompt, + job.mode, + job.output_type, + job.session_id, + job.prompt_hash, + serialize_json(job.spec.model_dump() if job.spec else None), + serialize_json(job.notes), + job.error, + ), + ) + conn.commit() + + +def get_job(job_id: str) -> dict[str, Any] | None: + db_state = get_database_state() + if not db_state.enabled: + return _JOBS.get(job_id) + + with connect() as conn: + with conn.cursor() as cur: + cur.execute( + """ + select id, status, prompt, mode, output_type, client_session_id, + prompt_hash, spec_json, notes_json, error_text + from jobs + where id = %s + """, + (job_id,), + ) + row = cur.fetchone() + if not row: + return None + + id_, status, prompt, mode, output_type, client_session_id, prompt_hash, spec_json, notes_json, error_text = row + return { + "id": str(id_), + "status": status, + "prompt": prompt, + "mode": mode, + "output_type": output_type, + "session_id": client_session_id, + "prompt_hash": prompt_hash, + "spec": spec_json, + "notes": notes_json or [], + "error": error_text, + } diff --git a/apps/backend-api/app/store.py b/apps/backend-api/app/store.py new file mode 100644 index 0000000000000000000000000000000000000000..13df07f4a57dc11f1d10218e7b5d784de4ae72b3 --- /dev/null +++ b/apps/backend-api/app/store.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from collections import defaultdict, deque + +_REQUESTS: dict[str, deque[float]] = defaultdict(deque) +_CACHE: dict[str, dict] = {} +_JOBS: dict[str, dict] = {} diff --git a/apps/backend-api/db/README.md b/apps/backend-api/db/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d7e6e60a336668f0862bfa41d2f50174e7e8494c --- /dev/null +++ b/apps/backend-api/db/README.md @@ -0,0 +1,12 @@ +# NaturalCAD backend database + +This folder holds the first Postgres schema for NaturalCAD v0. + +## Target +- Supabase Postgres + +## Files +- `schema.sql` - initial jobs, artifacts, audit_events, and rate_limits tables + +## Notes +This is the first persistence layer replacing the in-memory backend store. Apply this schema to Supabase once the project/account is ready. diff --git a/apps/backend-api/db/schema.sql b/apps/backend-api/db/schema.sql new file mode 100644 index 0000000000000000000000000000000000000000..03cd421f295e23c20d7cad7efe092212764d18c7 --- /dev/null +++ b/apps/backend-api/db/schema.sql @@ -0,0 +1,59 @@ +-- NaturalCAD Backend v0 schema +-- Target: Supabase Postgres + +create extension if not exists pgcrypto; + +create table if not exists jobs ( + id uuid primary key default gen_random_uuid(), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + status text not null check (status in ('submitted', 'validated', 'queued', 'running', 'completed', 'failed')), + prompt text not null, + normalized_prompt text, + mode text not null, + output_type text not null, + client_session_id text, + prompt_hash text, + spec_json jsonb, + error_text text, + model_info_json jsonb, + notes_json jsonb not null default '[]'::jsonb +); + +create index if not exists idx_jobs_status on jobs (status); +create index if not exists idx_jobs_created_at on jobs (created_at desc); +create index if not exists idx_jobs_prompt_hash on jobs (prompt_hash); + +create table if not exists artifacts ( + id uuid primary key default gen_random_uuid(), + created_at timestamptz not null default now(), + job_id uuid not null references jobs(id) on delete cascade, + kind text not null check (kind in ('stl', 'step', 'preview', 'log')), + storage_key text not null, + size_bytes bigint, + expires_at timestamptz +); + +create index if not exists idx_artifacts_job_id on artifacts (job_id); +create index if not exists idx_artifacts_kind on artifacts (kind); + +create table if not exists audit_events ( + id uuid primary key default gen_random_uuid(), + created_at timestamptz not null default now(), + job_id uuid references jobs(id) on delete cascade, + event_type text not null, + details_json jsonb not null default '{}'::jsonb +); + +create index if not exists idx_audit_events_job_id on audit_events (job_id); +create index if not exists idx_audit_events_event_type on audit_events (event_type); + +create table if not exists rate_limits ( + id uuid primary key default gen_random_uuid(), + created_at timestamptz not null default now(), + key text not null, + window_start timestamptz not null, + request_count integer not null default 0 +); + +create index if not exists idx_rate_limits_key_window on rate_limits (key, window_start desc); diff --git a/apps/backend-api/requirements.txt b/apps/backend-api/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..7d6307684d94a6885330621bca4c30e0a06edc26 --- /dev/null +++ b/apps/backend-api/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +pydantic>=2.8.0 +python-dotenv>=1.0.1 +httpx>=0.27.0 +psycopg[binary]>=3.2.0 diff --git a/apps/gradio-demo/.gitignore b/apps/gradio-demo/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..74d5312409b5c9859ab0717c1705e047c764bdbb --- /dev/null +++ b/apps/gradio-demo/.gitignore @@ -0,0 +1,5 @@ +.venv/ +__pycache__/ +artifacts/model.stl +artifacts/model.step +artifacts/runs/ diff --git a/apps/gradio-demo/HF_SPACE_NOTES.md b/apps/gradio-demo/HF_SPACE_NOTES.md new file mode 100644 index 0000000000000000000000000000000000000000..0397592366feff0d1ff3342a1dd62ce67d37688f --- /dev/null +++ b/apps/gradio-demo/HF_SPACE_NOTES.md @@ -0,0 +1,21 @@ +# NaturalCAD HF Space Notes + +## Current intent +- Public-facing NaturalCAD app +- build123d-backed execution loop +- Noah will wire a service endpoint for LLM generation later + +## Current prototype state +- Gradio UI +- real build123d execution +- STL preview +- STL + STEP downloads +- starter sample picker +- prompt note field for future LLM integration +- archived per-run artifacts under `artifacts/runs/` + +## Next likely steps +- add endpoint config pattern for external LLM service +- convert prompt note into real prompt-to-code flow +- improve public-facing examples +- add safe execution constraints for Spaces diff --git a/apps/gradio-demo/README.md b/apps/gradio-demo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0f99e6e37f65b52a8bcd64cf127b005c86f75cbc --- /dev/null +++ b/apps/gradio-demo/README.md @@ -0,0 +1,54 @@ +# NaturalCAD + +Gradio prototype for NaturalCAD, a public natural-language CAD modeler built on build123d. + +## Purpose + +- Fast Hugging Face Spaces deployment +- Test prompt → spec → CAD loop +- Validate interaction model before deeper productization +- Keep the MVP portable enough to offload execution later if Space limits become a problem + +## Features + +- Prompt-driven model generation through the NaturalCAD backend when available +- Local fallback generation if the backend is unavailable +- Run build123d geometry and see STL preview +- Download STL and STEP exports +- View backend + execution logs +- Lightweight run logging for MVP testing data (`artifacts/logs/runs.jsonl`) + +## Run locally + +Start the backend first: + +```bash +cd ../backend-api +python3 -m venv .venv +.venv/bin/pip install -r requirements.txt +.venv/bin/uvicorn app.main:app --reload --port 8010 +``` + +Then run the Gradio app: + +```bash +pip install -r requirements.txt +python app/main.py +``` + +Current Space-oriented dependency note: +- `build123d==0.10.0` is now declared directly in `requirements.txt` +- if Hugging Face Space cannot reliably support the CAD dependency stack, we can keep the UI there and offload execution to a container or VM later without changing the product direction + +Optional environment variables: +- `NATURALCAD_BACKEND_URL` (leave unset for a pure Space-only MVP, or set it to enable backend-assisted spec generation) +- `NATURALCAD_API_KEY` +- `NATURALCAD_BACKEND_TIMEOUT` (default `4` seconds) +- `BUILD123D_PYTHON` (defaults to the current Python runtime, which is better for Hugging Face Space deployment) + +Runtime artifacts: +- latest files in `artifacts/` +- archived runs in `artifacts/runs/` +- lightweight run logs in `artifacts/logs/runs.jsonl` + +Open http://localhost:7860 \ No newline at end of file diff --git a/apps/gradio-demo/app/main.py b/apps/gradio-demo/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..5931e96f7af15bb49876a6e213892f44d2d62034 --- /dev/null +++ b/apps/gradio-demo/app/main.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +"""Gradio app for live build123d geometry execution and export.""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import time +import traceback +import uuid +from datetime import datetime, timezone +from pathlib import Path +from urllib import error, request + +import gradio as gr + +BUILD123D_PYTHON = os.getenv("BUILD123D_PYTHON", sys.executable) +BACKEND_URL = os.getenv("NATURALCAD_BACKEND_URL", os.getenv("NL_CAD_BACKEND_URL", "")).strip() +BACKEND_API_KEY = os.getenv("NATURALCAD_API_KEY", os.getenv("NL_CAD_API_KEY", "")) +BACKEND_TIMEOUT_SECONDS = float(os.getenv("NATURALCAD_BACKEND_TIMEOUT", "4")) +ARTIFACTS_DIR = Path(__file__).parent.parent / "artifacts" +RUNS_DIR = ARTIFACTS_DIR / "runs" +LOGS_DIR = ARTIFACTS_DIR / "logs" +RUN_LOG_PATH = LOGS_DIR / "runs.jsonl" +ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True) +RUNS_DIR.mkdir(parents=True, exist_ok=True) +LOGS_DIR.mkdir(parents=True, exist_ok=True) + +EXAMPLE_PROMPTS = [ + ["Heavy steel bracket with 4 bolt holes, 90 mm wide, 8 mm thick", "part", "3d_solid"], + ["Light structural truss beam with 9 panels and a 180 mm span", "part", "3d_solid"], + ["Industrial notched tower block, 140 mm tall", "part", "3d_solid"], + ["Smooth roof canopy surface, 200 mm span, shallow rise", "part", "surface"], + ["Bracket plate profile with 6 holes for a laser-cut sketch", "sketch", "2d_vector"], +] + +DEFAULT_CODE = '''from build123d import * + +width = 80 +height = 50 +thickness = 6 +hole_diameter = 10 + +with BuildPart() as bp: + with BuildSketch(Plane.XY) as base: + Rectangle(width, height) + with GridLocations(width * 0.6, height * 0.6, 2, 2): + Circle(hole_diameter / 2) + extrude(amount=thickness) + +result = bp.part +''' + + +def render_code_from_spec(spec: dict) -> str: + geometry_family = spec.get("geometry_family", "bracket_plate") + output_type = spec.get("output_type", "3d_solid") + params = spec.get("parameters", {}) + + if geometry_family == "tower_block": + width = params.get("width", 30) + length = params.get("length", 30) + height = params.get("height", 120) + notch = params.get("notch", 10) + return f'''from build123d import * + +width = {width} +length = {length} +height = {height} +notch = {notch} + +with BuildPart() as bp: + Box(width, length, height) + with Locations((0, 0, height / 4), (0, 0, -height / 4)): + Box(width + 2, notch, notch, mode=Mode.SUBTRACT) + Box(notch, length + 2, notch, mode=Mode.SUBTRACT) + +result = bp.part +''' + + if geometry_family == "truss_beam": + span = params.get("span", 140) + height = params.get("height", 24) + panel_count = max(3, int(params.get("panel_count", 7))) + member_size = params.get("member_size", 3) + return f'''from build123d import * + +span = {span} +height = {height} +chord = {member_size} +post = {member_size} +panel_count = {panel_count} + +with BuildPart() as bp: + with Locations((0, 0, chord / 2)): + Box(span, chord, chord) + with Locations((0, 0, height - chord / 2)): + Box(span, chord, chord) + + panel = span / panel_count + post_locations = [(-span / 2 + i * panel, 0, height / 2) for i in range(panel_count + 1)] + with Locations(*post_locations): + Box(post, chord, height) + +result = bp.part +''' + + if geometry_family == "truss_elevation": + span = params.get("span", 140) + height = params.get("height", 24) + panel_count = max(3, int(params.get("panel_count", 7))) + member_size = params.get("member_size", 3) + preview_thickness = params.get("preview_thickness", 1) + return f'''from build123d import * + +span = {span} +height = {height} +panel_count = {panel_count} +member_size = {member_size} +preview_thickness = {preview_thickness} + +with BuildPart() as bp: + with BuildSketch(Plane.XY) as sk: + Rectangle(span, member_size, align=(Align.CENTER, Align.CENTER)) + with Locations((0, height)): + Rectangle(span, member_size, align=(Align.CENTER, Align.CENTER)) + panel = span / panel_count + for i in range(panel_count + 1): + x = -span / 2 + i * panel + with Locations((x, height / 2)): + Rectangle(member_size, height, align=(Align.CENTER, Align.CENTER)) + extrude(amount=preview_thickness) + +result = bp.part +''' + + if geometry_family in {"canopy_surface", "lofted_panel"} or output_type == "surface": + if geometry_family == "canopy_surface": + span = params.get("span", 160) + depth = params.get("depth", 90) + peak_height = params.get("peak_height", 38) + thickness = params.get("thickness", 2) + return f'''from build123d import * + +span = {span} +depth = {depth} +peak_height = {peak_height} +thickness = {thickness} + +with BuildPart() as bp: + with BuildSketch(Plane.XY.offset(0)) as s1: + Rectangle(span, depth) + with BuildSketch(Plane.XY.offset(peak_height)) as s2: + Rectangle(span * 0.65, depth * 0.65) + loft() + offset(amount=thickness) + +result = bp.part +''' + width = params.get("width", 80) + depth = params.get("depth", 50) + rise = params.get("rise", 18) + thickness = params.get("thickness", 2) + return f'''from build123d import * + +width = {width} +depth = {depth} +rise = {rise} +thickness = {thickness} + +with BuildPart() as bp: + with BuildSketch(Plane.XY.offset(0)) as s1: + Rectangle(width, depth) + with BuildSketch(Plane.XY.offset(rise)) as s2: + Rectangle(width * 0.55, depth * 0.55) + loft() + offset(amount=thickness) + +result = bp.part +''' + + width = params.get("width", 80) + height = params.get("height", 50) + hole_count = max(1, int(params.get("hole_count", 4))) + hole_diameter = params.get("hole_diameter", 10) + x_count = max(1, round(hole_count ** 0.5)) + y_count = max(1, (hole_count + x_count - 1) // x_count) + + if output_type == "2d_vector": + preview_thickness = params.get("preview_thickness", 1) + return f'''from build123d import * + +width = {width} +height = {height} +hole_diameter = {hole_diameter} +preview_thickness = {preview_thickness} + +with BuildPart() as bp: + with BuildSketch(Plane.XY) as base: + Rectangle(width, height) + with GridLocations(width * 0.6, height * 0.6, {x_count}, {y_count}): + Circle(hole_diameter / 2, mode=Mode.SUBTRACT) + extrude(amount=preview_thickness) + +result = bp.part +''' + + thickness = params.get("thickness", 6) + return f'''from build123d import * + +width = {width} +height = {height} +thickness = {thickness} +hole_diameter = {hole_diameter} + +with BuildPart() as bp: + with BuildSketch(Plane.XY) as base: + Rectangle(width, height) + with GridLocations(width * 0.6, height * 0.6, {x_count}, {y_count}): + Circle(hole_diameter / 2, mode=Mode.SUBTRACT) + extrude(amount=thickness) + +result = bp.part +''' + + +def create_job(prompt: str, mode: str, output_type: str) -> tuple[dict | None, str]: + if not prompt.strip(): + return None, "" + + if not BACKEND_URL: + return None, json.dumps({"info": "backend disabled", "detail": "No NATURALCAD_BACKEND_URL configured."}, indent=2) + + payload = json.dumps({"prompt": prompt, "mode": mode, "output_type": output_type}).encode() + headers = {"Content-Type": "application/json"} + if BACKEND_API_KEY: + headers["x-api-key"] = BACKEND_API_KEY + + req = request.Request( + f"{BACKEND_URL.rstrip('/')}/v1/jobs", + data=payload, + headers=headers, + method="POST", + ) + + try: + with request.urlopen(req, timeout=BACKEND_TIMEOUT_SECONDS) as response: + data = json.loads(response.read().decode()) + return data, json.dumps(data, indent=2) + except error.HTTPError as exc: + detail = exc.read().decode() if exc.fp else str(exc) + return None, json.dumps({"error": f"backend http {exc.code}", "detail": detail}, indent=2) + except Exception as exc: # noqa: BLE001 + return None, json.dumps({"error": f"backend unavailable: {exc}"}, indent=2) + + +def _append_run_log(entry: dict) -> None: + with RUN_LOG_PATH.open("a", encoding="utf-8") as fh: + fh.write(json.dumps(entry) + "\n") + + +def run_build123d(code: str, prompt: str = "") -> tuple[str | None, str | None, str, str, str | None, float]: + if not code or not code.strip(): + return None, None, "No code provided.", "No geometry was generated.", None, 0.0 + + logs: list[str] = [] + stl_path: str | None = None + step_path: str | None = None + started_at = time.time() + run_id = uuid.uuid4().hex[:8] + + with tempfile.TemporaryDirectory() as tmpdir: + source_file = Path(tmpdir) / "user_script.py" + source_file.write_text(code) + stl_file = RUNS_DIR / f"{run_id}.stl" + step_file = RUNS_DIR / f"{run_id}.step" + + logs.append(f"Run ID: {run_id}") + if prompt.strip(): + logs.append(f"Prompt: {prompt.strip()}") + logs.append("Running build123d script...") + + runner_code = f''' +import sys +from pathlib import Path +from build123d import export_stl, export_step + +source_path = Path(r"{source_file}") +user_globals = {{}} +exec(compile(source_path.read_text(), str(source_path), "exec"), user_globals) + +candidate = user_globals.get("result") +if candidate is None: + sys.exit("No `result` geometry found after execution.") + +def coerce_shape(obj): + if obj is None: + return None + if hasattr(obj, "wrapped"): + return obj + for attr in ("part", "shape", "solid", "obj"): + value = getattr(obj, attr, None) + if value is not None and not callable(value): + obj = value + if hasattr(obj, "wrapped"): + return obj + return obj + +shape = coerce_shape(candidate) +if shape is None: + sys.exit("Could not extract exportable shape from `result`.") + +export_stl(shape, r"{stl_file}") +export_step(shape, r"{step_file}") +print("STL exported to {stl_file}") +print("STEP exported to {step_file}") +''' + + runner_file = Path(tmpdir) / "_runner.py" + runner_file.write_text(runner_code) + + try: + result = subprocess.run( + [BUILD123D_PYTHON, str(runner_file)], + capture_output=True, + text=True, + timeout=60, + ) + if result.stdout: + logs.append(result.stdout.strip()) + if result.stderr: + logs.append(f"[stderr] {result.stderr.strip()}") + if result.returncode == 0 and stl_file.exists() and step_file.exists(): + latest_stl = ARTIFACTS_DIR / "model.stl" + latest_step = ARTIFACTS_DIR / "model.step" + shutil.copy2(stl_file, latest_stl) + shutil.copy2(step_file, latest_step) + stl_path = str(latest_stl) + step_path = str(latest_step) + logs.append(f"Export successful. Archived artifacts at runs/{run_id}.*") + else: + logs.append(f"Runner exited with code {result.returncode}.") + except subprocess.TimeoutExpired: + logs.append("Execution timed out after 60 seconds.") + except Exception as exc: # noqa: BLE001 + logs.append(f"Execution error: {exc}") + logs.append(traceback.format_exc()) + + duration = time.time() - started_at + summary = f"Model ready in {duration:.2f}s." + return stl_path, step_path, "\n".join(logs), summary, run_id, duration + + +def generate_from_prompt(prompt: str, mode: str, output_type: str): + started_at = time.time() + backend_ok = True + client_notice = None + fallback_level = "normal" + suspicious_input = False + job_data, backend_log = create_job(prompt, mode, output_type) + if job_data is None: + backend_ok = False + backend_log = backend_log or "Backend request failed." + spec = { + "output_type": output_type, + "geometry_family": "bracket_plate", + "parameters": {}, + } + client_notice = "Backend was unavailable or disabled, so NaturalCAD used a simple local fallback." + fallback_level = "backend_unavailable" + else: + spec = job_data.get("spec") + suspicious_input = bool(job_data.get("suspicious_input", False)) + fallback_level = job_data.get("fallback_level", "normal") + if suspicious_input: + client_notice = "Your prompt looked partly like code or unsafe instructions, so NaturalCAD used a safer interpretation for this run." + elif fallback_level == "underspecified": + client_notice = "Your prompt was pretty open-ended, so NaturalCAD filled in conservative defaults for this run." + + if not spec: + _append_run_log({ + "timestamp": datetime.now(timezone.utc).isoformat(), + "prompt": prompt, + "mode": mode, + "output_type": output_type, + "backend_ok": backend_ok, + "success": False, + "error": "Backend created no CAD spec.", + }) + return None, None, None, backend_log, "Backend created no CAD spec." + + code = render_code_from_spec(spec) + stl_path, step_path, logs, summary, run_id, execution_seconds = run_build123d(code, prompt) + combined_logs = "\n\n".join([ + "Backend job created:" if backend_ok else "Backend unavailable, using local fallback:", + backend_log, + "Local execution log:", + logs, + ]) + success = bool(stl_path) + _append_run_log({ + "timestamp": datetime.now(timezone.utc).isoformat(), + "run_id": run_id, + "prompt": prompt, + "mode": mode, + "output_type": output_type, + "geometry_family": spec.get("geometry_family"), + "backend_ok": backend_ok, + "suspicious_input": suspicious_input, + "fallback_level": fallback_level, + "success": success, + "runtime_seconds": round(time.time() - started_at, 3), + "execution_seconds": round(execution_seconds, 3), + "error": None if success else "Generation failed.", + }) + if not stl_path: + return None, None, None, combined_logs, "Generation failed. Try a simpler prompt or an example." + + final_summary = summary if not client_notice else f"{summary}\n\n⚠️ {client_notice}" + return stl_path, stl_path, step_path, combined_logs, final_summary + + +def use_example(prompt: str, mode: str, output_type: str): + return prompt, mode, output_type + + +def build_ui() -> gr.Blocks: + with gr.Blocks(title="NaturalCAD", theme=gr.themes.Base()) as demo: + gr.Markdown( + "# NaturalCAD\n" + "Turn a natural-language prompt into a downloadable CAD result." + ) + gr.Markdown( + "**Best for demo:** one-shot parts, frames, blocks, canopies, and simple profiles." + ) + + with gr.Row(equal_height=True): + with gr.Column(scale=1, min_width=360): + prompt_input = gr.Textbox( + label="Describe what you want", + placeholder="A heavy steel bracket with 4 bolt holes, 90 mm wide and 8 mm thick", + lines=6, + ) + with gr.Row(): + mode_picker = gr.Dropdown(choices=["part", "assembly", "sketch"], value="part", label="Mode") + output_picker = gr.Dropdown(choices=["3d_solid", "surface", "2d_vector"], value="3d_solid", label="Output") + generate_btn = gr.Button("Generate Model", variant="primary") + gr.Markdown("### Try one of these") + gr.Examples( + examples=EXAMPLE_PROMPTS, + inputs=[prompt_input, mode_picker, output_picker], + fn=use_example, + outputs=[prompt_input, mode_picker, output_picker], + cache_examples=False, + ) + + with gr.Column(scale=2, min_width=520): + model_viewer = gr.Model3D(label="Preview", elem_id="model-viewer", display_mode="solid") + with gr.Row(): + stl_download = gr.File(label="Download STL") + step_download = gr.File(label="Download STEP") + status_output = gr.Markdown("Ready. Use the mouse to orbit, pan, and zoom the model.") + + log_output = gr.Textbox( + label="Run log", + lines=7, + max_lines=20, + interactive=False, + elem_classes=["log-box"], + ) + + generate_btn.click( + fn=generate_from_prompt, + inputs=[prompt_input, mode_picker, output_picker], + outputs=[model_viewer, stl_download, step_download, log_output, status_output], + ) + + return demo + + +if __name__ == "__main__": + app = build_ui() + app.launch( + server_name="0.0.0.0", + server_port=7860, + css=""" + #model-viewer {height: 620px !important; border-radius: 18px; overflow: hidden;} + .log-box textarea {font-family: 'JetBrains Mono', monospace; font-size: 13px;} + .gradio-container {max-width: 1380px !important;} + button.primary {font-weight: 700;} + """, + ) diff --git a/apps/gradio-demo/artifacts/.gitkeep b/apps/gradio-demo/artifacts/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/gradio-demo/artifacts/logs/.gitkeep b/apps/gradio-demo/artifacts/logs/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/gradio-demo/requirements.txt b/apps/gradio-demo/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f212adac4ee8c2b4d194771ce8dda2894d9a6af3 --- /dev/null +++ b/apps/gradio-demo/requirements.txt @@ -0,0 +1,3 @@ +gradio>=4.0.0 +trimesh +build123d==0.10.0 \ No newline at end of file diff --git a/apps/viewer/README.md b/apps/viewer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..00abe88f5cb2480ff674323a59bebea3ff7250e2 --- /dev/null +++ b/apps/viewer/README.md @@ -0,0 +1,8 @@ +# Viewer App + +Main application shell for the live visualizer. + +Planned regions: +- left: prompt/editor/parameters +- center: viewport +- bottom or right: terminal/logs diff --git a/apps/web-visualizer/README.md b/apps/web-visualizer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..88913a293422a1bd7c8e9e87374cb142fde332dd --- /dev/null +++ b/apps/web-visualizer/README.md @@ -0,0 +1,49 @@ +# build123d Live Visualizer Prototype + +This prototype pairs a lightweight Express runner with a Vite + React front-end that streams runner logs and renders generated STL files inside a browser-based viewport. + +## Prerequisites + +- Node.js 18+ +- Access to the existing `build123d` Python environment at `/Users/noahk/.openclaw/workspace/skills/build123d-cad/.venv/bin/python` (automatically used by the server). + +## Getting Started + +```bash +npm install +npm run dev +``` + +The shortcut above launches both the Express runner (`http://localhost:4000`) and the Vite dev server (`http://localhost:5173`). + +### Manual split + +```bash +npm run dev:server # terminal 1 +npm run dev:client # terminal 2 +``` + +The front-end proxies `/api` and `/artifacts` calls to the Express server when running in dev mode. + +## Using the Prototype + +1. Paste or edit build123d code inside the left panel. Ensure your geometry is assigned to a variable called `result`. +2. Click **Run & Stream**. The server writes your code to a scratch file, executes it inside the configured `build123d` virtualenv, and exports STL and STEP artifacts into `./artifacts`. +3. Logs and errors stream into the right-hand panel. When the export succeeds, the STL is loaded into the three.js viewport and download links become available for both STL and STEP. + +### Sample Snippet + +```python +from build123d import * + +with BuildPart() as bp: + with BuildSketch(Plane.XY) as base: + Rectangle(40, 20) + Locations((0, 0)) + Circle(6) + extrude(amount=12) + +result = bp.part +``` + +All exported artifacts live inside the `artifacts/` folder, which is served statically for browser fetching. diff --git a/apps/web-visualizer/artifacts/.gitkeep b/apps/web-visualizer/artifacts/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/web-visualizer/docs/architecture.md b/apps/web-visualizer/docs/architecture.md new file mode 100644 index 0000000000000000000000000000000000000000..27a65861cebb7b9a691312f827edd0e63a6bd224 --- /dev/null +++ b/apps/web-visualizer/docs/architecture.md @@ -0,0 +1,26 @@ +# build123d Live Visualizer — Architecture Notes + +## Live Update Loop +- **Front-end trigger** – `Run & Stream` button encodes the textarea contents into an SSE request (`/api/run?code=...`). +- **Server orchestration** – Express writes the snippet to `artifacts/.py`, spawns the dedicated build123d Python interpreter, and relays stdout/stderr as Server-Sent Events (`log` events). +- **Completion signal** – When the Python runner finishes, Express emits a `complete` event containing the STL path (or an error message). The client loads the STL via `three.js` and refreshes the viewer. +- **Resilience** – Each run is isolated via UUIDs, making the log stream and artifacts easy to correlate while enabling multiple sequential runs without restarts. + +## Artifact Flow +1. Client sends code → Express persists under `artifacts/.py`. +2. Python runner executes snippet, requiring a `result` variable and exporting to `artifacts/.stl`. +3. Express serves `/artifacts` statically so the browser can fetch STL files immediately. +4. Front-end STL loader retrieves the file, renders it, and exposes a download link; artifacts remain on disk for inspection or later cleanup. + +## LLM Integration Options +- **OpenClaw Orchestrator** – Keep the current human-in-the-loop workflow where OpenClaw agents call the `/api/run` endpoint, enabling prompt-to-geometry iteration without exposing provider keys to the prototype. +- **Direct Provider Calls** – Embed provider SDK (OpenAI, Anthropic, etc.) within the server. The Express layer would accept natural-language prompts, forward them to an LLM, and pipe the generated build123d script straight into the runner before streaming results back. +- **Local Coding Agent** – Bundle a lightweight model (e.g., `llama.cpp`) or a deterministic templating agent that runs locally, translating UI prompts to build123d code without external network usage—aligned with offline or air-gapped deployments. + +## Next-Step Roadmap +- Add job queueing plus cancellation support per run id (currently a single in-memory stream). +- Persist structured job metadata (prompt, status, artifact path) for replay and auditing. +- Harden sandboxing by running the Python process inside a constrained container or Firejail profile. +- Expand the viewer with assembly overlays (multiple STL layers, color coding, exploded views). +- Wire optional LLM prompt templates + history so designers can iterate conversationally. +- Author smoke tests covering the SSE endpoint and sample runner invocation. diff --git a/apps/web-visualizer/docs/milestone-01.md b/apps/web-visualizer/docs/milestone-01.md new file mode 100644 index 0000000000000000000000000000000000000000..95ac581caa5e9bfba93c7263a82c67fd1f03f81f --- /dev/null +++ b/apps/web-visualizer/docs/milestone-01.md @@ -0,0 +1,34 @@ +# Milestone 01 - Stable Live Modeling Loop + +## Goal + +Turn the prototype into a dependable first product loop. + +## Scope + +- edit build123d code in-app +- run geometry through the local build123d runtime +- stream logs/errors live +- preview geometry in a live viewport via STL +- export both STL and STEP from the same run + +## Why this first + +This locks the core modeling runtime before deeper LLM integration. +If the live loop is weak, the AI layer becomes brittle and frustrating. + +## Definition of done + +- code changes run reliably from the UI +- viewport updates consistently after successful runs +- STL download works +- STEP download works +- errors are readable in the log pane +- repo is clean enough to keep building on + +## Next after this + +- parameter controls and editable variables +- document model and adapter contract +- LLM edit/apply flow +- vector and graphic-mass display modes diff --git a/apps/web-visualizer/index.html b/apps/web-visualizer/index.html new file mode 100644 index 0000000000000000000000000000000000000000..a09e0e6e5b11cd76285109300946adad3c4cb5b7 --- /dev/null +++ b/apps/web-visualizer/index.html @@ -0,0 +1,13 @@ + + + + + + + build123d Live Visualizer + + +
+ + + diff --git a/apps/web-visualizer/package-lock.json b/apps/web-visualizer/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..0e1e888b28672d9f58223c0d2dae458aad649cbd --- /dev/null +++ b/apps/web-visualizer/package-lock.json @@ -0,0 +1,3128 @@ +{ + "name": "build123d-live-visualizer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "build123d-live-visualizer", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "three": "^0.161.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.5", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^8.2.1", + "typescript": "^5.3.3", + "vite": "^5.0.0" + } + }, + "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", + "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.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "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/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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/@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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "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", + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@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/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "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/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/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/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "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": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "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/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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/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/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/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/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "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/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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "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.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "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/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "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/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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", + "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/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/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "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.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.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==", + "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==", + "license": "MIT" + }, + "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/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/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/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/three": { + "version": "0.161.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.161.0.tgz", + "integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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/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-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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/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" + } + } + } +} diff --git a/apps/web-visualizer/package.json b/apps/web-visualizer/package.json new file mode 100644 index 0000000000000000000000000000000000000000..e345127e6587c1075a99c93766fb363afe9a797b --- /dev/null +++ b/apps/web-visualizer/package.json @@ -0,0 +1,30 @@ +{ + "name": "build123d-live-visualizer", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "concurrently \"npm:dev:server\" \"npm:dev:client\"", + "dev:server": "node server/index.js", + "dev:client": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "echo 'Add linting as needed'" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "three": "^0.161.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.5", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^8.2.1", + "typescript": "^5.3.3", + "vite": "^5.0.0" + } +} diff --git a/apps/web-visualizer/server/index.js b/apps/web-visualizer/server/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f65bde7fadb08881cb9ecf3fdd56065401a1a041 --- /dev/null +++ b/apps/web-visualizer/server/index.js @@ -0,0 +1,93 @@ +const express = require('express'); +const cors = require('cors'); +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { randomUUID } = require('crypto'); + +const PYTHON_BIN = process.env.BUILD123D_PYTHON || '/Users/noahk/.openclaw/workspace/skills/build123d-cad/.venv/bin/python'; +const PORT = process.env.PORT || 4000; +const ARTIFACT_DIR = path.join(__dirname, '..', 'artifacts'); +const RUNNER_PATH = path.join(__dirname, 'runner.py'); +const DIST_PATH = path.join(__dirname, '..', 'dist'); + +fs.mkdirSync(ARTIFACT_DIR, { recursive: true }); + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: '1mb' })); +app.use('/artifacts', express.static(ARTIFACT_DIR)); + +if (fs.existsSync(DIST_PATH)) { + app.use(express.static(DIST_PATH)); +} + +const sendSse = (res, event, data) => { + res.write(`event: ${event}\n`); + res.write(`data: ${JSON.stringify(data)}\n\n`); +}; + +app.get('/api/run', (req, res) => { + const code = req.query.code; + if (typeof code !== 'string' || !code.trim()) { + res.status(400).json({ error: 'Missing code parameter.' }); + return; + } + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + const jobId = randomUUID(); + const codeFile = path.join(ARTIFACT_DIR, `${jobId}.py`); + const stlFile = path.join(ARTIFACT_DIR, `${jobId}.stl`); + const stepFile = path.join(ARTIFACT_DIR, `${jobId}.step`); + + fs.writeFileSync(codeFile, code); + sendSse(res, 'log', { message: `Job ${jobId} accepted.` }); + + const pythonArgs = [RUNNER_PATH, '--source', codeFile, '--stl-output', stlFile, '--step-output', stepFile]; + const child = spawn(PYTHON_BIN, pythonArgs, { env: process.env }); + + req.on('close', () => { + if (!child.killed) { + child.kill('SIGINT'); + } + }); + + child.stdout.on('data', (chunk) => { + sendSse(res, 'log', { message: chunk.toString().trim() }); + }); + + child.stderr.on('data', (chunk) => { + sendSse(res, 'log', { message: chunk.toString().trim(), level: 'error' }); + }); + + child.on('error', (error) => { + sendSse(res, 'log', { message: `Runner error: ${error.message}`, level: 'error' }); + sendSse(res, 'complete', { success: false, error: error.message }); + res.end(); + }); + + child.on('close', (code) => { + const success = code === 0; + if (success) { + sendSse(res, 'complete', { + success: true, + stlPath: `/artifacts/${path.basename(stlFile)}`, + stepPath: `/artifacts/${path.basename(stepFile)}` + }); + } else { + sendSse(res, 'complete', { success: false, error: `Runner exited with ${code}` }); + } + res.end(); + }); +}); + +app.get('/api/health', (_req, res) => { + res.json({ status: 'ok', python: PYTHON_BIN }); +}); + +app.listen(PORT, () => { + console.log(`build123d live runner listening on http://localhost:${PORT}`); +}); diff --git a/apps/web-visualizer/server/runner.py b/apps/web-visualizer/server/runner.py new file mode 100755 index 0000000000000000000000000000000000000000..f2dd2a444f676a97dca67802592687c6aae09c08 --- /dev/null +++ b/apps/web-visualizer/server/runner.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Simple build123d runner with STL and STEP export.""" + +from __future__ import annotations + +import argparse +import pathlib +import sys +import traceback + + +def coerce_shape(candidate): + """Try to pull an exportable shape from the user namespace.""" + if candidate is None: + return None + + if hasattr(candidate, "wrapped"): + return candidate + + for attr in ("part", "shape", "solid", "obj"): + value = getattr(candidate, attr, None) + if value is not None and not callable(value): + candidate = value + if hasattr(candidate, "wrapped"): + return candidate + return candidate + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run build123d code and export artifacts") + parser.add_argument("--source", required=True, help="Path to user code file") + parser.add_argument("--stl-output", required=True, help="Path for the STL output") + parser.add_argument("--step-output", required=False, help="Path for the STEP output") + args = parser.parse_args() + + source_path = pathlib.Path(args.source) + stl_output_path = pathlib.Path(args.stl_output) + step_output_path = pathlib.Path(args.step_output) if args.step_output else None + stl_output_path.parent.mkdir(parents=True, exist_ok=True) + if step_output_path: + step_output_path.parent.mkdir(parents=True, exist_ok=True) + + try: + code = source_path.read_text() + except OSError as exc: + print(f"Failed to read code: {exc}", file=sys.stderr) + return 1 + + user_globals: dict[str, object] = {} + try: + exec(compile(code, str(source_path), "exec"), user_globals) + except Exception as exec_error: # noqa: BLE001 + print("Execution error:", file=sys.stderr) + traceback.print_exception(exec_error, file=sys.stderr) + return 1 + + candidate = user_globals.get("result") + candidate = coerce_shape(candidate) + + if candidate is None: + print("No `result` geometry found after execution.", file=sys.stderr) + return 2 + + try: + from build123d import export_stl, export_step + except Exception as exc: # noqa: BLE001 + print(f"Unable to import build123d exporters: {exc}", file=sys.stderr) + return 3 + + try: + export_stl(candidate, str(stl_output_path)) + print(f"STL exported to {stl_output_path}") + if step_output_path: + export_step(candidate, str(step_output_path)) + print(f"STEP exported to {step_output_path}") + except Exception as export_error: # noqa: BLE001 + print("Export failed:", file=sys.stderr) + traceback.print_exception(export_error, file=sys.stderr) + return 4 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/apps/web-visualizer/src/App.tsx b/apps/web-visualizer/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ed50ad11d3dc8f4f27d4b18756c5789bfe078c5f --- /dev/null +++ b/apps/web-visualizer/src/App.tsx @@ -0,0 +1,270 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; + +const SAMPLE_SNIPPET = `from build123d import * + +# Simple parametric puck +radius = 15 +height = 8 + +with BuildPart() as bp: + with BuildSketch(Plane.XY) as base: + Circle(radius) + PolarLocations(radius / 2, 6) + Circle(radius / 6) + extrude(amount=height) + +result = bp.part`; + +type LogEntry = { + id: string; + message: string; + level: 'info' | 'error'; +}; + +const loader = new STLLoader(); + +export default function App() { + const [code, setCode] = useState(SAMPLE_SNIPPET); + const [logs, setLogs] = useState([]); + const [status, setStatus] = useState<'idle' | 'running' | 'done' | 'error'>('idle'); + const [artifactUrl, setArtifactUrl] = useState(null); + const [stepUrl, setStepUrl] = useState(null); + const viewerRef = useRef(null); + const eventSourceRef = useRef(null); + const rendererRef = useRef(null); + const sceneRef = useRef(null); + const meshRef = useRef(null); + const cameraRef = useRef(null); + const controlsRef = useRef(null); + + const appendLog = useCallback((message: string, level: 'info' | 'error' = 'info') => { + setLogs((prev) => [...prev, { id: crypto.randomUUID(), message, level }]); + }, []); + + useEffect(() => { + if (!viewerRef.current) return; + + const width = viewerRef.current.clientWidth; + const height = viewerRef.current.clientHeight; + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x020617); + + const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); + camera.position.set(60, 45, 60); + + const renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(width, height); + renderer.setPixelRatio(window.devicePixelRatio); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + + const ambient = new THREE.AmbientLight(0xffffff, 0.6); + const dir = new THREE.DirectionalLight(0xffffff, 0.8); + dir.position.set(50, 80, 30); + + const grid = new THREE.GridHelper(120, 20, 0x172554, 0x1e293b); + + scene.add(ambient); + scene.add(dir); + scene.add(grid); + + viewerRef.current.appendChild(renderer.domElement); + + sceneRef.current = scene; + rendererRef.current = renderer; + cameraRef.current = camera; + controlsRef.current = controls; + + const animate = () => { + controls.update(); + renderer.render(scene, camera); + requestAnimationFrame(animate); + }; + animate(); + + const handleResize = () => { + if (!viewerRef.current || !rendererRef.current || !cameraRef.current) return; + const newWidth = viewerRef.current.clientWidth; + const newHeight = viewerRef.current.clientHeight; + rendererRef.current.setSize(newWidth, newHeight); + cameraRef.current.aspect = newWidth / newHeight; + cameraRef.current.updateProjectionMatrix(); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + renderer.dispose(); + controls.dispose(); + }; + }, []); + + useEffect(() => { + if (!artifactUrl || !sceneRef.current) return; + + loader.load( + artifactUrl, + (geometry) => { + if (meshRef.current) { + sceneRef.current!.remove(meshRef.current); + meshRef.current.geometry.dispose(); + } + geometry.center(); + geometry.computeVertexNormals(); + const material = new THREE.MeshStandardMaterial({ + color: 0x38bdf8, + metalness: 0.1, + roughness: 0.4 + }); + const mesh = new THREE.Mesh(geometry, material); + meshRef.current = mesh; + sceneRef.current!.add(mesh); + appendLog('STL loaded in viewer.'); + setStatus('done'); + }, + undefined, + (error) => { + appendLog(`Viewer load error: ${error.message}`, 'error'); + setStatus('error'); + } + ); + }, [artifactUrl, appendLog]); + + const handleRun = () => { + if (!code.trim()) { + appendLog('Add some build123d code before running.', 'error'); + return; + } + + if (eventSourceRef.current) { + eventSourceRef.current.close(); + } + + setLogs([]); + setStatus('running'); + setArtifactUrl(null); + setStepUrl(null); + + const url = `/api/run?ts=${Date.now()}&code=${encodeURIComponent(code)}`; + const source = new EventSource(url); + eventSourceRef.current = source; + + source.addEventListener('log', (event) => { + const payload = JSON.parse((event as MessageEvent).data) as { message: string; level?: 'info' | 'error' }; + appendLog(payload.message, payload.level ?? 'info'); + }); + + source.addEventListener('complete', (event) => { + const payload = JSON.parse((event as MessageEvent).data) as { success: boolean; stlPath?: string; stepPath?: string; error?: string }; + if (payload.success && payload.stlPath) { + const finalUrl = `${payload.stlPath}?v=${Date.now()}`; + appendLog('Runner completed; STL ready.'); + if (payload.stepPath) { + appendLog('STEP export ready.'); + setStepUrl(`${payload.stepPath}?v=${Date.now()}`); + } + setArtifactUrl(finalUrl); + } else { + appendLog(payload.error ?? 'Runner failed.', 'error'); + setStatus('error'); + } + source.close(); + }); + + source.onerror = () => { + appendLog('Connection interrupted.', 'error'); + setStatus('error'); + source.close(); + }; + }; + + const handleStop = () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + eventSourceRef.current = null; + appendLog('Stream closed by user.'); + setStatus('idle'); + } + }; + + useEffect(() => { + return () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + } + if (rendererRef.current) { + rendererRef.current.dispose(); + } + if (controlsRef.current) { + controlsRef.current.dispose(); + } + }; + }, []); + + return ( +
+
+

build123d Prompt

+
+ + +
+