diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 685380c9..de131975 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -118,8 +118,37 @@ jobs: name: frontend path: ./dotbot/frontend/build + console: + name: check console + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up nodejs + uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npm install + working-directory: ./dotbot/console-web + - run: npm run lint + working-directory: ./dotbot/console-web + - run: npm run typecheck + working-directory: ./dotbot/console-web + - run: npm run test + working-directory: ./dotbot/console-web + - run: npm run build + working-directory: ./dotbot/console-web + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + verbose: true + - name: Upload console build + uses: actions/upload-artifact@v4 + with: + name: console + path: ./dotbot/console-web/dist + package: - needs: [test, doc, frontend, control_loop] + needs: [test, doc, frontend, console, control_loop] name: build source package runs-on: ${{ matrix.os }} strategy: diff --git a/.gitignore b/.gitignore index de2230ff..b95a706b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.log *.egg-info/ .coverage +coverage/ .tox/ dist/ node_modules/ diff --git a/dotbot/__init__.py b/dotbot/__init__.py index 216b851b..4e89fd18 100644 --- a/dotbot/__init__.py +++ b/dotbot/__init__.py @@ -17,6 +17,7 @@ MQTT_PORT_DEFAULT = 1883 MAP_SIZE_DEFAULT = "2000x2000" # in mm unit SIMULATOR_INIT_STATE_DEFAULT = "simulator_init_state.toml" +SWARMIT_URL_DEFAULT = "http://localhost:8001" # swarmit server default port def pydotbot_version() -> str: diff --git a/dotbot/config.py b/dotbot/config.py index 7027c43d..10e1e317 100644 --- a/dotbot/config.py +++ b/dotbot/config.py @@ -145,6 +145,7 @@ class ControllerSection(_Strict): headless: bool | None = None gw_address: str | None = None simulator_init_state: str | None = None + swarmit_url: str | None = None class GatewaySection(_Strict): @@ -292,11 +293,13 @@ def _env_candidates(section: str | None, key: str) -> tuple[str, ...]: """Env-var names to check, in priority order (Cargo's mechanical mapping). Sectioned key -> `DOTBOT_
_`, then the shared `DOTBOT_` - alias. Top-level key -> just `DOTBOT_`. + alias. Top-level key -> just `DOTBOT_`. A nested section like + `run.controller` flattens its dots: `DOTBOT_RUN_CONTROLLER_`. """ key_part = key.upper().replace("-", "_") if section: - return (f"DOTBOT_{section.upper()}_{key_part}", f"DOTBOT_{key_part}") + section_part = section.upper().replace(".", "_") + return (f"DOTBOT_{section_part}_{key_part}", f"DOTBOT_{key_part}") return (f"DOTBOT_{key_part}",) @@ -318,11 +321,17 @@ def _file_value( key: str, deployment: Deployment | None, ) -> Any: - """The value this key has in the file layer: section > deployment > top-level.""" + """The value this key has in the file layer: section > deployment > top-level. + + `section` may be nested (dot-separated, e.g. `run.controller`); each part + is walked with getattr. + """ if config is None: return None if section is not None: - section_obj = getattr(config, section, None) + section_obj: Any = config + for part in section.split("."): + section_obj = getattr(section_obj, part, None) value = getattr(section_obj, key, None) if value is not None: return value @@ -348,9 +357,10 @@ def resolve( `flag` > env (`DOTBOT_
_`, then shared `DOTBOT_`) > file (section > deployment > top-level) > `default`. - `section` is one of `SECTIONS` for a per-namespace key, or `None` for a - top-level shared key (e.g. `conn`, `swarm_id`). Env values are coerced to - the type of `default`. + `section` is one of `SECTIONS` for a per-namespace key, a dotted path for + a nested table (e.g. `run.controller`), or `None` for a top-level shared + key (e.g. `conn`, `swarm_id`). Env values are coerced to the type of + `default`. """ if flag is not None: return flag diff --git a/dotbot/console-web/dev/TESTING.md b/dotbot/console-web/dev/TESTING.md new file mode 100644 index 00000000..18814f0e --- /dev/null +++ b/dotbot/console-web/dev/TESTING.md @@ -0,0 +1,81 @@ +# Console manual test guide + +Start the stack (three terminals, from the workspace root, venv active): + +```bash +PYTHONPATH=repos/wt-PyDotBot-unified-web-ui dotbot run simulator --headless \ + --simulator-init-state repos/wt-PyDotBot-unified-web-ui/dotbot/console-web/dev/simulator_init_state.toml + +python repos/wt-PyDotBot-unified-web-ui/dotbot/console-web/dev/fake_swarmit_server.py + +npm --prefix repos/wt-PyDotBot-unified-web-ui/dotbot/console-web run start +``` + +Open http://localhost:5173. Handy URL params: `?sel=1111` (preselect), +`?view=list|grid`, `?rail=testbed|missions`, `?theme=light`. + +If the title bar says OFFLINE: the vite proxy targets do not match where the +controller runs. `curl localhost:5173/controller/dotbots` must return bots. + +## Checklist + +Map + selection +- [ ] Title bar: LIVE (green pulse), bot count; Dark|Light toggle flips theme +- [ ] Bots move smoothly; heading pointer at the circle edge; battery bar above +- [ ] Plain drag pans (clamped); plain click clears selection +- [ ] Shift-drag marquee selects; shift-click toggles one bot +- [ ] Click bot: red rectangle + id chip; hover another bot: chip appears +- [ ] Zoom +/-/recenter; arena keeps margins when rail opens or window resizes +- [ ] Layers panel: Battery Bars / Waypoints / DotBots / Real-scale / Trails toggle live + +Footer (bottom strip) +- [ ] Nothing selected: N/1000 + per-state rollup; click a state row selects those bots +- [ ] One bot: LED thumb, short id, device, Drivable pill, state, battery, "x, y mm" +- [ ] Ghost bot (0001/0002): "Not drivable" pill, dock grayed, warning hint +- [ ] Multi: "N selected", per-state mini-rollup, dock drives the group +- [ ] Minimap: square (arena aspect), dots colored by state, drag moves the map view + +Control dock +- [ ] Joystick: drag = bot drives, release = stops; knob shows LED color + live heading +- [ ] LED button -> swatch grid -> bot circle + map dot recolor (toast confirms) +- [ ] Alt-click map queues waypoints (dashed diamonds); popover lists "x, y mm" with per-item remove +- [ ] Go sends: bot navigates, diamonds go solid, button morphs to "Stop nav" +- [ ] Deselect and reselect other bots: the Planned mission survives (see rail Missions) + +Testbed rail +- [ ] Icon strip -> panel; Testbed tab: Target label follows selection +- [ ] Flash... dialog: pick image, Flash N device(s); bots blink amber, queue tab + shows per-device % bars, fleet % bar in Testbed tab, footer shows chunk progress +- [ ] After flash: bots in Bootloader (not drivable) -> Start returns them to Running +- [ ] Stop: Stopping -> Bootloader; Reset: Resetting -> Bootloader +- [ ] Console tab: log lines stream (flash/start/stop events), Clear works +- [ ] Missions tab: Planned (Go / discard) and Active (interrupt) rows; click row + reselects its bots; arrival adds a "Recently completed" line + +List / Grid +- [ ] Search by id, state filter, column sort; checkbox multi-select + select-all +- [ ] Selection carries across Map/List/Grid and drives the same footer dock + +## Scripted actions (against the running stack) + +```bash +# color a bot's LED +curl -X PUT localhost:8000/controller/dotbots/badcafe111111111/0/rgb_led \ + -H 'Content-Type: application/json' -d '{"red":34,"green":197,"blue":94}' + +# send a waypoint mission (bot navigates on its own) +curl -X PUT localhost:8000/controller/dotbots/badcafe111111111/0/waypoints \ + -H 'Content-Type: application/json' \ + -d '{"threshold":60,"waypoints":[{"x":400,"y":1600},{"x":1600,"y":400}]}' + +# flash two bots (watch rail queue + console) +curl -N -X POST localhost:8001/flash/stream -H 'Content-Type: application/json' \ + -d '{"firmware_b64":"ZmFrZQ==","devices":["badcafe111111111","deadbeef22222222"]}' + +# stop / start / reset a subset (or omit devices = whole fleet) +curl -X POST localhost:8001/stop -H 'Content-Type: application/json' \ + -d '{"devices":["badcafe111111111"]}' + +# screenshot for the Claude Design re-seed loop +node dotbot/console-web/dev/screenshot.mjs "http://127.0.0.1:5173/?sel=1111" out.png +``` diff --git a/dotbot/console-web/dev/fake_swarmit_server.py b/dotbot/console-web/dev/fake_swarmit_server.py new file mode 100644 index 00000000..b1426a3d --- /dev/null +++ b/dotbot/console-web/dev/fake_swarmit_server.py @@ -0,0 +1,285 @@ +"""Fake SwarmIT server for console development. + +Simulates the swarmit testbed server's HTTP surface with the same shapes: + +- GET /status -> {"response": {"": {device, status, battery, pos_x, pos_y}}} +- GET /settings -> {network_id, area_width, area_height, calibration_distance, auth_mode} +- POST /start|/stop|/reset {devices?} -> state transitions +- POST /flash {firmware_b64, devices?} (blocking) +- POST /flash/stream {firmware_b64, devices?} -> SSE: flash_started / chunk / + device_done / complete (same event shapes as swarmit) +- GET /events -> SSE: log_event entries (+ periodic status snapshots) + +Bot addresses are read live from the PyDotBot controller so both planes join +on the same ids; two ghost bots exist only on this plane (start in Bootloader, +never drivable). The DEVICE side is simulated; the HTTP contract is the real +one, so the console's write path exercises the same calls it will make against +a real swarmit server. + +Usage: python fake_swarmit_server.py [--controller http://localhost:8000] [--port 8001] +""" + +import argparse +import asyncio +import json +import time +import urllib.request + +import uvicorn +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse + +GHOSTS = { + "C0FFEE0000000001": {"pos_x": 300, "pos_y": 1800}, + "C0FFEE0000000002": {"pos_x": 1750, "pos_y": 1650}, +} + +TOTAL_CHUNKS = 320 +CHUNKS_PER_TICK = 12 +TICK_S = 0.15 + +app = FastAPI(title="fake-swarmit") +app.add_middleware( + CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"] +) + +settings = {"controller": "http://localhost:8000"} + +# Per-device simulated orchestration state. +states: dict = {} # addr -> {"status": str, "progress": {"acked", "total"} | None} +events: list = [] # [{"id", "type": "log_event", "level", "message", "ts"}] +_event_seq = 0 + + +def push_log(level: str, message: str) -> None: + global _event_seq + _event_seq += 1 + events.append( + { + "id": _event_seq, + "type": "log_event", + "level": level, + "message": message, + "ts": time.time(), + } + ) + del events[:-200] + + +def fetch_dotbots() -> list: + try: + with urllib.request.urlopen( + f"{settings['controller']}/controller/dotbots", timeout=2 + ) as res: + return json.loads(res.read()) + except Exception: + return [] + + +def known_devices() -> list: + return sorted({b["address"] for b in fetch_dotbots()} | set(GHOSTS)) + + +def state_of(addr: str) -> dict: + if addr not in states: + states[addr] = { + "status": "Bootloader" if addr in GHOSTS else "Running", + "progress": None, + } + return states[addr] + + +def resolve_devices(payload_devices) -> list: + if not payload_devices: + return known_devices() + if isinstance(payload_devices, str): + return [payload_devices] + return list(payload_devices) + + +def _sse(data: dict) -> str: + return f"data: {json.dumps(data)}\n\n" + + +@app.get("/status") +def status(): + response = {} + for bot in fetch_dotbots(): + addr = bot["address"] + st = state_of(addr) + pos = bot.get("lh2_position") or {} + response[addr] = { + "device": "DotBotV3", + "status": st["status"], + "battery": int(float(bot.get("battery", 3.0)) * 1000), + "pos_x": int(pos.get("x", 0)), + "pos_y": int(pos.get("y", 0)), + } + for addr, pos in GHOSTS.items(): + st = state_of(addr) + response[addr] = { + "device": "DotBotV3", + "status": st["status"], + "battery": 3900, + "pos_x": pos["pos_x"], + "pos_y": pos["pos_y"], + } + return {"response": response} + + +@app.get("/settings") +def get_settings(): + return { + "network_id": 0x12, + "area_width": 2000, + "area_height": 2000, + "calibration_distance": 400, + "auth_mode": "none", + } + + +async def transition(devices: list, via: str, to: str, delay: float) -> None: + for a in devices: + state_of(a)["status"] = via + await asyncio.sleep(delay) + for a in devices: + state_of(a)["status"] = to + + +@app.post("/start") +async def start(request: Request): + body = ( + await request.json() if int(request.headers.get("content-length") or 0) else {} + ) + devices = resolve_devices(body.get("devices")) + for a in devices: + state_of(a)["status"] = "Running" + push_log("ok", f"testbed started · {len(devices)} device(s)") + return {"result": "ok", "devices": devices} + + +@app.post("/stop") +async def stop(request: Request): + body = ( + await request.json() if int(request.headers.get("content-length") or 0) else {} + ) + devices = resolve_devices(body.get("devices")) + push_log("warn", f"stopping · {len(devices)} device(s)") + asyncio.create_task(transition(devices, "Stopping", "Bootloader", 1.2)) + return {"result": "ok", "devices": devices} + + +@app.post("/reset") +async def reset(request: Request): + body = ( + await request.json() if int(request.headers.get("content-length") or 0) else {} + ) + devices = resolve_devices( + body.get("devices") or list(body.get("locations", {}) or {}) + ) + push_log("warn", f"resetting · {len(devices)} device(s)") + asyncio.create_task(transition(devices, "Resetting", "Bootloader", 1.6)) + return {"result": "ok", "devices": devices} + + +async def flash_events(devices: list, fw_len: int): + for a in devices: + st = state_of(a) + st["status"] = "Programming" + st["progress"] = {"acked": 0, "total": TOTAL_CHUNKS} + push_log("info", f"flash started · {len(devices)} device(s) · {fw_len} bytes") + yield _sse( + { + "type": "flash_started", + "image_size": fw_len, + "total_chunks": TOTAL_CHUNKS, + "fw_hash": "F4KEF4KE", + "devices": sorted(devices), + } + ) + done: set = set() + while len(done) < len(devices): + await asyncio.sleep(TICK_S) + for a in devices: + if a in done: + continue + p = state_of(a)["progress"] + p["acked"] = min(p["total"], p["acked"] + CHUNKS_PER_TICK) + yield _sse( + {"type": "chunk", "addr": a, "acked": p["acked"], "total": p["total"]} + ) + if p["acked"] >= p["total"]: + done.add(a) + st = state_of(a) + st["status"] = "Bootloader" # flashed image sits ready; /start runs it + st["progress"] = None + push_log("ok", f"{a[-4:].upper()} flashed · {p['total']} chunks") + yield _sse( + { + "type": "device_done", + "addr": a, + "success": True, + "retries": 0, + "chunks_acked": p["total"], + "chunks_total": p["total"], + } + ) + push_log("ok", "flash complete · all devices") + yield _sse( + { + "type": "complete", + "all_success": True, + "elapsed_s": TOTAL_CHUNKS / CHUNKS_PER_TICK * TICK_S, + } + ) + + +@app.post("/flash/stream") +async def flash_stream(request: Request): + body = await request.json() + devices = resolve_devices(body.get("devices")) + fw_len = len(body.get("firmware_b64", "")) * 3 // 4 + return StreamingResponse( + flash_events(devices, fw_len), media_type="text/event-stream" + ) + + +@app.post("/flash") +async def flash(request: Request): + body = await request.json() + devices = resolve_devices(body.get("devices")) + fw_len = len(body.get("firmware_b64", "")) * 3 // 4 + async for _ in flash_events(devices, fw_len): + pass + return {"result": "ok", "devices": devices} + + +@app.get("/events") +async def sse_events(request: Request): + async def gen(): + last_id = max(0, _event_seq - 50) # replay recent history on connect + last_snapshot = 0.0 + while True: + if await request.is_disconnected(): + return + for ev in events: + if ev["id"] > last_id: + yield _sse(ev) + last_id = ev["id"] + now = time.time() + if now - last_snapshot > 2.0: + last_snapshot = now + yield _sse({"type": "status", "response": status()["response"]}) + await asyncio.sleep(0.3) + + return StreamingResponse(gen(), media_type="text/event-stream") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--controller", default="http://localhost:8000") + parser.add_argument("--port", type=int, default=8001) + args = parser.parse_args() + settings["controller"] = args.controller + uvicorn.run(app, host="127.0.0.1", port=args.port, log_level="warning") diff --git a/dotbot/console-web/dev/screenshot.mjs b/dotbot/console-web/dev/screenshot.mjs new file mode 100644 index 00000000..ae987aec --- /dev/null +++ b/dotbot/console-web/dev/screenshot.mjs @@ -0,0 +1,23 @@ +// Dev screenshot helper: loads a console URL in headless Chrome, waits for +// live data to render (or a fixed delay), then captures a PNG. +// Usage: node dev/screenshot.mjs [waitMs=4000] +import puppeteer from "puppeteer-core"; + +const [url, outfile, waitMs = "4000"] = process.argv.slice(2); +if (!url || !outfile) { + console.error("usage: node dev/screenshot.mjs [waitMs]"); + process.exit(1); +} + +const browser = await puppeteer.launch({ + executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + headless: "shell", + args: ["--disable-gpu", "--hide-scrollbars"], +}); +const page = await browser.newPage(); +await page.setViewport({ width: 1440, height: 900 }); +await page.goto(url, { waitUntil: "domcontentloaded" }); +await new Promise((r) => setTimeout(r, Number(waitMs))); +await page.screenshot({ path: outfile }); +await browser.close(); +console.log("saved", outfile); diff --git a/dotbot/console-web/dev/simulator_init_state.toml b/dotbot/console-web/dev/simulator_init_state.toml new file mode 100644 index 00000000..8515ba73 --- /dev/null +++ b/dotbot/console-web/dev/simulator_init_state.toml @@ -0,0 +1,47 @@ +# Demo fleet for console development. Run the simulator from this directory +# (it picks up ./simulator_init_state.toml) or copy this file next to it. + +[network] +pdr = 100 + +[[dotbots]] +address = "BADCAFE111111111" +calibrated = 0xff +pos_x = 400 +pos_y = 350 +direction = 45 + +[[dotbots]] +address = "DEADBEEF22222222" +calibrated = 0xff +pos_x = 1500 +pos_y = 250 +direction = 180 + +[[dotbots]] +address = "B0B0F00D33333333" +calibrated = 0xff +pos_x = 1500 +pos_y = 1500 +direction = 270 + +[[dotbots]] +address = "CAFEBABE44444444" +calibrated = 0xff +pos_x = 600 +pos_y = 1200 +direction = 90 + +[[dotbots]] +address = "FACEFEED55555555" +calibrated = 0xff +pos_x = 1000 +pos_y = 800 +direction = 0 + +[[dotbots]] +address = "ABAD1DEA66666666" +calibrated = 0xff +pos_x = 250 +pos_y = 900 +direction = 135 diff --git a/dotbot/console-web/eslint.config.mjs b/dotbot/console-web/eslint.config.mjs new file mode 100644 index 00000000..aea6b8f7 --- /dev/null +++ b/dotbot/console-web/eslint.config.mjs @@ -0,0 +1,10 @@ +// @ts-check + +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; + +export default defineConfig( + js.configs.recommended, + tseslint.configs.recommended, +); diff --git a/dotbot/console-web/index.html b/dotbot/console-web/index.html new file mode 100644 index 00000000..9d8975e0 --- /dev/null +++ b/dotbot/console-web/index.html @@ -0,0 +1,18 @@ + + + + + + DotBot Console + + + + + +
+ + + diff --git a/dotbot/console-web/package-lock.json b/dotbot/console-web/package-lock.json new file mode 100644 index 00000000..c851ec83 --- /dev/null +++ b/dotbot/console-web/package-lock.json @@ -0,0 +1,4804 @@ +{ + "name": "dotbot-console-web", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dotbot-console-web", + "version": "0.0.1", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^2.1.9", + "eslint": "^10.6.0", + "puppeteer-core": "^24.43.1", + "typescript": "^5.2.2", + "typescript-eslint": "^8.63.0", + "vite": "^5.0.8", + "vitest": "^2.1.9" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "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/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "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/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~8.3.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/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "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/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-stream/node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "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.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "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/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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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/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-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "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/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "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.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream/node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "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/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/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/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/dotbot/console-web/package.json b/dotbot/console-web/package.json new file mode 100644 index 00000000..bbc7fd92 --- /dev/null +++ b/dotbot/console-web/package.json @@ -0,0 +1,31 @@ +{ + "name": "dotbot-console-web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "lint": "eslint src/.", + "test": "vitest run --coverage" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^2.1.9", + "eslint": "^10.6.0", + "puppeteer-core": "^24.43.1", + "typescript": "^5.2.2", + "typescript-eslint": "^8.63.0", + "vite": "^5.0.8", + "vitest": "^2.1.9" + } +} diff --git a/dotbot/console-web/src/App.tsx b/dotbot/console-web/src/App.tsx new file mode 100644 index 00000000..fe0b6b0b --- /dev/null +++ b/dotbot/console-web/src/App.tsx @@ -0,0 +1,490 @@ +import React, { useCallback, useRef, useState } from "react"; + +import { putWaypoints } from "./api"; +import { Footer } from "./Footer"; +import { GridView } from "./GridView"; +import { ListView } from "./ListView"; +import { Camera, Layers, MapView, ViewGeom } from "./MapView"; +import { DoneMission, TestbedRail } from "./TestbedRail"; +import { LH2Position, PlannedMission } from "./types"; +import { useFleet } from "./useFleet"; +import { useOrchestration } from "./useOrchestration"; +import { FlashDialog } from "./FlashDialog"; + +const WAYPOINT_THRESHOLD = 60; // mm, arrival radius sent with waypoint missions + +type ViewKind = "map" | "list" | "grid"; + +export const App: React.FC = () => { + const { bots, mapSize, wsUp } = useFleet(); + // ?theme=dark|light presets the theme (handy for dev/screenshots). + const [theme, setTheme] = useState<"dark" | "light">(() => + new URLSearchParams(window.location.search).get("theme") === "light" ? "light" : "dark", + ); + // ?view=map|list|grid opens a specific view (handy for dev/screenshots). + const [view, setView] = useState(() => { + const v = new URLSearchParams(window.location.search).get("view"); + return v === "list" || v === "grid" ? v : "map"; + }); + const [cam, setCam] = useState({ scale: 1, tx: 0, ty: 0 }); + const [geom, setGeom] = useState(null); + const [toast, setToast] = useState(null); + const toastTimer = useRef(undefined); + const showToast = useCallback((msg: string) => { + setToast(msg); + window.clearTimeout(toastTimer.current); + toastTimer.current = window.setTimeout(() => setToast(null), 2500); + }, []); + + const orch = useOrchestration(showToast); + const [flashOpen, setFlashOpen] = useState(false); + + // ?sel=[,] preselects bots (handy for dev/screenshots). + const [selection, setSelection] = useState>(new Set()); + const preselRef = useRef(false); + React.useEffect(() => { + if (preselRef.current || bots.length === 0) return; + const raw = new URLSearchParams(window.location.search).get("sel"); + if (raw) { + const suffixes = raw.toLowerCase().split(","); + const hits = bots.filter((b) => suffixes.some((s) => b.id.toLowerCase().endsWith(s))).map((b) => b.id); + if (hits.length) setSelection(new Set(hits)); + } + preselRef.current = true; + }, [bots]); + + // Planned missions: local waypoint queues bound to bots at queue time. + const [planned, setPlanned] = useState([]); + const [layersOpen, setLayersOpen] = useState(false); + const [layers, setLayers] = useState({ + batteryBars: true, + waypoints: true, + hotSpots: false, + dotBots: true, + trueScale: false, + trails: false, + }); + + // replace = set selection to ids · toggle = flip each id · add = union (range select) + const onSelect = useCallback((ids: string[], mode: "replace" | "toggle" | "add") => { + setSelection((prev) => { + if (mode === "replace") return new Set(ids); + const next = new Set(prev); + if (mode === "add") ids.forEach((id) => next.add(id)); + else ids.forEach((id) => (next.has(id) ? next.delete(id) : next.add(id))); + return next; + }); + }, []); + + const selectedBots = bots.filter((b) => selection.has(b.id)); + const drivableSelected = selectedBots.filter((b) => b.drivable); + const selKey = drivableSelected.map((b) => b.id).sort().join("-"); + const selPlanned = planned.find((m) => m.key === selKey); + const pending = selPlanned?.waypoints ?? []; + + const onAddWaypoint = useCallback( + (p: LH2Position) => { + if (drivableSelected.length === 0) return; + const ids = drivableSelected.map((b) => b.id).sort(); + const key = ids.join("-"); + setPlanned((prev) => { + const hit = prev.find((m) => m.key === key); + if (hit) return prev.map((m) => (m.key === key ? { ...m, waypoints: [...m.waypoints, p] } : m)); + return [...prev, { key, ids, waypoints: [p] }]; + }); + }, + [drivableSelected], + ); + + const sendMission = useCallback( + (m: PlannedMission) => { + const targets = bots.filter((b) => m.ids.includes(b.id) && b.drivable); + targets.forEach((b) => { + putWaypoints(b.id, b.application, WAYPOINT_THRESHOLD, m.waypoints).catch(() => {}); + }); + showToast( + `${m.waypoints.length} waypoint${m.waypoints.length > 1 ? "s" : ""} sent to ${targets.length} bot${ + targets.length > 1 ? "s" : "" + }`, + ); + setPlanned((prev) => prev.filter((x) => x.key !== m.key)); + }, + [bots, showToast], + ); + + const onGo = useCallback(() => { + if (selPlanned) sendMission(selPlanned); + }, [selPlanned, sendMission]); + + const onGoMission = useCallback( + (key: string) => { + const m = planned.find((x) => x.key === key); + if (m) sendMission(m); + }, + [planned, sendMission], + ); + + const onStopNav = useCallback(() => { + drivableSelected.forEach((b) => { + putWaypoints(b.id, b.application, WAYPOINT_THRESHOLD, []).catch(() => {}); + }); + if (drivableSelected.length > 0) showToast("Navigation stopped"); + }, [drivableSelected, showToast]); + + const onClearQueue = useCallback(() => { + setPlanned((prev) => prev.filter((m) => m.key !== selKey)); + }, [selKey]); + const onDiscardMission = useCallback((key: string) => { + setPlanned((prev) => prev.filter((m) => m.key !== key)); + }, []); + const onRemovePending = useCallback( + (i: number) => { + setPlanned((prev) => + prev + .map((m) => (m.key === selKey ? { ...m, waypoints: m.waypoints.filter((_, j) => j !== i) } : m)) + .filter((m) => m.waypoints.length > 0), + ); + }, + [selKey], + ); + + // Recently-completed missions: a bot flipping AUTO -> MANUAL just arrived. + const [doneMissions, setDoneMissions] = useState([]); + const prevNavRef = useRef>({}); + React.useEffect(() => { + const prev = prevNavRef.current; + const arrived = bots.filter((b) => prev[b.id] === "auto" && b.nav === "drive"); + if (arrived.length) { + const t = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + setDoneMissions((d) => + [...arrived.map((b) => ({ key: `${b.id}-${Date.now()}`, id: b.id.slice(-4).toUpperCase(), t })), ...d].slice(0, 8), + ); + } + prevNavRef.current = Object.fromEntries(bots.map((b) => [b.id, b.nav])); + }, [bots]); + + const onStopMission = useCallback( + (ids: string[]) => { + bots + .filter((b) => ids.includes(b.id) && b.drivable) + .forEach((b) => putWaypoints(b.id, b.application, WAYPOINT_THRESHOLD, []).catch(() => {})); + showToast("Mission interrupted"); + }, + [bots, showToast], + ); + + const layerRows: { key: keyof Layers; label: string }[] = [ + { key: "batteryBars", label: "Battery Bars" }, + { key: "waypoints", label: "Waypoints" }, + { key: "hotSpots", label: "HotSpots" }, + { key: "dotBots", label: "DotBots" }, + { key: "trueScale", label: "Real-scale bots" }, + { key: "trails", label: "Trails" }, + ]; + + return ( +
+ {/* Title bar */} +
+
DotBots
+
+ {window.location.host} +
+
+ + {wsUp ? "LIVE" : "OFFLINE"} + + · {bots.length} bots +
+
+ {/* theme: Dark | Light segmented (v1) */} +
+ {(["dark", "light"] as const).map((t) => ( +
setTheme(t)} + style={{ + padding: "4px 12px", + borderRadius: 5, + fontSize: 12, + fontWeight: 500, + cursor: "pointer", + background: theme === t ? "var(--accent)" : "transparent", + color: theme === t ? "#fff" : "var(--muted)", + textTransform: "capitalize", + }} + > + {t} +
+ ))} +
+
+ ✕ +
+
+ + {/* Body row: testbed rail + view area */} +
+ setFlashOpen(true)} + onStart={() => orch.act("start", selection.size ? [...selection] : undefined)} + onStop={() => orch.act("stop", selection.size ? [...selection] : undefined)} + onReset={() => orch.act("reset", selection.size ? [...selection] : undefined)} + onSelectIds={(ids) => onSelect(ids, "replace")} + onGoMission={onGoMission} + onDiscardMission={onDiscardMission} + onStopMission={onStopMission} + /> + + {/* view area */} +
+ {view === "map" && ( + { + const owner = bots.find((b) => m.ids.includes(b.id) && b.led); + return { + waypoints: m.waypoints, + led: owner?.led ? `rgb(${owner.led.red},${owner.led.green},${owner.led.blue})` : null, + }; + })} + cam={cam} + setCam={setCam} + onGeom={setGeom} + onSelect={onSelect} + onAddWaypoint={onAddWaypoint} + /> + )} + {view === "list" && } + {view === "grid" && } + + {/* shared view switcher */} +
+ {view === "map" && ( +
setLayersOpen((v) => !v)} + style={{ + display: "flex", + alignItems: "center", + gap: 6, + padding: "7px 11px", + borderRadius: 8, + cursor: "pointer", + fontSize: 12, + background: layersOpen ? "var(--elevated)" : "var(--surface)", + border: "1px solid var(--hairline)", + boxShadow: "0 4px 16px rgba(0,0,0,.3)", + }} + > + ▤ Layers +
+ )} +
+ {(["map", "list", "grid"] as ViewKind[]).map((v) => ( +
setView(v)} + style={{ + padding: "6px 14px", + borderRadius: 6, + fontSize: 12, + cursor: "pointer", + background: view === v ? "var(--accent)" : "transparent", + color: view === v ? "#fff" : "var(--muted)", + fontWeight: view === v ? 600 : 400, + textTransform: "capitalize", + }} + > + {v} +
+ ))} +
+
+ + {/* toast */} + {toast && ( +
+ {toast} +
+ )} + + {/* layers panel */} + {layersOpen && view === "map" && ( +
+
+ Layers +
+ setLayersOpen(false)} style={{ cursor: "pointer", color: "var(--muted)", fontSize: 14, lineHeight: 1 }}> + ✕ + +
+ {layerRows.map((l) => ( +
setLayers((prev) => ({ ...prev, [l.key]: !prev[l.key] }))} + style={{ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "5px 4px", + borderRadius: 5, + cursor: "pointer", + fontSize: 12, + }} + > + {l.label} + + {layers[l.key] ? "✓" : ""} + +
+ ))} +
+ )} +
+
+ + setFlashOpen(false)} + onFlash={(fw) => orch.flash(fw, selection.size ? [...selection] : undefined)} + /> + +
onSelect(ids, "replace")} + onGo={onGo} + onStopNav={onStopNav} + onClearQueue={onClearQueue} + onRemovePending={onRemovePending} + onToast={showToast} + /> +
+ ); +}; diff --git a/dotbot/console-web/src/FlashDialog.tsx b/dotbot/console-web/src/FlashDialog.tsx new file mode 100644 index 00000000..0277dd0b --- /dev/null +++ b/dotbot/console-web/src/FlashDialog.tsx @@ -0,0 +1,108 @@ +import React, { useState } from "react"; + +// v1 flash dialog. The firmware list is static until a real artifact source +// exists (dotbot fw registry / file upload against the real swarmit server). +const FIRMWARES = ["app_v0.4.1.bin", "blink_demo.bin", "swarm_nav.bin", "line_follow.bin"]; + +interface FlashDialogProps { + open: boolean; + targetCount: number; + targetLabel: string; + onClose: () => void; + onFlash: (firmware: string) => void; +} + +export const FlashDialog: React.FC = (props) => { + const [fw, setFw] = useState(FIRMWARES[0]); + if (!props.open) return null; + return ( +
+
e.stopPropagation()} + style={{ + width: 400, + background: "var(--surface)", + border: "1px solid var(--hairline)", + borderRadius: 14, + padding: 22, + boxShadow: "0 20px 60px rgba(0,0,0,.5)", + }} + > +
Flash firmware
+
+ Over-the-air flash to {props.targetCount} device(s) ·{" "} + {props.targetLabel} +
+
+ Firmware image +
+ +
+
+ Cancel +
+
{ + props.onFlash(fw); + props.onClose(); + }} + style={{ + padding: "9px 18px", + borderRadius: 8, + cursor: "pointer", + fontSize: 13, + fontWeight: 600, + background: "var(--accent)", + color: "#fff", + }} + > + Flash {props.targetCount} device(s) +
+
+
+
+ ); +}; diff --git a/dotbot/console-web/src/Footer.tsx b/dotbot/console-web/src/Footer.tsx new file mode 100644 index 00000000..9fea5558 --- /dev/null +++ b/dotbot/console-web/src/Footer.tsx @@ -0,0 +1,524 @@ +import React, { useState } from "react"; + +import { putRgbLed } from "./api"; +import { Pad } from "./Joystick"; +import { Camera, ViewGeom } from "./MapView"; +import { Minimap } from "./Minimap"; +import { LH2Position, MapSize, STATE_ORDER, UnifiedBot } from "./types"; +import { FlashJob } from "./useOrchestration"; + +// v1 swatch palette. +const SWATCHES: [number, number, number][] = [ + [228, 3, 46], + [255, 140, 0], + [255, 200, 0], + [34, 197, 94], + [13, 148, 136], + [56, 189, 248], + [64, 80, 230], + [168, 85, 247], + [255, 255, 255], + [60, 60, 60], +]; + +const label = { fontSize: 10, letterSpacing: ".5px", textTransform: "uppercase", color: "var(--muted)" } as const; +const mono = { fontFamily: "var(--font-mono)" } as const; +// v1 gate style for non-drivable selections. +const gateOff = { opacity: 0.32, pointerEvents: "none" as const, filter: "grayscale(.7)" }; + +const ledCss = (b: UnifiedBot | undefined) => + b?.led ? `rgb(${b.led.red},${b.led.green},${b.led.blue})` : "var(--s-Inactive)"; +const batPct = (v: number) => Math.max(0, Math.min(1, (v - 2.0) / (4.2 - 2.0))); +const batColor = (p: number) => + p < 0.2 ? "var(--s-Stopping)" : p < 0.45 ? "var(--s-Programming)" : "var(--s-Running)"; +const short = (id: string) => id.slice(-4).toUpperCase(); + +interface FooterProps { + bots: UnifiedBot[]; + flashQueue: Record; + mapSize: MapSize; + selection: Set; + pendingWaypoints: LH2Position[]; + cam: Camera; + setCam: React.Dispatch>; + geom: ViewGeom | null; + onSelectState: (ids: string[]) => void; + onGo: () => void; + onStopNav: () => void; + onClearQueue: () => void; + onRemovePending: (index: number) => void; + onToast: (msg: string) => void; +} + +const StateDot: React.FC<{ state: string; glow?: boolean; size?: number }> = ({ state, glow, size = 9 }) => ( + +); + +const BatteryBar: React.FC<{ volts: number }> = ({ volts }) => { + const pct = batPct(volts); + return ( +
+
+
+
+ {volts.toFixed(2)} V +
+ ); +}; + +// The control dock, per v1: pad + LED button + segmented waypoint group, +// with popovers anchored above and everything gated when nothing is drivable. +const ControlDock: React.FC<{ + targets: UnifiedBot[]; + pending: LH2Position[]; + isGroup: boolean; + selCount: number; + onGo: () => void; + onStopNav: () => void; + onClearQueue: () => void; + onRemovePending: (i: number) => void; + onToast: (msg: string) => void; +}> = ({ targets, pending, isGroup, selCount, onGo, onStopNav, onClearQueue, onRemovePending, onToast }) => { + const [ledOpen, setLedOpen] = useState(false); + const [wpOpen, setWpOpen] = useState(false); + const drivable = targets.filter((b) => b.drivable); + const enabled = drivable.length > 0; + const anyAuto = drivable.some((b) => b.nav === "auto"); + // The controller stores [own-start, ...targets]; count the targets. + const activeCount = Math.max( + ...drivable.map((b) => (b.waypoints.length > 1 ? b.waypoints.length - 1 : b.waypoints.length)), + 0, + ); + const wpCount = pending.length > 0 ? pending.length : anyAuto ? activeCount : 0; + const single = !isGroup ? targets[0] : undefined; + + const hint = !enabled + ? isGroup + ? "⚠ Not drivable - no DBP in selection" + : `⚠ Not drivable - no DBP in currently running ${single?.state === "Running" ? "image" : single?.state.toLowerCase()}` + : anyAuto + ? `▶ Navigating · ${activeCount} waypoint${activeCount === 1 ? "" : "s"} left` + : `${isGroup ? `${drivable.length} of ${selCount} drivable · ` : "◉ "}Drag pad to drive · ⌥ Alt-click map to add waypoints${pending.length ? ` · ${pending.length} queued` : ""}`; + + const popBase: React.CSSProperties = { + position: "absolute", + bottom: 72, + left: 0, + width: 212, + background: "var(--surface)", + border: "1px solid var(--hairline)", + borderRadius: 10, + padding: 12, + boxShadow: "0 10px 30px rgba(0,0,0,.45)", + zIndex: 30, + }; + + return ( +
+
Control
+
+
+ +
+
+ {/* LED button */} +
enabled && setLedOpen((v) => !v)} + style={{ + display: "flex", + alignItems: "center", + gap: 7, + padding: "8px 12px", + borderRadius: 8, + background: "var(--elevated)", + border: "1px solid var(--hairline)", + cursor: "pointer", + fontSize: 12, + ...(enabled ? {} : gateOff), + }} + > +
+ LED{isGroup ? " all" : ""} +
+ {/* waypoint group: [Waypoints · N][Go / Stop nav][Clear] */} +
+
setWpOpen((v) => !v)} + style={{ + padding: "6px 11px", + cursor: "pointer", + fontSize: 12, + display: "flex", + alignItems: "center", + gap: 6, + whiteSpace: "nowrap", + background: wpOpen ? "rgba(228,3,46,.14)" : "transparent", + }} + > + ◎ Waypoints{wpCount ? ` · ${wpCount}` : ""} +
+ {(pending.length > 0 || anyAuto) && ( +
(anyAuto ? onStopNav() : onGo())} + style={{ + display: "flex", + alignItems: "center", + padding: "6px 11px", + cursor: "pointer", + fontSize: 12, + fontWeight: 600, + whiteSpace: "nowrap", + borderLeft: "1px solid var(--hairline)", + color: anyAuto ? "var(--text)" : "var(--accent)", + }} + > + {anyAuto ? "■ Stop nav" : "▶ Go"} +
+ )} + {pending.length > 0 && ( +
+ Clear +
+ )} +
+
+
+
{hint}
+ + {/* click-away overlay */} + {(ledOpen || wpOpen) && ( +
{ + setLedOpen(false); + setWpOpen(false); + }} + /> + )} + + {/* LED popover */} + {ledOpen && ( +
+
LED color{isGroup ? ` · ${drivable.length} bots` : ""}
+
+ {SWATCHES.map(([r, g, b], i) => ( +
{ + drivable.forEach((bot) => + putRgbLed(bot.id, bot.application, { red: r, green: g, blue: b }).catch(() => {}), + ); + onToast(`LED set on ${drivable.length} bot${drivable.length > 1 ? "s" : ""}`); + setLedOpen(false); + }} + style={{ + width: "100%", + aspectRatio: 1, + borderRadius: 6, + cursor: "pointer", + background: `rgb(${r},${g},${b})`, + boxShadow: "0 0 0 1px rgba(255,255,255,.08)", + }} + /> + ))} +
+
+ )} + + {/* waypoint queue popover */} + {wpOpen && ( +
+
+ Waypoint queue + {pending.length > 0 && ( + + Clear all + + )} +
+ {pending.length > 0 ? ( +
+ {pending.map((w, i) => ( +
+ {i + 1} + + {Math.round(w.x)}, {Math.round(w.y)} mm + + onRemovePending(i)} + style={{ marginLeft: "auto", color: "var(--muted)", cursor: "pointer", fontSize: 14, lineHeight: 1, padding: "0 3px" }} + > + × + +
+ ))} +
+ ) : ( +
+ ⌥ Alt-click the map to add waypoints for {isGroup ? "the selection" : "this bot"}. +
+ )} +
+ )} +
+ ); +}; + +const Sep: React.FC = () =>
; + +export const Footer: React.FC = (props) => { + const selected = props.bots.filter((b) => props.selection.has(b.id)); + const one = selected.length === 1 ? selected[0] : undefined; + + return ( +
+ + +
+ {/* NONE: fleet rollup (all states, zero-count rows dimmed, per v1) */} + {selected.length === 0 && ( +
+
+
+ {props.bots.length} + / 1000 +
+
DotBots online
+
+ +
+ {STATE_ORDER.map((s) => { + const ids = props.bots.filter((b) => b.state === s).map((b) => b.id); + return ( +
ids.length && props.onSelectState(ids)} + title={ids.length ? "Select these bots" : undefined} + style={{ + display: "flex", + alignItems: "center", + gap: 9, + opacity: ids.length ? 1 : 0.4, + cursor: ids.length ? "pointer" : "default", + }} + > + + {ids.length} + {s} +
+ ); + })} +
+
+ )} + + {/* ONE: selected bot */} + {one && ( +
+
+
+
+ {short(one.id)} +
+ {one.id.toUpperCase()} + {one.deviceType} + {"—"} + + ◎ {one.drivable ? "Drivable" : "Not drivable"} + +
+ +
+
Status
+
+ + {one.state} +
+ {props.flashQueue[one.id] && !props.flashQueue[one.id].done && ( +
+
+ {props.flashQueue[one.id].acked} / {props.flashQueue[one.id].total} chunks +
+
+
+
+
+ )} +
+ + + {one.position ? `${Math.round(one.position.x)}, ${Math.round(one.position.y)} mm` : "— unknown"} + +
+
+ + +
+
+ )} + + {/* MULTI */} + {selected.length > 1 && ( +
+
+
+ ☰ +
+
+ {selected.length} + selected + props.onSelectState([])} style={{ fontSize: 11, color: "var(--accent)", cursor: "pointer" }}> + Clear selection + +
+
+ +
+
Status
+
+ {STATE_ORDER.map((s) => { + const n = selected.filter((b) => b.state === s).length; + return n > 0 ? ( +
+ + {n} + {s} +
+ ) : null; + })} +
+
+ + +
+
+ )} +
+
+ ); +}; diff --git a/dotbot/console-web/src/GridView.tsx b/dotbot/console-web/src/GridView.tsx new file mode 100644 index 00000000..8b79ff67 --- /dev/null +++ b/dotbot/console-web/src/GridView.tsx @@ -0,0 +1,120 @@ +import React from "react"; + +import { UnifiedBot } from "./types"; +import { BatteryCell, FilterBar, LedDot, Pagination, useQueriedBots, useViewQuery } from "./viewChrome"; + +interface GridViewProps { + bots: UnifiedBot[]; + selection: Set; + onSelect: (ids: string[], mode: "replace" | "toggle" | "add") => void; +} + +export const GridView: React.FC = ({ bots, selection, onSelect }) => { + const { q, setQ } = useViewQuery(); + const { rows, total, pages } = useQueriedBots(bots, q); + // File-manager selection: click = single, shift+click = range from the + // anchor in the current card order, cmd/ctrl = toggle. + const anchorRef = React.useRef(null); + const cardClick = (e: React.MouseEvent, id: string) => { + if (e.shiftKey && anchorRef.current) { + const ids = rows.map((r) => r.id); + const a = ids.indexOf(anchorRef.current); + const b = ids.indexOf(id); + if (a >= 0 && b >= 0) { + onSelect(ids.slice(Math.min(a, b), Math.max(a, b) + 1), "add"); + return; + } + } + if (e.metaKey || e.ctrlKey) { + onSelect([id], "toggle"); + anchorRef.current = id; + return; + } + // Plain click on the sole selected item deselects it. + if (selection.has(id) && selection.size === 1) { + onSelect([], "replace"); + anchorRef.current = null; + return; + } + onSelect([id], "replace"); + anchorRef.current = id; + }; + + return ( +
onSelect([], "replace")} + > +
e.stopPropagation()}> + +
+
+
+ {rows.map((b) => { + const checked = selection.has(b.id); + return ( +
{ + e.stopPropagation(); + cardClick(e, b.id); + }} + style={{ + display: "flex", + flexDirection: "column", + gap: 9, + padding: "13px 14px", + borderRadius: 10, + background: "var(--surface)", + border: checked ? "1px solid var(--accent)" : "1px solid var(--hairline)", + boxShadow: checked ? "0 0 0 1px var(--accent)" : "none", + cursor: "pointer", + }} + > +
+ + {b.id.slice(-4)} +
+ + {b.state} +
+
{b.id}
+ +
+ Device {b.deviceType} +
+
+ ); + })} +
+
+
e.stopPropagation()}> + +
+
+ ); +}; diff --git a/dotbot/console-web/src/Joystick.tsx b/dotbot/console-web/src/Joystick.tsx new file mode 100644 index 00000000..b3109467 --- /dev/null +++ b/dotbot/console-web/src/Joystick.tsx @@ -0,0 +1,186 @@ +import React, { useEffect, useRef, useState } from "react"; + +import { putMoveRaw } from "./api"; +import { UnifiedBot } from "./types"; + +// v1 drive pad: 64px rounded square, crosshair guides, LED-colored knob with +// the bot's live heading pointer (single) or an accent knob with a xN count +// (group). +// +// Control model: SCREEN-relative steering. The pad vector is the direction +// you want the bot to move on the (north-up) map; the pad closes the heading +// loop itself at 10 Hz: steer = P * heading-error, throttle scales with +// deflection and drops while the bot is badly misaligned. This replaces the +// classic body-relative mapping (y = throttle along heading, x = yaw rate), +// which felt erratic against the simulator's motion-derived heading. +// +// Caveat baked into the throttle floor: the controller derives heading from +// MOTION (successive position fixes), so a bot spinning in place reports no +// heading change - the loop always keeps some forward speed so the heading +// stays observable. +const SPEED_OFFSET = 30; +const PAD = 64; +const R = 20; // knob travel radius, as in v1 + +const clampPwm = (v: number) => Math.max(-128, Math.min(127, Math.trunc(v))); + +function steerTowards(bot: UnifiedBot, kx: number, ky: number): { left: number; right: number } { + const mag = Math.min(1, Math.hypot(kx, ky) / R); + if (mag < 0.12) return { left: 0, right: 0 }; + // Desired motion direction in the controller's convention (0 = north/+y, + // positive CCW): dir = -atan2(ax, ay) with arena ax = kx, ay = -ky (screen + // y grows down, arena y grows up). + const desired = (-Math.atan2(kx, -ky) * 180) / Math.PI; + let error = bot.heading === null ? 0 : desired - bot.heading; + while (error > 180) error -= 360; + while (error < -180) error += 360; + // Throttle: floor keeps the heading observable; alignment factor slows the + // bot down while it still points the wrong way. + const align = Math.max(0, Math.cos((error * Math.PI) / 180)); + const throttle = 25 + mag * 55 * align; + // Verified against the simulator: left channel faster = CCW on the + // north-up map = heading (CCW-positive) increases. + const steer = Math.max(-55, Math.min(55, error * 0.9)); + let left = throttle + steer; + let right = throttle - steer; + if (left > 0) left += SPEED_OFFSET; + if (left < 0) left -= SPEED_OFFSET; + if (right > 0) right += SPEED_OFFSET; + if (right < 0) right -= SPEED_OFFSET; + return { left: clampPwm(left), right: clampPwm(right) }; +} + +interface PadProps { + targets: UnifiedBot[]; // drivable bots to drive together + disabled: boolean; // parent applies the gate style; this blocks input +} + +export const Pad: React.FC = ({ targets, disabled }) => { + const [knob, setKnob] = useState({ x: 0, y: 0 }); + const [active, setActive] = useState(false); + const knobRef = useRef(knob); + knobRef.current = knob; + const targetsRef = useRef(targets); + targetsRef.current = targets; + + const single = targets.length === 1 ? targets[0] : null; + const led = single + ? single.led + ? `rgb(${single.led.red},${single.led.green},${single.led.blue})` + : "var(--s-Inactive)" + : null; + + useEffect(() => { + if (!active) return; + const t = setInterval(() => { + targetsRef.current.forEach((b) => { + const { left, right } = steerTowards(b, knobRef.current.x, knobRef.current.y); + putMoveRaw(b.id, b.application, left, right).catch(() => {}); + }); + }, 100); + return () => clearInterval(t); + }, [active]); + + const stop = () => { + if (!active) return; + setActive(false); + setKnob({ x: 0, y: 0 }); + targetsRef.current.forEach((b) => { + putMoveRaw(b.id, b.application, 0, 0).catch(() => {}); + }); + }; + + const move = (e: React.PointerEvent) => { + const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); + let dx = e.clientX - (r.left + r.width / 2); + let dy = e.clientY - (r.top + r.height / 2); + const len = Math.hypot(dx, dy); + if (len > R) { + dx = (dx / len) * R; + dy = (dy / len) * R; + } + setKnob({ x: dx, y: dy }); + }; + + return ( +
{ + if (disabled || targets.length === 0) return; + setActive(true); + (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + move(e); + }} + onPointerMove={(e) => active && move(e)} + onPointerUp={stop} + onPointerCancel={stop} + title="Drag pad to drive" + style={{ + width: PAD, + height: PAD, + flex: "none", + borderRadius: 12, + background: "var(--elevated)", + border: `1px solid ${active ? "var(--accent)" : "var(--hairline)"}`, + position: "relative", + touchAction: "none", + cursor: "grab", + }} + > + {/* crosshair guides */} +
+
+ {single ? ( +
+ {single.heading !== null && ( +
+ )} +
+ ) : ( +
+ ×{targets.length} +
+ )} +
+ ); +}; diff --git a/dotbot/console-web/src/ListView.tsx b/dotbot/console-web/src/ListView.tsx new file mode 100644 index 00000000..2e9ae1e5 --- /dev/null +++ b/dotbot/console-web/src/ListView.tsx @@ -0,0 +1,212 @@ +import React from "react"; + +import { UnifiedBot } from "./types"; +import { + BatteryCell, + FilterBar, + LedDot, + Pagination, + SortKey, + useQueriedBots, + useViewQuery, +} from "./viewChrome"; + +interface ListViewProps { + bots: UnifiedBot[]; + selection: Set; + onSelect: (ids: string[], mode: "replace" | "toggle" | "add") => void; +} + +export const ListView: React.FC = ({ bots, selection, onSelect }) => { + const { q, setQ } = useViewQuery(); + const { rows, total, pages } = useQueriedBots(bots, q); + // File-manager selection: click = single, shift+click = range from the + // anchor (last plain/cmd click) in the current row order, cmd/ctrl = toggle. + const anchorRef = React.useRef(null); + const rowClick = (e: React.MouseEvent, id: string) => { + if (e.shiftKey && anchorRef.current) { + const ids = rows.map((r) => r.id); + const a = ids.indexOf(anchorRef.current); + const b = ids.indexOf(id); + if (a >= 0 && b >= 0) { + onSelect(ids.slice(Math.min(a, b), Math.max(a, b) + 1), "add"); + return; + } + } + if (e.metaKey || e.ctrlKey) { + onSelect([id], "toggle"); + anchorRef.current = id; + return; + } + // Plain click on the sole selected item deselects it. + if (selection.has(id) && selection.size === 1) { + onSelect([], "replace"); + anchorRef.current = null; + return; + } + onSelect([id], "replace"); + anchorRef.current = id; + }; + + const sortBy = (key: SortKey) => + setQ((p) => ({ + ...p, + sortKey: key, + sortDir: p.sortKey === key ? ((p.sortDir * -1) as 1 | -1) : 1, + })); + const arrow = (key: SortKey) => (q.sortKey === key ? (q.sortDir === 1 ? " ↑" : " ↓") : ""); + + const allVisibleSelected = rows.length > 0 && rows.every((b) => selection.has(b.id)); + const toggleAll = () => { + if (allVisibleSelected) onSelect(rows.map((b) => b.id), "toggle"); // all off + else onSelect(rows.filter((b) => !selection.has(b.id)).map((b) => b.id), "add"); + }; + + const th: React.CSSProperties = { + padding: "11px 12px", + position: "sticky", + top: 0, + background: "var(--surface)", + borderBottom: "1px solid var(--hairline)", + fontSize: 10, + letterSpacing: ".5px", + textTransform: "uppercase", + color: "var(--muted)", + textAlign: "left", + cursor: "pointer", + userSelect: "none", + }; + const checkBox = (checked: boolean): React.CSSProperties => ({ + width: 15, + height: 15, + borderRadius: 4, + border: "1px solid var(--hairline)", + background: checked ? "var(--accent)" : "transparent", + color: "#fff", + fontSize: 10, + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + }); + + return ( +
onSelect([], "replace")} + > +
e.stopPropagation()}> + +
+
+ + + + + + + + + + + + {rows.map((b) => { + const checked = selection.has(b.id); + return ( + { + e.stopPropagation(); + rowClick(e, b.id); + }} + style={{ + cursor: "pointer", + background: checked ? "rgba(228,3,46,.07)" : "transparent", + borderLeft: checked ? "2px solid var(--accent)" : "2px solid transparent", + }} + > + + + + + + + ); + })} + +
+
{ + e.stopPropagation(); + toggleAll(); + }} + style={checkBox(allVisibleSelected)} + > + {allVisibleSelected ? "✓" : ""} +
+
{ e.stopPropagation(); sortBy("id"); }}> + ID{arrow("id")} + { e.stopPropagation(); sortBy("fw"); }}> + Device{arrow("fw")} + { e.stopPropagation(); sortBy("battery"); }}> + Battery{arrow("battery")} + { e.stopPropagation(); sortBy("state"); }}> + State{arrow("state")} +
+
{ + e.stopPropagation(); + onSelect([b.id], "toggle"); + anchorRef.current = b.id; + }} + style={checkBox(checked)} + > + {checked ? "✓" : ""} +
+
+
+ + {b.id} +
+
+ {b.deviceType} + + + +
+ + {b.state} +
+
+
+
e.stopPropagation()}> + +
+
+ ); +}; diff --git a/dotbot/console-web/src/MapView.tsx b/dotbot/console-web/src/MapView.tsx new file mode 100644 index 00000000..55c75776 --- /dev/null +++ b/dotbot/console-web/src/MapView.tsx @@ -0,0 +1,569 @@ +import React, { useCallback, useMemo, useRef, useState } from "react"; + +import { LH2Position, MapSize, UnifiedBot } from "./types"; +import { useSmoothPositions } from "./useSmoothPositions"; + +// Layer set mirrors the v1 design (Battery Bars / Waypoints / HotSpots / +// DotBots / Real-scale bots); Trails is our addition on top. +export interface Layers { + batteryBars: boolean; + waypoints: boolean; + hotSpots: boolean; + dotBots: boolean; + trueScale: boolean; + trails: boolean; +} + +export interface Camera { + scale: number; + tx: number; + ty: number; +} + +export interface ViewGeom { + w: number; + h: number; + side: number; +} + +// v1 clampPan: keep the arena reachable, never fling it off-screen. +export function clampCam(cam: Camera, geom: ViewGeom): Camera { + const sw = geom.side * cam.scale; + const padX = Math.max(0, (geom.w - geom.side) / 2); + const padY = Math.max(0, (geom.h - geom.side) / 2); + const mx = Math.max(0, (sw - geom.w) / 2) + padX; + const my = Math.max(0, (sw - geom.h) / 2) + padY; + return { + ...cam, + tx: Math.max(-mx, Math.min(mx, cam.tx)), + ty: Math.max(-my, Math.min(my, cam.ty)), + }; +} + +interface MapViewProps { + bots: UnifiedBot[]; + mapSize: MapSize; + selection: Set; + layers: Layers; + plannedMissions: { waypoints: LH2Position[]; led: string | null }[]; // local queues, not yet sent + cam: Camera; + setCam: React.Dispatch>; + onGeom: (g: ViewGeom) => void; + onSelect: (ids: string[], mode: "replace" | "toggle" | "add") => void; + onAddWaypoint: (p: LH2Position) => void; +} + +const BOT_R = 11; // px radius of the bot circle at zoom 1 (22px glyph, as in v1) +const REAL_BOT_MM = 80; // approximate DotBot footprint for the Real-scale layer + +const ledCss = (b: UnifiedBot) => + b.led ? `rgb(${b.led.red},${b.led.green},${b.led.blue})` : "var(--s-Inactive)"; + +const batColor = (pct: number) => + pct < 0.2 ? "var(--s-Stopping)" : pct < 0.45 ? "var(--s-Programming)" : "var(--s-Running)"; + +const batPct = (volts: number) => Math.max(0, Math.min(1, (volts - 2.0) / (4.2 - 2.0))); + +export const MapView: React.FC = (props) => { + const wrapRef = useRef(null); + const { cam, setCam } = props; + const [marquee, setMarquee] = useState<{ x0: number; y0: number; x1: number; y1: number } | null>(null); + const [hoverId, setHoverId] = useState(null); + const panRef = useRef<{ x0: number; y0: number; tx0: number; ty0: number; moved: boolean } | null>(null); + const marqueeRef = useRef<{ additive: boolean } | null>(null); + const geomRef = useRef({ w: 1000, h: 600, side: 600 }); + + const mapDiagonal = Math.hypot(props.mapSize.width, props.mapSize.height); + const smoothPositions = useSmoothPositions(props.bots, mapDiagonal); + + const [side, setSide] = useState(600); + const onGeomRef = useRef(props.onGeom); + onGeomRef.current = props.onGeom; + // Track the canvas size live (rail open/close, window resize): the arena + // keeps its margins instead of overflowing when the canvas shrinks. + const measure = useCallback((el: HTMLDivElement | null) => { + (wrapRef as React.MutableRefObject).current = el; + }, []); + React.useEffect(() => { + const el = wrapRef.current; + if (!el) return; + const update = () => { + const r = el.getBoundingClientRect(); + const s = Math.max(200, Math.min(r.width, r.height) - 48); + setSide(s); + geomRef.current = { w: r.width, h: r.height, side: s }; + onGeomRef.current(geomRef.current); + }; + update(); + const ro = new ResizeObserver(update); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + // v1 coordinate convention: y grows NORTH (up); screen top = arena max-y. + const pctPos = (p: LH2Position) => ({ + left: (p.x / props.mapSize.width) * 100, + top: (1 - p.y / props.mapSize.height) * 100, + }); + + const pxToMm = (clientX: number, clientY: number): LH2Position | null => { + const el = wrapRef.current; + if (!el) return null; + const r = el.getBoundingClientRect(); + const cx = r.left + r.width / 2; + const cy = r.top + r.height / 2; + const ux = (clientX - cx - cam.tx) / cam.scale + r.width / 2; + const uy = (clientY - cy - cam.ty) / cam.scale + r.height / 2; + const ax = ux - (r.width - side) / 2; + const ay = uy - (r.height - side) / 2; + const x = (ax / side) * props.mapSize.width; + const y = (1 - ay / side) * props.mapSize.height; + if (x < 0 || y < 0 || x > props.mapSize.width || y > props.mapSize.height) return null; + return { x: Math.round(x), y: Math.round(y) }; + }; + + // v1 canvas semantics: alt-click = waypoint, shift-drag = marquee, + // plain drag = pan, plain click (no movement) = clear selection. + const onCanvasDown = (e: React.PointerEvent) => { + if (e.button !== 0) return; + (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + if (e.altKey) { + const p = pxToMm(e.clientX, e.clientY); + if (p) props.onAddWaypoint(p); + return; + } + if (e.shiftKey) { + marqueeRef.current = { additive: true }; + setMarquee({ x0: e.clientX, y0: e.clientY, x1: e.clientX, y1: e.clientY }); + return; + } + panRef.current = { x0: e.clientX, y0: e.clientY, tx0: cam.tx, ty0: cam.ty, moved: false }; + }; + + const onCanvasMove = (e: React.PointerEvent) => { + if (panRef.current) { + const p = panRef.current; + const dx = e.clientX - p.x0; + const dy = e.clientY - p.y0; + if (Math.abs(dx) + Math.abs(dy) > 3) p.moved = true; + setCam((c) => clampCam({ ...c, tx: p.tx0 + dx, ty: p.ty0 + dy }, geomRef.current)); + return; + } + if (marqueeRef.current) setMarquee((m) => (m ? { ...m, x1: e.clientX, y1: e.clientY } : m)); + }; + + const onCanvasUp = () => { + if (panRef.current) { + const moved = panRef.current.moved; + panRef.current = null; + if (!moved) props.onSelect([], "replace"); // plain click on empty canvas clears + return; + } + if (!marqueeRef.current || !marquee) { + marqueeRef.current = null; + setMarquee(null); + return; + } + const x0 = Math.min(marquee.x0, marquee.x1); + const x1 = Math.max(marquee.x0, marquee.x1); + const y0 = Math.min(marquee.y0, marquee.y1); + const y1 = Math.max(marquee.y0, marquee.y1); + if (x1 - x0 >= 5 || y1 - y0 >= 5) { + const hits = props.bots + .filter((b) => { + if (!b.position) return false; + const el = document.getElementById(`bot-${b.id}`); + if (!el) return false; + const r = el.getBoundingClientRect(); + const cx = r.left + r.width / 2; + const cy = r.top + r.height / 2; + return cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1; + }) + .map((b) => b.id); + props.onSelect(hits, "add"); + } + marqueeRef.current = null; + setMarquee(null); + }; + + const gridStep = side / 10; + const gridBg = useMemo( + () => + `repeating-linear-gradient(0deg, var(--grid) 0 1px, transparent 1px ${gridStep}px),` + + `repeating-linear-gradient(90deg, var(--grid) 0 1px, transparent 1px ${gridStep}px)`, + [gridStep], + ); + + // Real-scale layer: glyphs scale to the actual DotBot footprint. + const gscale = props.layers.trueScale + ? Math.max(0.2, (side * (REAL_BOT_MM / props.mapSize.width)) / (BOT_R * 2)) + : 1; + + return ( +
+ {/* camera layer */} +
+ {/* arena */} +
+ {/* subtle center accents (v1) */} +
+
+ + {/* trails (our extra layer) */} + {props.layers.trails && ( + + {props.bots + .filter((b) => b.trail.length > 1) + .map((b) => ( + `${(p.x / props.mapSize.width) * side},${(1 - p.y / props.mapSize.height) * side}`) + .join(" ")} + fill="none" + stroke={ledCss(b)} + strokeWidth={1} + opacity={0.35} + /> + ))} + + )} + + {/* waypoints: per-bot LED-colored diamonds (v1: no connector lines) */} + {props.layers.waypoints && + props.bots.flatMap((b) => { + if (b.waypoints.length === 0) return []; + const isSel = props.selection.has(b.id); + const led = ledCss(b); + const s = (isSel ? 10 : 8) * gscale; + return b.waypoints.map((w, i) => { + const q = pctPos(w); + return ( +
+ ); + }); + })} + + {/* planned (queued, not sent) waypoints */} + {props.layers.waypoints && + props.plannedMissions.flatMap((m, mi) => + m.waypoints.map((p, i) => { + const q = pctPos(p); + const led = m.led ?? "var(--accent)"; + return ( +
+ ); + }), + )} + + {/* bots (v1 glyph: state-colored body, LED pip, drive dot, chip label) */} + {props.layers.dotBots && + props.bots + .filter((b) => b.position) + .map((b) => { + const q = pctPos(smoothPositions.get(b.id) ?? b.position!); + const selected = props.selection.has(b.id); + const hovered = hoverId === b.id; + const led = ledCss(b); + const stc = `var(--s-${b.state})`; + const pct = batPct(b.battery); + const blink = b.state === "Programming" || b.state === "Resetting"; + return ( +
{ + e.stopPropagation(); + if (e.altKey) { + const p = pxToMm(e.clientX, e.clientY); + if (p) props.onAddWaypoint(p); + return; + } + if (e.shiftKey || e.metaKey || e.ctrlKey) { + props.onSelect([b.id], "toggle"); + } else if (props.selection.has(b.id) && props.selection.size === 1) { + props.onSelect([], "replace"); // click the sole selected bot again = deselect + } else { + props.onSelect([b.id], "replace"); + } + }} + onPointerEnter={() => setHoverId(b.id)} + onPointerLeave={() => setHoverId((h) => (h === b.id ? null : h))} + style={{ + position: "absolute", + left: `${q.left}%`, + top: `${q.top}%`, + transform: `translate(-50%, -50%) scale(${gscale})`, + cursor: "pointer", + zIndex: selected ? 6 : 2, + width: 0, + height: 0, + }} + > + {/* selection rectangle */} + {selected && ( +
+ )} + {/* battery bar */} + {props.layers.batteryBars && ( +
+
+
+ )} + {/* body: state-colored circle */} +
+ {/* heading pointer (state-colored, at the edge). The + controller reports direction as 0 = north (+y), + positive COUNTERclockwise; CSS rotates clockwise in + screen coords, so the angle is negated. */} + {b.heading !== null && ( +
+
+
+ )} + {/* drive dot: white ring at center = drivable; its FILL is + the LED color (experiment: merges the v1 LED pip into the + drive indicator - see design-feedback) */} + {b.drivable && ( +
+ )} + {/* chip label: selected or hovered only */} + {(selected || hovered) && ( +
+ {b.id.slice(-4).toUpperCase()} +
+ )} +
+ ); + })} +
+
+ + {/* marquee */} + {marquee && ( +
+ )} + + {/* zoom controls */} +
e.stopPropagation()} + > + {[ + { label: "+", fn: () => setCam((c) => clampCam({ ...c, scale: Math.min(4, c.scale * 1.25) }, geomRef.current)) }, + { label: "−", fn: () => setCam((c) => clampCam({ ...c, scale: Math.max(0.5, c.scale / 1.25) }, geomRef.current)) }, + { label: "◎", fn: () => setCam({ scale: 1, tx: 0, ty: 0 }) }, + ].map((z, i) => ( +
+ {z.label} +
+ ))} +
+ + {/* hint */} +
+ drag = pan · shift-drag = select · ⌥ alt-click = waypoint +
+
+ ); +}; diff --git a/dotbot/console-web/src/Minimap.tsx b/dotbot/console-web/src/Minimap.tsx new file mode 100644 index 00000000..3d143278 --- /dev/null +++ b/dotbot/console-web/src/Minimap.tsx @@ -0,0 +1,125 @@ +import React, { useRef } from "react"; + +import { Camera, clampCam, ViewGeom } from "./MapView"; +import { MapSize, UnifiedBot } from "./types"; + +interface MinimapProps { + bots: UnifiedBot[]; + mapSize: MapSize; + cam: Camera; + setCam: React.Dispatch>; + geom: ViewGeom | null; +} + +// Whole-arena overview with the current map viewport as a rectangle. +// Dragging moves the camera (the design pans through here). The arena box +// keeps the real arena aspect ratio - a 2000x2000 arena is a square. +export const Minimap: React.FC = ({ bots, mapSize, cam, setCam, geom }) => { + const boxRef = useRef(null); + const dragging = useRef(false); + + const viewportRect = () => { + if (!geom) return null; + const { w, h, side } = geom; + const span = (extent: number, t: number) => { + const min = 0.5 - (extent / 2 + t) / (side * cam.scale); + const max = 0.5 + (extent / 2 - t) / (side * cam.scale); + return [Math.max(0, min), Math.min(1, max)]; + }; + const [x0, x1] = span(w, cam.tx); + const [y0, y1] = span(h, cam.ty); + return { x0, x1, y0, y1 }; + }; + + const centerOn = (clientX: number, clientY: number) => { + const el = boxRef.current; + if (!el || !geom) return; + const r = el.getBoundingClientRect(); + const fx = Math.max(0, Math.min(1, (clientX - r.left) / r.width)); + const fy = Math.max(0, Math.min(1, (clientY - r.top) / r.height)); + setCam((c) => + clampCam( + { ...c, tx: -(fx - 0.5) * geom.side * c.scale, ty: -(fy - 0.5) * geom.side * c.scale }, + geom, + ), + ); + }; + + const rect = viewportRect(); + + return ( +
+
+ Arena · {mapSize.width}×{mapSize.height}mm +
+
+
{ + dragging.current = true; + (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + centerOn(e.clientX, e.clientY); + }} + onPointerMove={(e) => dragging.current && centerOn(e.clientX, e.clientY)} + onPointerUp={() => (dragging.current = false)} + style={{ + position: "relative", + aspectRatio: `${mapSize.width} / ${mapSize.height}`, + height: "100%", + maxWidth: "100%", + background: "var(--canvas)", + border: "1px solid var(--hairline)", + borderRadius: 5, + overflow: "hidden", + cursor: "grab", + touchAction: "none", + }} + > + {bots + .filter((b) => b.position) + .map((b) => ( +
+ ))} + {rect && ( +
+ )} +
+
+
+ ); +}; diff --git a/dotbot/console-web/src/TestbedRail.tsx b/dotbot/console-web/src/TestbedRail.tsx new file mode 100644 index 00000000..029f2843 --- /dev/null +++ b/dotbot/console-web/src/TestbedRail.tsx @@ -0,0 +1,552 @@ +import React, { useState } from "react"; + +import { PlannedMission, UnifiedBot } from "./types"; +import { FlashJob, LogRow } from "./useOrchestration"; + +// Left testbed rail, per v1: collapsed 52px icon strip <-> 340px panel with a +// Testbed tab (orchestration controls - disabled until the swarmit write path +// lands; never mocked) and a Missions tab (waypoint missions derived from +// live state: Planned = the local queue, Active = bots navigating). + +export interface DoneMission { + key: string; + id: string; // short label + t: string; // time string +} + +interface Mission { + key: string; + ids: string[]; + label: string; + count: number; + n: number; // waypoints (max left among the group) + phase: "planned" | "active"; + dots: string[]; // led css colors, max 4 +} + +interface TestbedRailProps { + bots: UnifiedBot[]; + selection: Set; + planned: PlannedMission[]; + doneMissions: DoneMission[]; + logs: LogRow[]; + jobs: FlashJob[]; + fleetPct: number; + flashing: boolean; + clearLogs: () => void; + onFlashOpen: () => void; + onStart: () => void; + onStop: () => void; + onReset: () => void; + onSelectIds: (ids: string[]) => void; + onGoMission: (key: string) => void; + onDiscardMission: (key: string) => void; + onStopMission: (ids: string[]) => void; +} + +const ledCss = (b: UnifiedBot) => + b.led ? `rgb(${b.led.red},${b.led.green},${b.led.blue})` : "var(--s-Inactive)"; +const short = (id: string) => id.slice(-4).toUpperCase(); +const label10 = { fontSize: 10, letterSpacing: ".5px", textTransform: "uppercase", color: "var(--muted)" } as const; + +// v1 actBtn, rail variant (full width), rendered disabled until orchestration. +const railBtn = (accent: boolean): React.CSSProperties => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: 6, + padding: "7px 13px", + borderRadius: 7, + fontSize: 12, + fontWeight: 500, + whiteSpace: "nowrap", + border: `1px solid ${accent ? "var(--accent)" : "var(--hairline)"}`, + background: accent ? "var(--accent)" : "var(--elevated)", + color: accent ? "#fff" : "var(--text)", + width: "100%", + boxSizing: "border-box", + cursor: "pointer", +}); + +const tabStyle = (active: boolean): React.CSSProperties => ({ + padding: "5px 11px", + borderRadius: 5, + fontSize: 12, + fontWeight: 500, + cursor: "pointer", + background: active ? "var(--elevated)" : "transparent", + color: active ? "var(--text)" : "var(--muted)", +}); + +const topTabStyle = (active: boolean): React.CSSProperties => ({ + padding: "5px 12px", + borderRadius: 6, + fontSize: 12, + fontWeight: active ? 600 : 500, + cursor: "pointer", + background: active ? "var(--accent)" : "transparent", + color: active ? "#fff" : "var(--muted)", + display: "flex", + alignItems: "center", + gap: 6, +}); + +export function deriveMissions(bots: UnifiedBot[], planned: PlannedMission[]): Mission[] { + const missions: Mission[] = []; + const byId = new Map(bots.map((b) => [b.id, b])); + // Planned: local queues, bound to their bots at queue time. + planned.forEach((m) => { + const bs = m.ids.map((id) => byId.get(id)).filter(Boolean) as UnifiedBot[]; + if (!bs.length) return; + missions.push({ + key: m.key, + ids: m.ids, + label: bs.length === 1 ? short(bs[0].id) : `${bs.length} bots`, + count: bs.length, + n: m.waypoints.length, + phase: "planned", + dots: bs.slice(0, 4).map(ledCss), + }); + }); + // Active: navigating bots, grouped by identical mission targets. The + // controller prepends each bot's own start position to the list it stores, + // so the shared mission is the TAIL - skip the first element when the list + // has more than one entry. + const targetsOf = (b: UnifiedBot) => (b.waypoints.length > 1 ? b.waypoints.slice(1) : b.waypoints); + const navving = bots.filter((b) => b.nav === "auto" && b.waypoints.length > 0); + const groups = new Map(); + navving.forEach((b) => { + const sig = targetsOf(b) + .map((w) => `${Math.round(w.x)},${Math.round(w.y)}`) + .join(";"); + const arr = groups.get(sig) ?? []; + arr.push(b); + groups.set(sig, arr); + }); + [...groups.entries()].forEach(([sig, bs]) => { + missions.push({ + key: `active-${sig}`, + ids: bs.map((b) => b.id), + label: bs.length === 1 ? short(bs[0].id) : `${bs.length} bots`, + count: bs.length, + n: Math.max(...bs.map((b) => targetsOf(b).length)), + phase: "active", + dots: bs.slice(0, 4).map(ledCss), + }); + }); + return missions; +} + +export const TestbedRail: React.FC = (props) => { + // ?rail=testbed|missions opens the panel on a tab (handy for dev/screenshots). + const railParam = new URLSearchParams(window.location.search).get("rail"); + const [mode, setMode] = useState<"collapsed" | "panel">( + railParam === "testbed" || railParam === "missions" ? "panel" : "collapsed", + ); + const [top, setTop] = useState<"testbed" | "missions">(railParam === "missions" ? "missions" : "testbed"); + const [tab, setTab] = useState<"console" | "flash">("console"); + + const missions = deriveMissions(props.bots, props.planned); + const targetLabel = props.selection.size ? `${props.selection.size} selected` : "whole fleet"; + + const ico: React.CSSProperties = { + width: 32, + height: 32, + display: "flex", + alignItems: "center", + justifyContent: "center", + borderRadius: 7, + background: "var(--elevated)", + border: "1px solid var(--hairline)", + fontSize: 13, + color: "var(--text)", + }; + + return ( +
+ {/* collapsed icon strip */} + {mode === "collapsed" && ( +
+
setMode("panel")} title="Open testbed" style={{ ...ico, cursor: "pointer" }}> + ▤ +
+
+ {[ + { g: "⇩", t: "Flash…", fn: props.onFlashOpen }, + { g: "▶", t: "Start", fn: props.onStart }, + { g: "■", t: "Stop", fn: props.onStop }, + { g: "↻", t: "Reset", fn: props.onReset }, + ].map((x, i) => ( +
+ {x.g} +
+ ))} +
+
{ + setMode("panel"); + setTop("testbed"); + setTab("console"); + }} + title="Console" + style={{ ...ico, cursor: "pointer" }} + > + ☰ +
+
{ + setMode("panel"); + setTop("missions"); + }} + title={`Missions (${missions.length})`} + style={{ ...ico, cursor: "pointer", position: "relative" }} + > + ◎ + {missions.length > 0 && ( + + {missions.length} + + )} +
+
+ )} + + {/* full panel */} + {mode === "panel" && ( +
+ {/* header */} +
+
+
setTop("testbed")} style={topTabStyle(top === "testbed")}> + Testbed +
+
setTop("missions")} style={topTabStyle(top === "missions")}> + Missions{" "} + + {missions.length} + +
+
+
+ setMode("collapsed")} + title="Collapse" + style={{ cursor: "pointer", color: "var(--muted)", fontSize: 15, lineHeight: 1 }} + > + ‹ + +
+ + {/* TESTBED tab */} + {top === "testbed" && ( +
+
+
+ Target · {targetLabel} +
+
+
+ ⇩ Flash… +
+
+ ▶ Start +
+
+ ■ Stop +
+
+ ↻ Reset +
+
+ {props.flashing && ( +
+
+ Flashing + {props.fleetPct}% +
+
+
+
+
+ )} +
+
+
setTab("console")} style={tabStyle(tab === "console")}> + Console +
+
setTab("flash")} style={tabStyle(tab === "flash")}> + Flash queue +
+
+ {tab === "console" && ( +
+
+ + /events · log_event + +
+ + Clear + +
+
+
+ {props.logs.length === 0 && ( +
No log events yet.
+ )} + {props.logs.map((l) => ( +
+ {l.t} + {l.msg} +
+ ))} +
+
+
+ )} + {tab === "flash" && ( +
+ {props.jobs.length === 0 ? ( +
+ No active flash. Pick targets in any view (or none = whole fleet), then Flash… +
+ ) : ( + props.jobs.map((j) => { + const pct = j.total ? Math.round((j.acked / j.total) * 100) : 0; + return ( +
+
+ {short(j.addr)} + + {j.done ? "done" : `${pct}%`} + +
+
+
+
+
+ ); + }) + )} +
+ )} +
+ )} + + {/* MISSIONS tab */} + {top === "missions" && ( +
+
+ Active waypoint missions +
+ {missions.length > 0 && ( + props.onSelectIds(missions.flatMap((m) => m.ids))} + style={{ fontSize: 11, color: "var(--accent)", cursor: "pointer" }} + > + Select all + + )} +
+
+ {missions.map((m) => ( +
props.onSelectIds(m.ids)} + style={{ + display: "flex", + alignItems: "center", + gap: 10, + padding: "9px 10px", + background: "var(--elevated)", + border: "1px solid var(--hairline)", + borderRadius: 8, + marginBottom: 8, + cursor: "pointer", + }} + > +
+ {m.dots.map((c, i) => ( +
+ ))} +
+ {m.label} + + {m.phase === "active" ? "Active" : "Planned"} + +
+ + ◎ {m.n} + + {m.phase === "planned" && ( +
{ + e.stopPropagation(); + props.onGoMission(m.key); + }} + style={{ + display: "inline-flex", + alignItems: "center", + padding: "3px 10px", + borderRadius: 6, + fontSize: 11, + fontWeight: 600, + cursor: "pointer", + background: "var(--accent)", + color: "#fff", + }} + > + ▶ Go +
+ )} +
{ + e.stopPropagation(); + if (m.phase === "planned") props.onDiscardMission(m.key); + else props.onStopMission(m.ids); + }} + title={m.phase === "active" ? "Interrupt & discard mission" : "Discard mission"} + style={{ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + width: 22, + height: 22, + borderRadius: 6, + cursor: "pointer", + color: "var(--muted)", + fontSize: 16, + }} + > + × +
+
+ ))} + {missions.length === 0 && ( +
+ No missions yet. +
+
+ Select bots · ⌥ Alt-click the map to add waypoints (mission shows as Planned) · + press Go to make it Active. Click a mission to reselect its bots. +
+ )} + {props.doneMissions.length > 0 && ( +
+
Recently completed
+ {props.doneMissions.map((d) => ( +
+ + {d.id} + reached all waypoints +
+ {d.t} +
+ ))} +
+ )} +
+
+ )} +
+ )} +
+ ); +}; diff --git a/dotbot/console-web/src/api.test.ts b/dotbot/console-web/src/api.test.ts new file mode 100644 index 00000000..897fabe3 --- /dev/null +++ b/dotbot/console-web/src/api.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { parseSseChunk } from "./api"; + +describe("parseSseChunk", () => { + it("parses complete frames and keeps the partial remainder", () => { + const { events, rest } = parseSseChunk( + 'data: {"a": 1}\n\ndata: {"a": 2}\n\ndata: {"a"', + ); + expect(events).toEqual([{ a: 1 }, { a: 2 }]); + expect(rest).toBe('data: {"a"'); + }); + + it("skips malformed JSON frames", () => { + const { events } = parseSseChunk('data: not-json\n\ndata: {"ok": true}\n\n'); + expect(events).toEqual([{ ok: true }]); + }); + + it("skips frames without a data line", () => { + const { events } = parseSseChunk(': keepalive\n\ndata: {"ok": true}\n\n'); + expect(events).toEqual([{ ok: true }]); + }); + + it("finds the data line in a multi-field frame", () => { + const { events } = parseSseChunk('event: chunk\ndata: {"acked": 12}\n\n'); + expect(events).toEqual([{ acked: 12 }]); + }); + + it("returns everything as remainder when no frame is complete", () => { + const { events, rest } = parseSseChunk("data: {"); + expect(events).toEqual([]); + expect(rest).toBe("data: {"); + }); +}); diff --git a/dotbot/console-web/src/api.ts b/dotbot/console-web/src/api.ts new file mode 100644 index 00000000..6d084e33 --- /dev/null +++ b/dotbot/console-web/src/api.ts @@ -0,0 +1,141 @@ +import { LH2Position, MapSize, PyDotBot, RgbLed, SwarmitNode } from "./types"; + +// Same-origin in dev thanks to the vite proxy (see vite.config.ts). +const CONTROLLER = "/controller"; +const SWARMIT = "/swarmit"; + +export async function fetchDotBots(): Promise { + const res = await fetch(`${CONTROLLER}/dotbots`); + return res.json(); +} + +export async function fetchMapSize(): Promise { + const res = await fetch(`${CONTROLLER}/map_size`); + return res.json(); +} + +export async function fetchSwarmitStatus(): Promise> { + const res = await fetch(`${SWARMIT}/status`); + const body = await res.json(); + return body.response ?? {}; +} + +export async function putMoveRaw( + address: string, + application: number, + left: number, + right: number, +): Promise { + await fetch(`${CONTROLLER}/dotbots/${address}/${application}/move_raw`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ left_x: 0, left_y: left, right_x: 0, right_y: right }), + }); +} + +export async function putRgbLed( + address: string, + application: number, + led: RgbLed, +): Promise { + await fetch(`${CONTROLLER}/dotbots/${address}/${application}/rgb_led`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(led), + }); +} + +export async function putWaypoints( + address: string, + application: number, + threshold: number, + waypoints: LH2Position[], +): Promise { + await fetch(`${CONTROLLER}/dotbots/${address}/${application}/waypoints`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ threshold, waypoints }), + }); +} + +export function controllerWsUrl(): string { + const proto = window.location.protocol === "https:" ? "wss" : "ws"; + return `${proto}://${window.location.host}${CONTROLLER}/ws/status`; +} + +// --- SwarmIT orchestration (write path; same contract as the real server) --- + +export async function swarmitAction( + action: "start" | "stop" | "reset", + devices?: string[], +): Promise { + await fetch(`${SWARMIT}/${action}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(devices && devices.length ? { devices } : {}), + }); +} + +export interface FlashEvent { + type: "flash_started" | "chunk" | "device_done" | "complete" | "error"; + addr?: string; + acked?: number; + total?: number; + devices?: string[]; + total_chunks?: number; + success?: boolean; + all_success?: boolean; + message?: string; +} + +// Split an SSE buffer into parsed `data:` payloads plus the trailing +// incomplete remainder (kept for the next chunk). Malformed frames are +// skipped. +export function parseSseChunk(buf: string): { events: T[]; rest: string } { + const events: T[] = []; + let idx; + while ((idx = buf.indexOf("\n\n")) >= 0) { + const frame = buf.slice(0, idx); + buf = buf.slice(idx + 2); + const line = frame.split("\n").find((l) => l.startsWith("data: ")); + if (!line) continue; + try { + events.push(JSON.parse(line.slice(6))); + } catch { + /* skip malformed frame */ + } + } + return { events, rest: buf }; +} + +// POST /flash/stream and feed each SSE event to the callback. +export async function flashStream( + firmwareB64: string, + devices: string[] | undefined, + onEvent: (ev: FlashEvent) => void, +): Promise { + const res = await fetch(`${SWARMIT}/flash/stream`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + firmware_b64: firmwareB64, + ...(devices && devices.length ? { devices } : {}), + }), + }); + const reader = res.body?.getReader(); + if (!reader) return; + const decoder = new TextDecoder(); + let buf = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + buf += decoder.decode(value, { stream: true }); + const { events, rest } = parseSseChunk(buf); + buf = rest; + events.forEach(onEvent); + } +} + +export function swarmitEventsUrl(): string { + return `${SWARMIT}/events`; +} diff --git a/dotbot/console-web/src/main.tsx b/dotbot/console-web/src/main.tsx new file mode 100644 index 00000000..e64b87be --- /dev/null +++ b/dotbot/console-web/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import { App } from "./App"; +import "./tokens.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/dotbot/console-web/src/missions.test.ts b/dotbot/console-web/src/missions.test.ts new file mode 100644 index 00000000..dc39917a --- /dev/null +++ b/dotbot/console-web/src/missions.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; + +import { deriveMissions } from "./TestbedRail"; +import { PlannedMission, UnifiedBot } from "./types"; + +const bot = (id: string, over: Partial = {}): UnifiedBot => ({ + id, + state: "Running", + position: { x: 0, y: 0 }, + heading: null, + battery: 3.9, + led: null, + deviceType: "DotBotV3", + application: 0, + drivable: true, + nav: "drive", + waypoints: [], + trail: [], + ...over, +}); + +describe("deriveMissions", () => { + it("keeps a planned mission bound to its bots", () => { + const planned: PlannedMission[] = [ + { + key: "aaaa", + ids: ["aaaa"], + waypoints: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + ], + }, + ]; + const [m] = deriveMissions([bot("aaaa")], planned); + expect(m.phase).toBe("planned"); + expect(m.n).toBe(2); + expect(m.ids).toEqual(["aaaa"]); + }); + + it("drops a planned mission whose bots are gone", () => { + const planned: PlannedMission[] = [ + { key: "gone", ids: ["gone"], waypoints: [{ x: 1, y: 1 }] }, + ]; + expect(deriveMissions([bot("aaaa")], planned)).toEqual([]); + }); + + it("groups active bots by the mission TAIL (own start prepended)", () => { + // The controller stores [own-start, ...targets] per bot: same targets, + // different starts, must land in ONE mission. + const t = [ + { x: 500, y: 500 }, + { x: 900, y: 900 }, + ]; + const a = bot("aaaa", { nav: "auto", waypoints: [{ x: 1, y: 1 }, ...t] }); + const b = bot("bbbb", { nav: "auto", waypoints: [{ x: 2, y: 2 }, ...t] }); + const missions = deriveMissions([a, b], []); + expect(missions).toHaveLength(1); + expect(missions[0].phase).toBe("active"); + expect(missions[0].count).toBe(2); + expect(missions[0].n).toBe(2); + }); + + it("treats a single-entry waypoint list as the target itself", () => { + const a = bot("aaaa", { nav: "auto", waypoints: [{ x: 500, y: 500 }] }); + const [m] = deriveMissions([a], []); + expect(m.n).toBe(1); + }); + + it("ignores bots that are not navigating", () => { + const a = bot("aaaa", { waypoints: [{ x: 1, y: 1 }] }); // nav=drive + expect(deriveMissions([a], [])).toEqual([]); + }); +}); diff --git a/dotbot/console-web/src/tokens.css b/dotbot/console-web/src/tokens.css new file mode 100644 index 00000000..679a0b4c --- /dev/null +++ b/dotbot/console-web/src/tokens.css @@ -0,0 +1,54 @@ +/* Design tokens - the single source of truth for every color in the console. + Swap values here to re-theme; components must reference tokens only. */ +:root { + --canvas: #14161b; + --surface: #1e222b; + --elevated: #2a2f3a; + --grid: #2c313c; + --text: #e6e9ef; + --muted: #9aa4b2; + --accent: #e4032e; + --hairline: rgba(255, 255, 255, 0.07); + + /* State colors are shared across themes: a color always means the same thing. */ + --s-Running: #22c55e; + --s-Programming: #f59e0b; + --s-Bootloader: #38bdf8; + --s-Stopping: #ef4444; + --s-Resetting: #a855f7; + --s-Inactive: #6b7280; + + --font-ui: "IBM Plex Sans", system-ui, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, monospace; +} + +[data-theme="light"] { + --canvas: #f4f6fa; + --surface: #ffffff; + --elevated: #eef1f6; + --grid: #dbe1ea; + --text: #0f172a; + --muted: #5b6577; + --hairline: rgba(0, 0, 0, 0.07); +} + +* { + box-sizing: border-box; +} +body { + margin: 0; +} +a { + color: var(--accent); + text-decoration: none; +} + +@keyframes dbBlink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.35; + } +} diff --git a/dotbot/console-web/src/types.ts b/dotbot/console-web/src/types.ts new file mode 100644 index 00000000..d91d7107 --- /dev/null +++ b/dotbot/console-web/src/types.ts @@ -0,0 +1,89 @@ +// SwarmIT node lifecycle states, plus UI-derived Inactive. +export type BotState = + | "Running" + | "Programming" + | "Bootloader" + | "Stopping" + | "Resetting" + | "Inactive"; + +export const STATE_ORDER: BotState[] = [ + "Running", + "Programming", + "Bootloader", + "Stopping", + "Resetting", + "Inactive", +]; + +// PyDotBot REST/WS shapes (subset the console consumes). +export interface LH2Position { + x: number; + y: number; +} + +export interface RgbLed { + red: number; + green: number; + blue: number; +} + +export interface PyDotBot { + address: string; + application: number; // ApplicationType: 0 = DotBot + status: number; // 0 ACTIVE, 1 INACTIVE, 2 LOST + mode?: number; // ControlModeType: 0 MANUAL, 1 AUTO (navigating waypoints) + direction?: number; + lh2_position?: LH2Position; + position_history?: LH2Position[]; + waypoints?: LH2Position[]; + waypoints_threshold?: number; + rgb_led?: RgbLed; + battery?: number; // volts + calibrated?: number; +} + +export interface WsNotification { + cmd: number; // 1 RELOAD, 2 UPDATE, 4 NEW_DOTBOT + data?: Partial & { + lh2_waypoints?: LH2Position[]; + }; +} + +// SwarmIT /status record. +export interface SwarmitNode { + device: string; + status: string; // Bootloader | Running | Stopping | Resetting | Programming + battery: number; // millivolts + pos_x: number; + pos_y: number; +} + +// The merged per-bot object the UI binds to (controller + swarmit joined by address). +export interface UnifiedBot { + id: string; // hex address, the join key + state: BotState; + position: LH2Position | null; // arena mm + heading: number | null; // degrees + battery: number; // volts + led: RgbLed | null; + deviceType: string; + application: number; + drivable: boolean; // a DBP-speaking image is running (= known to PyDotBot and active) + nav: "drive" | "auto"; // auto = navigating waypoints (firmware AUTO mode) + waypoints: LH2Position[]; // active mission (as reported by the controller) + trail: LH2Position[]; +} + +export interface MapSize { + width: number; + height: number; +} + +// A waypoint mission queued locally but not yet sent: bound to the bots that +// were selected when its waypoints were dropped (survives deselection). +export interface PlannedMission { + key: string; // sorted ids joined + ids: string[]; + waypoints: LH2Position[]; +} diff --git a/dotbot/console-web/src/useFleet.test.ts b/dotbot/console-web/src/useFleet.test.ts new file mode 100644 index 00000000..73f6d675 --- /dev/null +++ b/dotbot/console-web/src/useFleet.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "vitest"; + +import { PyDotBot, SwarmitNode } from "./types"; +import { deriveState, merge } from "./useFleet"; + +const py = (over: Partial = {}): PyDotBot => ({ + address: "badcafe111111111", + application: 0, + status: 0, + ...over, +}); + +const sw = (over: Partial = {}): SwarmitNode => ({ + device: "DotBotV3", + status: "Running", + battery: 3900, + pos_x: 100, + pos_y: 200, + ...over, +}); + +describe("deriveState", () => { + it("control-plane INACTIVE/LOST wins over the swarmit state", () => { + expect(deriveState(py({ status: 1 }), sw())).toBe("Inactive"); + expect(deriveState(py({ status: 2 }), sw({ status: "Programming" }))).toBe( + "Inactive", + ); + }); + + it("passes swarmit lifecycle states through", () => { + for (const s of [ + "Running", + "Programming", + "Bootloader", + "Stopping", + "Resetting", + ]) { + expect(deriveState(py(), sw({ status: s }))).toBe(s); + } + }); + + it("maps an unknown swarmit state to Inactive", () => { + expect(deriveState(py(), sw({ status: "Off" }))).toBe("Inactive"); + }); + + it("control-plane-only active bot is Running", () => { + expect(deriveState(py(), undefined)).toBe("Running"); + }); + + it("no data at all is Inactive", () => { + expect(deriveState(undefined, undefined)).toBe("Inactive"); + }); +}); + +describe("merge", () => { + it("unions both planes and sorts by id", () => { + const bots = merge( + { bbbb: py({ address: "bbbb" }) }, + { aaaa: sw(), bbbb: sw() }, + ); + expect(bots.map((b) => b.id)).toEqual(["aaaa", "bbbb"]); + }); + + it("prefers the controller position, falls back to swarmit", () => { + const [a] = merge( + { a: py({ address: "a", lh2_position: { x: 1, y: 2 } }) }, + { a: sw() }, + ); + expect(a.position).toEqual({ x: 1, y: 2 }); + const [b] = merge({}, { b: sw() }); + expect(b.position).toEqual({ x: 100, y: 200 }); + }); + + it("treats direction -1000 (unknown) as no heading", () => { + const [a] = merge({ a: py({ address: "a", direction: -1000 }) }, {}); + expect(a.heading).toBeNull(); + const [b] = merge({ b: py({ address: "b", direction: 45 }) }, {}); + expect(b.heading).toBe(45); + }); + + it("converts a swarmit-only battery from mV to V", () => { + const [a] = merge({}, { a: sw({ battery: 3900 }) }); + expect(a.battery).toBeCloseTo(3.9); + }); + + it("only an active Running control-plane bot is drivable", () => { + const [a] = merge({ a: py({ address: "a" }) }, {}); + expect(a.drivable).toBe(true); + // swarmit-only (e.g. sitting in the bootloader): never drivable + const [b] = merge({}, { b: sw({ status: "Bootloader" }) }); + expect(b.drivable).toBe(false); + // known to the controller but flashing right now: not drivable + const [c] = merge( + { c: py({ address: "c" }) }, + { c: sw({ status: "Programming" }) }, + ); + expect(c.drivable).toBe(false); + }); + + it("maps firmware AUTO mode to nav=auto", () => { + const [a] = merge({ a: py({ address: "a", mode: 1 }) }, {}); + expect(a.nav).toBe("auto"); + const [b] = merge({ b: py({ address: "b", mode: 0 }) }, {}); + expect(b.nav).toBe("drive"); + }); +}); diff --git a/dotbot/console-web/src/useFleet.ts b/dotbot/console-web/src/useFleet.ts new file mode 100644 index 00000000..02d6b0e0 --- /dev/null +++ b/dotbot/console-web/src/useFleet.ts @@ -0,0 +1,182 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import { controllerWsUrl, fetchDotBots, fetchMapSize, fetchSwarmitStatus } from "./api"; +import { + BotState, + MapSize, + PyDotBot, + SwarmitNode, + UnifiedBot, + WsNotification, +} from "./types"; + +const TRAIL_MAX = 200; + +export function deriveState( + py: PyDotBot | undefined, + sw: SwarmitNode | undefined, +): BotState { + if (py && py.status !== 0) return "Inactive"; // control plane says INACTIVE/LOST + if (sw) { + const s = sw.status as BotState; + return ( + ["Running", "Programming", "Bootloader", "Stopping", "Resetting"] as BotState[] + ).includes(s) + ? s + : "Inactive"; + } + if (py) return "Running"; // control plane only (bare-mode bot): active = running + return "Inactive"; +} + +export function merge( + pyBots: Record, + swNodes: Record, +): UnifiedBot[] { + const ids = new Set([...Object.keys(pyBots), ...Object.keys(swNodes)]); + const out: UnifiedBot[] = []; + for (const id of ids) { + const py = pyBots[id]; + const sw = swNodes[id]; + const state = deriveState(py, sw); + out.push({ + id, + state, + position: + py?.lh2_position ?? (sw ? { x: sw.pos_x, y: sw.pos_y } : null), + heading: + py?.direction !== undefined && py.direction !== -1000 + ? py.direction + : null, + battery: py?.battery ?? (sw ? sw.battery / 1000 : 0), + led: py?.rgb_led ?? null, + deviceType: sw?.device ?? "DotBot", + application: py?.application ?? 0, + // Drivable = a DBP-speaking image is running: the bot is known to the + // control plane and active. SwarmIT-only bots (e.g. sitting in the + // bootloader) are not drivable. + drivable: !!py && py.status === 0 && state === "Running", + nav: py?.mode === 1 ? "auto" : "drive", + waypoints: py?.waypoints ?? [], + trail: py?.position_history?.slice(-TRAIL_MAX) ?? [], + }); + } + return out.sort((a, b) => a.id.localeCompare(b.id)); +} + +export function useFleet(): { + bots: UnifiedBot[]; + mapSize: MapSize; + wsUp: boolean; +} { + const pyRef = useRef>({}); + const swRef = useRef>({}); + const [bots, setBots] = useState([]); + const [mapSize, setMapSize] = useState({ width: 2000, height: 2000 }); + const [wsUp, setWsUp] = useState(false); + + const rebuild = useCallback(() => { + setBots(merge(pyRef.current, swRef.current)); + }, []); + + const reloadDotBots = useCallback(async () => { + try { + const list = await fetchDotBots(); + pyRef.current = Object.fromEntries(list.map((b) => [b.address, b])); + rebuild(); + } catch { + /* controller not up yet; ws reconnect loop will retrigger */ + } + }, [rebuild]); + + // Initial data + map size. + useEffect(() => { + reloadDotBots(); + fetchMapSize() + .then(setMapSize) + .catch(() => {}); + }, [reloadDotBots]); + + // Live updates over the controller WebSocket. + useEffect(() => { + let ws: WebSocket | null = null; + let closed = false; + const connect = () => { + ws = new WebSocket(controllerWsUrl()); + ws.onopen = () => { + setWsUp(true); + reloadDotBots(); + }; + ws.onclose = () => { + setWsUp(false); + if (!closed) setTimeout(connect, 1000); + }; + ws.onerror = () => ws?.close(); + ws.onmessage = (ev) => { + let msg: WsNotification; + try { + msg = JSON.parse(ev.data); + } catch { + return; + } + if (msg.cmd === 2 && msg.data?.address) { + const bot = pyRef.current[msg.data.address]; + if (!bot) { + reloadDotBots(); + return; + } + const d = msg.data; + if (d.direction !== undefined) bot.direction = d.direction; + if (d.battery !== undefined) bot.battery = d.battery; + if (d.rgb_led !== undefined) bot.rgb_led = d.rgb_led; + if (d.lh2_position !== undefined) { + bot.lh2_position = d.lh2_position; + bot.position_history = [ + ...(bot.position_history ?? []), + d.lh2_position!, + ].slice(-TRAIL_MAX); + } + if (d.position_history !== undefined) + bot.position_history = d.position_history; + if (d.lh2_waypoints !== undefined) bot.waypoints = d.lh2_waypoints; + if (d.waypoints_threshold !== undefined) + bot.waypoints_threshold = d.waypoints_threshold; + rebuild(); + } else { + // RELOAD / NEW_DOTBOT / unknown -> refetch everything. + reloadDotBots(); + } + }; + }; + connect(); + return () => { + closed = true; + ws?.close(); + }; + }, [reloadDotBots, rebuild]); + + // Slow refresh for fields the WS does not push (mode/nav, status, waypoint + // clears): the controller only notifies telemetry deltas, so a bot's + // AUTO -> MANUAL arrival flip is invisible without an occasional refetch. + useEffect(() => { + const t = setInterval(reloadDotBots, 3000); + return () => clearInterval(t); + }, [reloadDotBots]); + + // SwarmIT status poll (read-only orchestration plane), 1 Hz. + useEffect(() => { + const tick = async () => { + try { + swRef.current = await fetchSwarmitStatus(); + } catch { + swRef.current = {}; + } + rebuild(); + }; + tick(); + const t = setInterval(tick, 1000); + return () => clearInterval(t); + }, [rebuild]); + + return { bots, mapSize, wsUp }; +} diff --git a/dotbot/console-web/src/useOrchestration.ts b/dotbot/console-web/src/useOrchestration.ts new file mode 100644 index 00000000..40172d72 --- /dev/null +++ b/dotbot/console-web/src/useOrchestration.ts @@ -0,0 +1,126 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import { flashStream, swarmitAction, swarmitEventsUrl } from "./api"; + +export interface LogRow { + key: string; + t: string; // HH:MM:SS + level: "info" | "ok" | "warn" | "err"; + msg: string; +} + +export interface FlashJob { + addr: string; + acked: number; + total: number; + done: boolean; + success?: boolean; +} + +// Orchestration plane: live log feed (swarmit /events SSE) + flash queue +// (driven by /flash/stream chunk events) + the start/stop/reset actions. +export function useOrchestration(onToast: (msg: string) => void) { + const [logs, setLogs] = useState([]); + const [queue, setQueue] = useState>({}); + const [flashing, setFlashing] = useState(false); + + // Log feed. + useEffect(() => { + let es: EventSource | null = null; + let closed = false; + const connect = () => { + es = new EventSource(swarmitEventsUrl()); + es.onmessage = (e) => { + try { + const ev = JSON.parse(e.data); + if (ev.type !== "log_event") return; + const t = new Date((ev.ts ?? Date.now() / 1000) * 1000).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + setLogs((prev) => + [...prev, { key: `${ev.id}`, t, level: ev.level ?? "info", msg: ev.message ?? "" }].slice(-200), + ); + } catch { + /* ignore */ + } + }; + es.onerror = () => { + es?.close(); + if (!closed) setTimeout(connect, 2000); + }; + }; + connect(); + return () => { + closed = true; + es?.close(); + }; + }, []); + + const clearLogs = useCallback(() => setLogs([]), []); + + const act = useCallback( + (action: "start" | "stop" | "reset", devices?: string[]) => { + const target = devices && devices.length ? `${devices.length} device(s)` : "whole fleet"; + swarmitAction(action, devices) + .then(() => onToast(`${action[0].toUpperCase()}${action.slice(1)} sent · ${target}`)) + .catch(() => onToast(`${action} failed`)); + }, + [onToast], + ); + + const flashingRef = useRef(false); + const flash = useCallback( + (firmwareName: string, devices?: string[]) => { + if (flashingRef.current) { + onToast("A flash is already in progress"); + return; + } + flashingRef.current = true; + setFlashing(true); + setQueue({}); + // The fake accepts any payload; against a real server this is the + // base64 of the picked image file. + const b64 = btoa(firmwareName); + flashStream(b64, devices, (ev) => { + if (ev.type === "flash_started" && ev.devices) { + setQueue( + Object.fromEntries( + ev.devices.map((a) => [a, { addr: a, acked: 0, total: ev.total_chunks ?? 0, done: false }]), + ), + ); + } else if (ev.type === "chunk" && ev.addr) { + setQueue((q) => ({ + ...q, + [ev.addr!]: { ...q[ev.addr!], acked: ev.acked ?? 0, total: ev.total ?? 0 }, + })); + } else if (ev.type === "device_done" && ev.addr) { + setQueue((q) => ({ + ...q, + [ev.addr!]: { ...q[ev.addr!], done: true, success: ev.success }, + })); + } else if (ev.type === "complete") { + onToast(ev.all_success ? "Flash complete" : "Flash finished with failures"); + } else if (ev.type === "error") { + onToast(`Flash error: ${ev.message ?? "unknown"}`); + } + }) + .catch(() => onToast("Flash stream failed")) + .finally(() => { + flashingRef.current = false; + setFlashing(false); + // Keep the last queue visible briefly, then clear. + setTimeout(() => setQueue({}), 4000); + }); + }, + [onToast], + ); + + const jobs = Object.values(queue); + const fleetPct = jobs.length + ? Math.round((jobs.reduce((a, j) => a + j.acked, 0) / Math.max(1, jobs.reduce((a, j) => a + j.total, 0))) * 100) + : 0; + + return { logs, clearLogs, queue, jobs, flashing, fleetPct, act, flash }; +} diff --git a/dotbot/console-web/src/useSmoothPositions.test.ts b/dotbot/console-web/src/useSmoothPositions.test.ts new file mode 100644 index 00000000..492ba7c4 --- /dev/null +++ b/dotbot/console-web/src/useSmoothPositions.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; + +import { + MAX_DURATION_MS, + MIN_DURATION_MS, + lerpPos, + nextPosState, + positionAt, +} from "./useSmoothPositions"; + +const MAP_DIAGONAL = Math.hypot(2000, 2000); + +describe("lerpPos", () => { + it("interpolates linearly between two points", () => { + expect(lerpPos({ x: 0, y: 0 }, { x: 100, y: 200 }, 0.5)).toEqual({ x: 50, y: 100 }); + }); +}); + +describe("nextPosState", () => { + it("snaps instantly on first sight of a bot (no prior state)", () => { + const s = nextPosState(undefined, { x: 10, y: 20 }, 1000, MAP_DIAGONAL); + expect(s.duration).toBe(0); + expect(positionAt(s, 1000)).toEqual({ x: 10, y: 20 }); + }); + + it("keeps the previous state when the target has not changed", () => { + const s0 = nextPosState(undefined, { x: 10, y: 20 }, 1000, MAP_DIAGONAL); + const s1 = nextPosState(s0, { x: 10, y: 20 }, 1200, MAP_DIAGONAL); + expect(s1).toBe(s0); + }); + + it("animates from the in-flight interpolated position, not the last target", () => { + // t=0 -> (0,0); update at t=0 sets target (100,0), duration 200ms. + const s0 = nextPosState(undefined, { x: 0, y: 0 }, 0, MAP_DIAGONAL); + const s1 = { ...nextPosState(s0, { x: 100, y: 0 }, 0, MAP_DIAGONAL), duration: 200 }; + // halfway through that transition (t=100ms), a new update arrives. + const s2 = nextPosState(s1, { x: 100, y: 50 }, 100, MAP_DIAGONAL); + expect(s2.from).toEqual({ x: 50, y: 0 }); // interpolated, not (100, 0) + expect(s2.to).toEqual({ x: 100, y: 50 }); + }); + + it("uses the observed update interval as the next animation duration, clamped", () => { + const s0 = nextPosState(undefined, { x: 0, y: 0 }, 0, MAP_DIAGONAL); + const s1 = nextPosState(s0, { x: 10, y: 0 }, 30, MAP_DIAGONAL); // 30ms gap -> clamped up + expect(s1.duration).toBe(MIN_DURATION_MS); + const s2 = nextPosState(s1, { x: 20, y: 0 }, 30 + 5000, MAP_DIAGONAL); // 5s gap -> clamped down + expect(s2.duration).toBe(MAX_DURATION_MS); + const s3 = nextPosState(s2, { x: 30, y: 0 }, 30 + 5000 + 250, MAP_DIAGONAL); // 250ms gap -> as-is + expect(s3.duration).toBe(250); + }); + + it("treats a large jump as a teleport: instant, no animation", () => { + const s0 = nextPosState(undefined, { x: 0, y: 0 }, 0, MAP_DIAGONAL); + const s1 = nextPosState(s0, { x: 1900, y: 1900 }, 100, MAP_DIAGONAL); + expect(s1.duration).toBe(0); + expect(positionAt(s1, 100)).toEqual({ x: 1900, y: 1900 }); + }); +}); + +describe("positionAt", () => { + it("clamps at the target once the duration has elapsed", () => { + const s = nextPosState(nextPosState(undefined, { x: 0, y: 0 }, 0, MAP_DIAGONAL), { x: 100, y: 0 }, 0, MAP_DIAGONAL); + const withDuration = { ...s, duration: 200 }; + expect(positionAt(withDuration, 500)).toEqual({ x: 100, y: 0 }); + }); +}); diff --git a/dotbot/console-web/src/useSmoothPositions.ts b/dotbot/console-web/src/useSmoothPositions.ts new file mode 100644 index 00000000..f1c71b04 --- /dev/null +++ b/dotbot/console-web/src/useSmoothPositions.ts @@ -0,0 +1,111 @@ +import { useEffect, useRef, useState } from "react"; + +import { LH2Position, UnifiedBot } from "./types"; + +// Position updates arrive at whatever rate the source reports them: ~20Hz +// from the simulator, sparser and irregular from real LH2 hardware. A fixed +// CSS transition duration is picked independent of that rate, so it is +// either too long (a fast update interrupts the transition already in +// flight, changing its direction/speed mid-motion) or too short relative to +// the interval between updates (the bot glides for the transition duration, +// then holds still until the next update -- the "saccade" reported on both +// simulated and real bots, on the old frontend too, where there was no +// transition at all and every update snapped instantly). +// +// Using the previous observed update interval as the next animation's +// duration keeps the animation running for roughly the whole gap between +// updates, whatever that gap turns out to be, instead of assuming a rate. +export const MIN_DURATION_MS = 60; +export const MAX_DURATION_MS = 600; + +// A jump bigger than this fraction of the arena diagonal in a single update +// is treated as a teleport (bot re-added, position reset by an operator) +// rather than motion, and rendered instantly instead of animated across the +// whole arena. +export const TELEPORT_FRACTION = 0.35; + +export interface PosState { + from: LH2Position; + to: LH2Position; + t0: number; + duration: number; + lastUpdateAt: number; +} + +export function dist(a: LH2Position, b: LH2Position): number { + return Math.hypot(a.x - b.x, a.y - b.y); +} + +export function lerpPos(a: LH2Position, b: LH2Position, t: number): LH2Position { + return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }; +} + +export function positionAt(state: PosState, now: number): LH2Position { + const t = state.duration > 0 ? Math.min(1, (now - state.t0) / state.duration) : 1; + return lerpPos(state.from, state.to, t); +} + +// Folds one new target position into the previous animation state. `now` +// and `mapDiagonal` are passed in (rather than read from globals) so this +// stays a pure function the hook can be tested through. +export function nextPosState( + prev: PosState | undefined, + target: LH2Position, + now: number, + mapDiagonal: number, +): PosState { + if (!prev) { + return { from: target, to: target, t0: now, duration: 0, lastUpdateAt: now }; + } + if (prev.to.x === target.x && prev.to.y === target.y) return prev; + + const teleport = dist(prev.to, target) > mapDiagonal * TELEPORT_FRACTION; + const interval = now - prev.lastUpdateAt; + const current = positionAt(prev, now); + return { + from: teleport ? target : current, + to: target, + t0: now, + duration: teleport ? 0 : Math.max(MIN_DURATION_MS, Math.min(MAX_DURATION_MS, interval)), + lastUpdateAt: now, + }; +} + +// Smoothed per-bot positions, keyed by bot id. Only bots with a known +// position are present. Re-renders on every animation frame while at least +// one bot is mid-transition; callers read from the returned map instead of +// `bot.position` for the animated glyph, and keep using `bot.position` +// directly for anything that should update instantly (trails, waypoints). +export function useSmoothPositions( + bots: UnifiedBot[], + mapDiagonal: number, +): Map { + const statesRef = useRef>(new Map()); + const [, tick] = useState(0); + + useEffect(() => { + const now = performance.now(); + for (const b of bots) { + if (!b.position) continue; + const prev = statesRef.current.get(b.id); + statesRef.current.set(b.id, nextPosState(prev, b.position, now, mapDiagonal)); + } + }, [bots, mapDiagonal]); + + useEffect(() => { + let raf: number; + const loop = () => { + tick((n) => n + 1); + raf = requestAnimationFrame(loop); + }; + raf = requestAnimationFrame(loop); + return () => cancelAnimationFrame(raf); + }, []); + + const now = performance.now(); + const result = new Map(); + for (const [id, state] of statesRef.current) { + result.set(id, positionAt(state, now)); + } + return result; +} diff --git a/dotbot/console-web/src/viewChrome.tsx b/dotbot/console-web/src/viewChrome.tsx new file mode 100644 index 00000000..860332f4 --- /dev/null +++ b/dotbot/console-web/src/viewChrome.tsx @@ -0,0 +1,203 @@ +import React, { useMemo, useState } from "react"; + +import { BotState, STATE_ORDER, UnifiedBot } from "./types"; + +export const PAGE_SIZE = 50; + +export type SortKey = "id" | "fw" | "battery" | "state"; + +export interface ViewQuery { + search: string; + stateFilter: BotState | "all"; + sortKey: SortKey; + sortDir: 1 | -1; + page: number; +} + +export function useViewQuery() { + const [q, setQ] = useState({ + search: "", + stateFilter: "all", + sortKey: "id", + sortDir: 1, + page: 0, + }); + return { q, setQ }; +} + +export function applyQuery(bots: UnifiedBot[], q: ViewQuery): { rows: UnifiedBot[]; total: number; pages: number } { + let rows = bots; + if (q.search) { + const s = q.search.toLowerCase(); + rows = rows.filter((b) => b.id.toLowerCase().includes(s)); + } + if (q.stateFilter !== "all") rows = rows.filter((b) => b.state === q.stateFilter); + const dir = q.sortDir; + rows = [...rows].sort((a, b) => { + switch (q.sortKey) { + case "battery": + return dir * (a.battery - b.battery); + case "state": + return dir * a.state.localeCompare(b.state); + case "fw": + return dir * a.deviceType.localeCompare(b.deviceType); + default: + return dir * a.id.localeCompare(b.id); + } + }); + const total = rows.length; + const pages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + const page = Math.min(q.page, pages - 1); + return { rows: rows.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE), total, pages }; +} + +export const FilterBar: React.FC<{ + q: ViewQuery; + setQ: React.Dispatch>; + total: number; +}> = ({ q, setQ, total }) => ( +
+
+ + setQ((p) => ({ ...p, search: e.target.value, page: 0 }))} + placeholder="Search by ID" + style={{ + background: "transparent", + border: "none", + outline: "none", + color: "var(--text)", + fontFamily: "var(--font-mono)", + fontSize: 12, + width: "100%", + }} + /> +
+ +
+ + {total} bot{total === 1 ? "" : "s"} + +
+); + +export const Pagination: React.FC<{ + q: ViewQuery; + setQ: React.Dispatch>; + pages: number; +}> = ({ q, setQ, pages }) => { + const page = Math.min(q.page, pages - 1); + const btn = (enabled: boolean) => + ({ + cursor: enabled ? "pointer" : "default", + opacity: enabled ? 1 : 0.4, + padding: "4px 8px", + borderRadius: 6, + }) as const; + return ( +
+
page > 0 && setQ((p) => ({ ...p, page: page - 1 }))} style={btn(page > 0)}> + ‹ Prev +
+ + {page + 1} / {pages} + +
page < pages - 1 && setQ((p) => ({ ...p, page: page + 1 }))} style={btn(page < pages - 1)}> + Next › +
+
+ ); +}; + +// Shared cell bits. +export const LedDot: React.FC<{ bot: UnifiedBot }> = ({ bot }) => ( + +); + +export const BatteryCell: React.FC<{ volts: number; width?: number; fill?: boolean }> = ({ + volts, + width = 54, + fill = false, +}) => { + const pct = Math.max(0, Math.min(100, ((volts - 2.0) / (4.2 - 2.0)) * 100)); + return ( +
+
+
+
+ {volts.toFixed(2)} V +
+ ); +}; + +export function useQueriedBots(bots: UnifiedBot[], q: ViewQuery) { + return useMemo(() => applyQuery(bots, q), [bots, q]); +} diff --git a/dotbot/console-web/tsconfig.json b/dotbot/console-web/tsconfig.json new file mode 100644 index 00000000..6bfa73af --- /dev/null +++ b/dotbot/console-web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/dotbot/console-web/vite.config.ts b/dotbot/console-web/vite.config.ts new file mode 100644 index 00000000..d53ff6ca --- /dev/null +++ b/dotbot/console-web/vite.config.ts @@ -0,0 +1,36 @@ +/// +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Dev proxies: the console talks same-origin, vite forwards to the two backends. +// /controller -> PyDotBot controller (REST + WS), default :8000 +// /swarmit -> swarmit status server (real or fake), default :8001 +// Override with CONTROLLER_TARGET / SWARMIT_TARGET env vars when the default +// ports are occupied by another controller/swarmit instance. +const controllerTarget = + process.env.CONTROLLER_TARGET ?? "http://localhost:8000"; +const swarmitTarget = process.env.SWARMIT_TARGET ?? "http://localhost:8001"; + +export default defineConfig({ + // Relative asset URLs: the production build is mounted at /console by the + // controller; the dev server stays at /. API paths are absolute either way. + base: "./", + plugins: [react()], + server: { + port: 5173, + proxy: { + "/controller": { + target: controllerTarget, + ws: true, + }, + "/swarmit": { + target: swarmitTarget, + rewrite: (path) => path.replace(/^\/swarmit/, ""), + }, + }, + }, + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/dotbot/controller.py b/dotbot/controller.py index 043862d2..e741bc76 100644 --- a/dotbot/controller.py +++ b/dotbot/controller.py @@ -39,6 +39,7 @@ SERIAL_BAUDRATE_DEFAULT, SERIAL_PORT_DEFAULT, SIMULATOR_INIT_STATE_DEFAULT, + SWARMIT_URL_DEFAULT, ) from dotbot.adapter import ( DotBotSimulatorAdapter, @@ -135,6 +136,7 @@ class ControllerSettings: log_output: str = os.path.join(os.getcwd(), "pydotbot.log") csv_data_output: Optional[str] = None simulator_init_state: str = SIMULATOR_INIT_STATE_DEFAULT + swarmit_url: str = SWARMIT_URL_DEFAULT def lh2_distance(last: DotBotLH2Position, new: DotBotLH2Position) -> float: diff --git a/dotbot/controller_app.py b/dotbot/controller_app.py index 117f8377..e3aee008 100644 --- a/dotbot/controller_app.py +++ b/dotbot/controller_app.py @@ -22,6 +22,7 @@ GATEWAY_ADDRESS_DEFAULT, MAP_SIZE_DEFAULT, SIMULATOR_INIT_STATE_DEFAULT, + SWARMIT_URL_DEFAULT, pydotbot_version, ) from dotbot.cli._cfg import from_config @@ -226,6 +227,14 @@ def _maybe_scaffold_sim_state(explicit_init_state): type=click.Path(dir_okay=False), help=f"Path to the simulator initial state .toml file. Defaults to '{SIMULATOR_INIT_STATE_DEFAULT}'.", ) +@click.option( + "--swarmit-url", + type=str, + help=( + "Base URL of the swarmit server the controller proxies /swarmit/* " + f"requests to (for the web console). Defaults to '{SWARMIT_URL_DEFAULT}'." + ), +) @click.pass_context def main( ctx, @@ -237,6 +246,7 @@ def main( map_size, background_map, simulator_init_state, + swarmit_url, headless, verbose, log_level, @@ -261,6 +271,7 @@ def main( # legacy `--config-path` fallback that follows. conn = from_config(ctx, "conn", "conn", "run") swarm_id = from_config(ctx, "swarm_id", "swarm_id", "run") + swarmit_url = from_config(ctx, "swarmit_url", "swarmit_url", "run.controller") conn = conn if conn is not None else file_data.get("conn") swarm_id = swarm_id if swarm_id is not None else file_data.get("swarm_id") @@ -296,6 +307,7 @@ def main( "map_size": map_size, "background_map": background_map, "simulator_init_state": simulator_init_state, + "swarmit_url": swarmit_url, "headless": True if headless else None, "verbose": verbose, "log_level": log_level, diff --git a/dotbot/server.py b/dotbot/server.py index 0e64a3bd..5884018d 100644 --- a/dotbot/server.py +++ b/dotbot/server.py @@ -14,13 +14,15 @@ FastAPI, HTTPException, Query, + Request, WebSocket, WebSocketDisconnect, ) from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import Response +from fastapi.responses import Response, StreamingResponse from fastapi.staticfiles import StaticFiles from pydantic import TypeAdapter, ValidationError +from starlette.background import BackgroundTask from starlette.middleware.base import BaseHTTPMiddleware from dotbot import pydotbot_version @@ -362,6 +364,55 @@ async def ws_dotbots(websocket: WebSocket): LOGGER.debug("WebSocket client disconnected") +# Timeouts for the swarmit proxy: fail fast when the server is down, but +# never time out reads - /events and /flash/stream are long-lived SSE. +SWARMIT_PROXY_TIMEOUT = httpx.Timeout(5.0, read=None) + + +@api.api_route( + path="/swarmit/{path:path}", + methods=["GET", "POST"], + include_in_schema=False, +) +async def swarmit_proxy(path: str, request: Request): + """Forward /swarmit/* to the configured swarmit server (same-origin for + the web console; the streaming body keeps SSE responses live).""" + base = api.controller.settings.swarmit_url.rstrip("/") + client = httpx.AsyncClient(timeout=SWARMIT_PROXY_TIMEOUT) + upstream_request = client.build_request( + method=request.method, + url=f"{base}/{path}", + params=request.query_params, + headers={ + k: v + for k, v in request.headers.items() + if k.lower() in ("content-type", "accept") + }, + content=await request.body(), + ) + try: + upstream = await client.send(upstream_request, stream=True) + except httpx.HTTPError as exc: + await client.aclose() + LOGGER.debug("swarmit server unreachable", url=f"{base}/{path}", error=str(exc)) + return Response(status_code=502, content=b"swarmit server unreachable") + + async def cleanup(): + await upstream.aclose() + await client.aclose() + + return StreamingResponse( + upstream.aiter_raw(), + status_code=upstream.status_code, + headers={ + k: v + for k, v in upstream.headers.items() + if k.lower() in ("content-type", "cache-control") + }, + background=BackgroundTask(cleanup), + ) + + # Mount static files after all routes are defined FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "frontend", "build") if os.path.isdir(FRONTEND_DIR): @@ -375,3 +426,15 @@ async def ws_dotbots(websocket: WebSocket): "frontend: cd dotbot/frontend && npm install && npm run build", FRONTEND_DIR, ) + +# The unified console (map-first PyDotBot + swarmit UI), served side by side +# with the classic frontend while it matures towards replacing it. +CONSOLE_DIR = os.path.join(os.path.dirname(__file__), "console-web", "dist") +if os.path.isdir(CONSOLE_DIR): + api.mount("/console", StaticFiles(directory=CONSOLE_DIR, html=True), name="console") +else: + LOGGER.info( + "Console build not found at %s; /console will be unavailable. " + "Build it with: cd dotbot/console-web && npm install && npm run build", + CONSOLE_DIR, + ) diff --git a/dotbot/tests/test_config.py b/dotbot/tests/test_config.py index b3e88df7..25dc123e 100644 --- a/dotbot/tests/test_config.py +++ b/dotbot/tests/test_config.py @@ -309,3 +309,50 @@ def test_resolve_bad_int_env_raises(): cfg.resolve( "http_port", section="run", environ={"DOTBOT_HTTP_PORT": "x"}, default=8000 ) + + +def test_resolve_nested_section_from_file(): + config = cfg.DotbotConfig( + run=cfg.RunSection( + controller=cfg.ControllerSection(swarmit_url="http://lab:9001") + ) + ) + got = cfg.resolve( + "swarmit_url", + section="run.controller", + config=config, + environ={}, + default="http://localhost:8001", + ) + assert got == "http://lab:9001" + + +def test_resolve_nested_section_env_name(): + got = cfg.resolve( + "swarmit_url", + section="run.controller", + environ={"DOTBOT_RUN_CONTROLLER_SWARMIT_URL": "http://env:9001"}, + default="http://localhost:8001", + ) + assert got == "http://env:9001" + + +def test_resolve_nested_section_shared_env_alias(): + got = cfg.resolve( + "swarmit_url", + section="run.controller", + environ={"DOTBOT_SWARMIT_URL": "http://env:9001"}, + default="http://localhost:8001", + ) + assert got == "http://env:9001" + + +def test_resolve_nested_section_missing_falls_to_default(): + got = cfg.resolve( + "swarmit_url", + section="run.controller", + config=cfg.DotbotConfig(), + environ={}, + default="http://localhost:8001", + ) + assert got == "http://localhost:8001" diff --git a/dotbot/tests/test_controller_app.py b/dotbot/tests/test_controller_app.py index 613a9209..5e2050f8 100644 --- a/dotbot/tests/test_controller_app.py +++ b/dotbot/tests/test_controller_app.py @@ -72,6 +72,51 @@ def test_run_controller_uses_selected_deployment(controller, _asyncio_run, tmp_p assert settings.adapter == "dotbot-simulator" +@pytest.mark.skipif(sys.platform == "win32", reason="Doesn't work on Windows") +@patch("dotbot.controller_app.asyncio.run") +@patch("dotbot.controller_app.Controller") +def test_run_controller_swarmit_url_flag(controller, _asyncio_run): + runner = CliRunner() + result = runner.invoke( + main, ["--conn", "simulator", "--swarmit-url", "http://lab:9001"] + ) + assert result.exit_code == 0, result.output + settings = controller.call_args.args[0] + assert settings.swarmit_url == "http://lab:9001" + + +@pytest.mark.skipif(sys.platform == "win32", reason="Doesn't work on Windows") +@patch("dotbot.controller_app.asyncio.run") +@patch("dotbot.controller_app.Controller") +def test_run_controller_swarmit_url_from_unified_config( + controller, _asyncio_run, tmp_path +): + """`[run.controller] swarmit_url` in dotbot.toml reaches the settings; + without it the built-in default applies.""" + from dotbot.cli.main import cli + + config_file = tmp_path / "dotbot.toml" + config_file.write_text( + """ +conn = "simulator" + +[run.controller] +swarmit_url = "http://lab:9001" +""" + ) + + runner = CliRunner() + result = runner.invoke(cli, ["-c", str(config_file), "run", "controller"]) + assert result.exit_code == 0, result.output + settings = controller.call_args.args[0] + assert settings.swarmit_url == "http://lab:9001" + + result = runner.invoke(main, ["--conn", "simulator"]) + assert result.exit_code == 0, result.output + settings = controller.call_args.args[0] + assert settings.swarmit_url == "http://localhost:8001" + + def test_main_without_conn_errors(): """No `--conn` → a clear error listing the connection forms.""" runner = CliRunner() diff --git a/dotbot/tests/test_server.py b/dotbot/tests/test_server.py index b7235681..b97b149e 100644 --- a/dotbot/tests/test_server.py +++ b/dotbot/tests/test_server.py @@ -579,6 +579,109 @@ def mock_async_client(*args, **kwargs): assert response.headers["X-Upstream"] == "mock" +class _MockByteStream(httpx.AsyncByteStream): + """Streamable body for MockTransport responses (a plain `content=` body + counts as already consumed, which the streaming proxy rejects).""" + + def __init__(self, chunks): + self._chunks = chunks + + async def __aiter__(self): + for chunk in self._chunks: + yield chunk + + +@pytest.mark.asyncio +async def test_swarmit_proxy_forwards(monkeypatch): + + async def mock_send(request: httpx.Request): + assert request.url == httpx.URL("http://swarmit-host:9001/status") + return httpx.Response( + status_code=200, + stream=_MockByteStream([b'{"response": {}}']), + headers={"Content-Type": "application/json"}, + ) + + transport = httpx.MockTransport(mock_send) + RealAsyncClient = httpx.AsyncClient + + def mock_async_client(*args, **kwargs): + kwargs.pop("transport", None) + return RealAsyncClient(transport=transport, **kwargs) + + import dotbot.server as server_module + + monkeypatch.setattr(server_module.httpx, "AsyncClient", mock_async_client) + api.controller.settings.swarmit_url = "http://swarmit-host:9001" + + client = TestClient(api) + response = client.get("/swarmit/status") + + assert response.status_code == 200 + assert response.content == b'{"response": {}}' + assert response.headers["Content-Type"] == "application/json" + + +@pytest.mark.asyncio +async def test_swarmit_proxy_forwards_post_body(monkeypatch): + + async def mock_send(request: httpx.Request): + assert request.url == httpx.URL("http://swarmit-host:9001/start") + assert request.method == "POST" + assert request.content == b'{"devices": []}' + assert request.headers["content-type"] == "application/json" + return httpx.Response( + status_code=200, stream=_MockByteStream([b'{"result": "ok"}']) + ) + + transport = httpx.MockTransport(mock_send) + RealAsyncClient = httpx.AsyncClient + + def mock_async_client(*args, **kwargs): + kwargs.pop("transport", None) + return RealAsyncClient(transport=transport, **kwargs) + + import dotbot.server as server_module + + monkeypatch.setattr(server_module.httpx, "AsyncClient", mock_async_client) + api.controller.settings.swarmit_url = "http://swarmit-host:9001" + + client = TestClient(api) + response = client.post( + "/swarmit/start", + content=b'{"devices": []}', + headers={"Content-Type": "application/json"}, + ) + + assert response.status_code == 200 + assert response.content == b'{"result": "ok"}' + + +@pytest.mark.asyncio +async def test_swarmit_proxy_unreachable(monkeypatch): + + async def mock_send_failed(*args, **kwargs): + raise httpx.ConnectError("connection failed") + + transport = httpx.MockTransport(mock_send_failed) + RealAsyncClient = httpx.AsyncClient + + def mock_async_client(*args, **kwargs): + kwargs.pop("transport", None) + return RealAsyncClient(transport=transport, **kwargs) + + import dotbot.server as server_module + + monkeypatch.setattr(server_module.httpx, "AsyncClient", mock_async_client) + api.controller.settings.swarmit_url = "http://swarmit-host:9001" + + client = TestClient(api) + response = client.get("/swarmit/status") + + assert response.status_code == 502 + assert b"swarmit server unreachable" in response.content + + @pytest.mark.asyncio async def test_reverse_proxy_middleware_connect_error(monkeypatch): diff --git a/pyproject.toml b/pyproject.toml index 20f96104..0c3c680c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,20 @@ build-backend = "hatchling.build" [tool.hatch.build] include = [ "dotbot/frontend/*", + "dotbot/console-web/*", "dotbot/*.toml", "*.py" ] exclude = [ "dotbot/frontend/node_modules", + "dotbot/console-web/node_modules", "utils/" ] +# The console build output is gitignored (dist/); artifacts overrides the +# VCS-ignore so the built app ships in the packages. +artifacts = [ + "dotbot/console-web/dist", +] [tool.hatch.metadata] allow-direct-references = true diff --git a/tox.ini b/tox.ini index 193a7eef..1ff7bdd5 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,9 @@ commands= allowlist_externals= /bin/bash /usr/bin/bash -commands = bash -exc "cd dotbot/frontend && npm run lint" +commands = + bash -exc "cd dotbot/frontend && npm run lint" + bash -exc "cd dotbot/console-web && npm run lint" # pin_code env removed: dotbot/pin_code_ui never existed in the tree — # silent dead config flagged in workspace AGENTS.md. diff --git a/utils/hooks/pydotbot_utils.py b/utils/hooks/pydotbot_utils.py index da126208..d0a97d59 100644 --- a/utils/hooks/pydotbot_utils.py +++ b/utils/hooks/pydotbot_utils.py @@ -23,3 +23,14 @@ def build_frontend(root): print("Building React frontend application...") subprocess.run(shlex.split(NPM_INSTALL_CMD), cwd=frontend_dir, check=True) subprocess.run(shlex.split(NPM_BUILD_CMD), cwd=frontend_dir, check=True) + + +def build_console(root): + """Builds the console web application.""" + console_dir = os.path.join(root, "dotbot", "console-web") + os.makedirs(os.path.join(console_dir, "dist"), exist_ok=True) + + if sys.platform != "win32": + print("Building console web application...") + subprocess.run(shlex.split(NPM_INSTALL_CMD), cwd=console_dir, check=True) + subprocess.run(shlex.split(NPM_BUILD_CMD), cwd=console_dir, check=True) diff --git a/utils/hooks/sdist.py b/utils/hooks/sdist.py index aea0a73a..b0ebfed5 100644 --- a/utils/hooks/sdist.py +++ b/utils/hooks/sdist.py @@ -13,7 +13,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface sys.path.append(os.path.dirname(__file__)) -from pydotbot_utils import build_frontend # noqa: E402 +from pydotbot_utils import build_console, build_frontend # noqa: E402 class CustomBuildHook(BuildHookInterface): @@ -24,3 +24,4 @@ def initialize(self, _, __): if os.getenv("SKIP_SDIST_HOOK") is not None: return build_frontend(self.root) + build_console(self.root)