From 90b32db39088b28d1773589360b2fe51a9a58587 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 17:35:28 +0000 Subject: [PATCH 01/19] =?UTF-8?q?feat(desktop):=20Milestone=206=20?= =?UTF-8?q?=E2=80=94=20Tauri=20desktop=20shell=20over=20the=20headless=20e?= =?UTF-8?q?ngine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the desktop delivery layer (tasks 25 & 26) without reimplementing any loop logic (Req 13.3). The engine is reused through a thin Node HTTP/SSE server compiled to a single binary and run as a Tauri sidecar. Engine server (src/server/): - createServer wraps runGoal + buildTrace: POST /api/runs, SSE /api/runs/:id/stream (live transitions, attempts, outcomes, runner calls, log lines), GET /api/sessions, GET /api/sessions/:id/trace, /api/health. - RunHub: ordered in-memory pub/sub with replay buffer + Last-Event-ID resume. - tapStoreEvents bridges the store's event stream to live subscribers. - Optional static serving so the UI also runs in a plain browser. - 7 new tests; full suite 132 passing, tsc clean. Sidecar build: - scripts/build-sidecar.mjs compiles src/server/index.ts via `bun --compile` into desktop/src-tauri/binaries/loopwright-engine-. - npm scripts: `serve`, `build:sidecar`. Desktop app (desktop/): - Vite + vanilla TS frontend: Start / Monitor (live SSE) / Results (trace) / Sessions / Secrets views. Resolves the engine URL same-origin in a browser or via the `engine_url` Tauri command in the app. - Tauri v2 shell (src-tauri): spawns the engine sidecar, discovers its port from the readiness line, and stores runner API keys in the OS keychain (keyring), injecting them into the sidecar env so `apiKeyEnv` resolves with no plaintext on disk. Also syncs tasks.md to reality (M2–M5 were complete on main; the prior "current position" note was stale). Co-authored-by: Inzimam Ul Haq <27832433+inhaq@users.noreply.github.com> --- .gitignore | 7 + .kiro/specs/loop-engine/tasks.md | 74 +- desktop/index.html | 23 + desktop/package-lock.json | 1233 +++++ desktop/package.json | 21 + desktop/src-tauri/Cargo.lock | 4880 +++++++++++++++++++ desktop/src-tauri/Cargo.toml | 24 + desktop/src-tauri/build.rs | 3 + desktop/src-tauri/capabilities/default.json | 19 + desktop/src-tauri/icons/128x128.png | Bin 0 -> 401 bytes desktop/src-tauri/icons/32x32.png | Bin 0 -> 114 bytes desktop/src-tauri/icons/icon.png | Bin 0 -> 2210 bytes desktop/src-tauri/src/engine.rs | 146 + desktop/src-tauri/src/lib.rs | 65 + desktop/src-tauri/src/main.rs | 6 + desktop/src-tauri/src/secrets.rs | 78 + desktop/src-tauri/tauri.conf.json | 35 + desktop/src/api.ts | 124 + desktop/src/main.ts | 434 ++ desktop/src/styles.css | 150 + desktop/src/types.ts | 70 + desktop/src/vite-env.d.ts | 1 + desktop/tsconfig.json | 19 + desktop/vite.config.ts | 23 + package.json | 2 + scripts/build-sidecar.mjs | 92 + src/server/hub.ts | 112 + src/server/index.ts | 52 + src/server/server.ts | 330 ++ src/server/store-tap.ts | 38 + test/server.test.ts | 261 + 31 files changed, 8292 insertions(+), 30 deletions(-) create mode 100644 desktop/index.html create mode 100644 desktop/package-lock.json create mode 100644 desktop/package.json create mode 100644 desktop/src-tauri/Cargo.lock create mode 100644 desktop/src-tauri/Cargo.toml create mode 100644 desktop/src-tauri/build.rs create mode 100644 desktop/src-tauri/capabilities/default.json create mode 100644 desktop/src-tauri/icons/128x128.png create mode 100644 desktop/src-tauri/icons/32x32.png create mode 100644 desktop/src-tauri/icons/icon.png create mode 100644 desktop/src-tauri/src/engine.rs create mode 100644 desktop/src-tauri/src/lib.rs create mode 100644 desktop/src-tauri/src/main.rs create mode 100644 desktop/src-tauri/src/secrets.rs create mode 100644 desktop/src-tauri/tauri.conf.json create mode 100644 desktop/src/api.ts create mode 100644 desktop/src/main.ts create mode 100644 desktop/src/styles.css create mode 100644 desktop/src/types.ts create mode 100644 desktop/src/vite-env.d.ts create mode 100644 desktop/tsconfig.json create mode 100644 desktop/vite.config.ts create mode 100644 scripts/build-sidecar.mjs create mode 100644 src/server/hub.ts create mode 100644 src/server/index.ts create mode 100644 src/server/server.ts create mode 100644 src/server/store-tap.ts create mode 100644 test/server.test.ts diff --git a/.gitignore b/.gitignore index efd29f8..a3480ea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,10 @@ dist/ *.log .DS_Store sessions.db + +# Desktop app (Tauri) build artifacts +desktop/src-tauri/binaries/ +desktop/src-tauri/target/ +desktop/src-tauri/gen/ +desktop/dist/ +desktop/node_modules/ diff --git a/.kiro/specs/loop-engine/tasks.md b/.kiro/specs/loop-engine/tasks.md index 39e793c..3941009 100644 --- a/.kiro/specs/loop-engine/tasks.md +++ b/.kiro/specs/loop-engine/tasks.md @@ -25,56 +25,70 @@ Each task references the requirement(s) it satisfies. _(Req 8)_ - [x] 11. Test suites: units, behavioral loop scenarios, real-subprocess gate -## Milestone 2 — Runners (in progress) +## Milestone 2 — Runners (done) -- [~] 12. Generic `CliRunner`: profile-driven subprocess backend (argv +- [x] 12. Generic `CliRunner`: profile-driven subprocess backend (argv templating, stdin/arg prompt, env passthrough, output modes, quota detection, timeout, bounded capture, strict option validation) _(Req 8)_ -- [~] 13. Role-binding layer: turn a runner + role prompt templates into a +- [x] 13. Role-binding layer: turn a runner + role prompt templates into a working `Actor` and `Critic` _(Req 8)_ - - [~] 13.1 Actor prompts: draft plan, build task, fix from feedback - - [~] 13.2 Critic prompts: plan review and task review producing rubric JSON - - [~] 13.3 Wire role bindings through configuration -- [~] 14. `HttpRunner` for OpenAI-compatible endpoints (base URL + key + model) + - [x] 13.1 Actor prompts: draft plan, build task, fix from feedback + - [x] 13.2 Critic prompts: plan review and task review producing rubric JSON + - [x] 13.3 Wire role bindings through configuration +- [x] 14. `HttpRunner` for OpenAI-compatible endpoints (base URL + key + model) behind the same interface _(Req 8)_ -- [~] 15. End-to-end run against real runners on a sample goal _(Req 1, 2)_ +- [x] 15. End-to-end run against real runners on a sample goal _(Req 1, 2)_ -## Milestone 3 — Persistence and resilience +## Milestone 3 — Persistence and resilience (done) -- [~] 16. Local store for sessions, tasks, attempts, transitions, outcomes +- [x] 16. Local store for sessions, tasks, attempts, transitions, outcomes _(Req 10, 11)_ -- [~] 17. Checkpoint on each transition; resume a run after interruption +- [x] 17. Checkpoint on each transition; resume a run after interruption _(Req 10)_ -- [~] 18. Stuck-detection watchdog (no-progress threshold) feeding the loop +- [x] 18. Stuck-detection watchdog (no-progress threshold) feeding the loop _(Req 2, 5)_ -## Milestone 4 — Parallel execution +## Milestone 4 — Parallel execution (done) -- [~] 19. Dependency-graph scheduler honoring the parallelism limit _(Req 9)_ -- [~] 20. Isolated workspaces (git worktrees) per concurrent task _(Req 9)_ -- [~] 21. Integrator: merge completed work, run full verification, surface +- [x] 19. Dependency-graph scheduler honoring the parallelism limit _(Req 9)_ +- [x] 20. Isolated workspaces (git worktrees) per concurrent task _(Req 9)_ +- [x] 21. Integrator: merge completed work, run full verification, surface conflicts _(Req 9)_ -## Milestone 5 — Observability +## Milestone 5 — Observability (done) -- [~] 22. Structured event log for transitions and runner calls _(Req 11)_ -- [~] 23. Usage/cost ledger per role and per run _(Req 11)_ -- [~] 24. Session trace inspection _(Req 11)_ +- [x] 22. Structured event log for transitions and runner calls _(Req 11)_ +- [x] 23. Usage/cost ledger per role and per run _(Req 11)_ +- [x] 24. Session trace inspection _(Req 11)_ -## Milestone 6 — Desktop delivery +## Milestone 6 — Desktop delivery (in progress) -- [ ] 25. Desktop shell over the headless engine: start a run, monitor live +- [~] 25. Desktop shell over the headless engine: start a run, monitor live progress, review results _(Req 13)_ -- [ ] 26. Packaging and secure secret storage _(Req 13)_ + - [~] 25.1 Engine HTTP/SSE server (`src/server/`) wrapping `runGoal` + + `buildTrace`; streams live transitions, attempts, outcomes, and runner + calls over SSE without adding orchestration policy + - [~] 25.2 Tauri shell that runs the engine as a bundled sidecar process and + loads the web frontend (Start / Monitor / Results views) +- [~] 26. Packaging and secure secret storage _(Req 13)_ + - [~] 26.1 OS-keychain secret storage (Tauri commands backed by `keyring`) + - [~] 26.2 Inject stored API keys into the sidecar env so runner `apiKeyEnv` + bindings resolve without plaintext on disk --- ### Current position -Milestone 1 is complete and merged. Milestone 2 is underway: the generic -command-line runner is implemented and in review, and the **role-binding layer -(13)** is now in review — `RunnerActor`/`RunnerCritic` pair any runner with -prompt templates and are wired through configuration (`createRoles`), so a -profile + prompts becomes a usable backend driving the Milestone 1 loop. The -next active task is the **`HttpRunner` (14)** for OpenAI-compatible endpoints, -followed by an **end-to-end run against real runners (15)**. +Milestones 1–5 are complete and merged on `main` (engine core, runners + +role bindings, persistence/resume, the parallel scheduler with git-worktree +isolation and the integrator, and the observability/usage/trace layer); the +suite is green (125 tests, `tsc --noEmit` clean). + +Active work is **Milestone 6 — Desktop delivery**. Rather than reimplement loop +logic (forbidden by Req 13), the desktop app reuses the headless engine through +a thin Node **HTTP/SSE server** (`src/server/`) that wraps `runGoal` and +`buildTrace`. That server is compiled to a single binary and shipped as a +**Tauri sidecar**; the Tauri shell hosts the web frontend (start a run, monitor +live progress over SSE, review the final trace) and stores runner API keys in +the OS keychain, injecting them into the sidecar's environment so no secret is +written to disk in plaintext. diff --git a/desktop/index.html b/desktop/index.html new file mode 100644 index 0000000..1ae7c95 --- /dev/null +++ b/desktop/index.html @@ -0,0 +1,23 @@ + + + + + + Loopwright + + +
+
+
Loopwright
+ +
connecting…
+
+
+
+ + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..305e8e0 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,1233 @@ +{ + "name": "loopwright-desktop", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "loopwright-desktop", + "version": "0.0.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "typescript": "^5.6.3", + "vite": "^5.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", + "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.2", + "@tauri-apps/cli-darwin-x64": "2.11.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", + "@tauri-apps/cli-linux-arm64-musl": "2.11.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-musl": "2.11.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", + "@tauri-apps/cli-win32-x64-msvc": "2.11.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", + "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", + "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", + "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", + "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", + "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", + "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", + "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", + "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", + "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", + "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", + "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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/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/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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "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.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "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/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/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 + } + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..646ea0b --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,21 @@ +{ + "name": "loopwright-desktop", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Tauri desktop shell for the Loopwright actor-critic engine", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "typescript": "^5.6.3", + "vite": "^5.4.0" + } +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..813ff29 --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4880 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "loopwright-desktop" +version = "0.0.0" +dependencies = [ + "keyring", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-shell", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..7943a03 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "loopwright-desktop" +version = "0.0.0" +description = "Desktop shell for the Loopwright actor-critic engine" +edition = "2021" +rust-version = "1.77" + +[lib] +name = "loopwright_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } + +[features] +# default to a production build without devtools +custom-protocol = ["tauri/custom-protocol"] diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..48e887c --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,19 @@ +{ + "identifier": "default", + "description": "Permissions for the main window: core defaults plus permission to spawn the bundled engine sidecar.", + "windows": ["main"], + "permissions": [ + "core:default", + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "binaries/loopwright-engine", + "sidecar": true, + "args": false + } + ] + }, + "shell:allow-kill" + ] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..40b01cd268614adcfd2286fd329f10be20894fb3 GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV2to|aSW-L^Y)UVV1og}fdj7+ zU-pUWTrJjSU@Vv}Uw3{&;o0iTru+CFs59GS3nw*LFdvarILA=P)?w^0hw%}QK>DZ< iYDl~@W$m3P!_WG(C*wi_!97w45BR4n}abxZpUjsUZt$ABcr< WO>, + child: Mutex>, +} + +impl EngineManager { + pub fn new(app: AppHandle) -> Self { + Self { + app, + url: Mutex::new(None), + child: Mutex::new(None), + } + } + + /// The engine base URL once started (e.g. "http://127.0.0.1:53187"). + pub fn url(&self) -> Option { + self.url.lock().unwrap().clone() + } + + /// Spawns the sidecar and blocks until it reports its listening port. + pub fn start(&self) -> Result { + let data_dir = self.app.path().app_data_dir().map_err(|e| e.to_string())?; + std::fs::create_dir_all(&data_dir).map_err(|e| e.to_string())?; + let db_path = data_dir.join("sessions.json"); + + let mut envs: HashMap = HashMap::new(); + envs.insert("LOOPWRIGHT_HOST".into(), "127.0.0.1".into()); + envs.insert("LOOPWRIGHT_PORT".into(), "0".into()); // ephemeral + envs.insert( + "LOOPWRIGHT_DB_PATH".into(), + db_path.to_string_lossy().to_string(), + ); + // Inject stored API keys so runner `apiKeyEnv` bindings resolve. + for (k, v) in secrets::all(&self.app)? { + envs.insert(k, v); + } + + let (mut rx, child) = self + .app + .shell() + .sidecar(SIDECAR) + .map_err(|e| e.to_string())? + .envs(envs) + .spawn() + .map_err(|e| e.to_string())?; + + let url = tauri::async_runtime::block_on(read_listening_url(&mut rx))?; + + // Keep draining events so a full stdout/stderr pipe can't stall the + // engine during a long run. + tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); + + *self.url.lock().unwrap() = Some(url.clone()); + *self.child.lock().unwrap() = Some(child); + Ok(url) + } + + /// Kills the running sidecar (if any) and starts a fresh one. Used after + /// secrets change so the new values are picked up. + pub fn restart(&self) -> Result { + if let Some(child) = self.child.lock().unwrap().take() { + let _ = child.kill(); + } + *self.url.lock().unwrap() = None; + self.start() + } +} + +/// Reads sidecar output until the readiness line is seen, then returns the URL. +async fn read_listening_url(rx: &mut Receiver) -> Result { + let deadline = Instant::now() + STARTUP_TIMEOUT; + let mut buf = String::new(); + + loop { + let remaining = deadline + .checked_duration_since(Instant::now()) + .ok_or_else(|| "engine did not report a listening port in time".to_string())?; + + let event = match tokio::time::timeout(remaining, rx.recv()).await { + Ok(ev) => ev, + Err(_) => return Err("timed out waiting for engine to start".to_string()), + }; + + match event { + Some(CommandEvent::Stdout(bytes)) | Some(CommandEvent::Stderr(bytes)) => { + buf.push_str(&String::from_utf8_lossy(&bytes)); + if let Some(url) = parse_listening(&buf) { + return Ok(url); + } + } + Some(CommandEvent::Error(e)) => return Err(format!("engine error: {e}")), + Some(CommandEvent::Terminated(_)) => { + return Err("engine exited before reporting a port".to_string()) + } + None => return Err("engine output closed before reporting a port".to_string()), + _ => {} + } + } +} + +/// Scans accumulated output for the `{"loopwright":"listening",...}` line. +fn parse_listening(buf: &str) -> Option { + for line in buf.lines() { + let line = line.trim(); + if !line.starts_with('{') { + continue; + } + if let Ok(v) = serde_json::from_str::(line) { + if v.get("loopwright").and_then(|x| x.as_str()) == Some("listening") { + let host = v.get("host").and_then(|x| x.as_str()).unwrap_or("127.0.0.1"); + let port = v.get("port").and_then(|x| x.as_u64())?; + return Some(format!("http://{host}:{port}")); + } + } + } + None +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..bce68c7 --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,65 @@ +//! Loopwright desktop shell (Milestone 6). +//! +//! A thin Tauri wrapper around the headless engine: it runs the engine as a +//! sidecar process and hosts the web frontend. All loop orchestration stays in +//! the engine (Req 13.3); this layer only manages the sidecar's lifecycle and +//! the OS-keychain secret storage the engine consumes via its environment. + +mod engine; +mod secrets; + +use engine::EngineManager; +use tauri::Manager; + +/// Returns the running engine's base URL for the frontend to call. +#[tauri::command] +fn engine_url(state: tauri::State) -> Result { + state.url().ok_or_else(|| "engine not started".to_string()) +} + +/// Restarts the engine sidecar (e.g. after secrets change) and returns its URL. +#[tauri::command] +fn restart_engine(state: tauri::State) -> Result { + state.restart() +} + +#[tauri::command] +fn set_secret(app: tauri::AppHandle, key: String, value: String) -> Result<(), String> { + secrets::set(&app, &key, &value) +} + +#[tauri::command] +fn delete_secret(app: tauri::AppHandle, key: String) -> Result<(), String> { + secrets::delete(&app, &key) +} + +#[tauri::command] +fn list_secret_keys(app: tauri::AppHandle) -> Result, String> { + secrets::list_keys(&app) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .setup(|app| { + // Start the engine sidecar up front so the UI has an endpoint as + // soon as it loads. A startup failure aborts the app with a clear + // error rather than leaving a dead UI. + let manager = EngineManager::new(app.handle().clone()); + manager + .start() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + app.manage(manager); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + engine_url, + restart_engine, + set_secret, + delete_secret, + list_secret_keys + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..99245b5 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an extra console window on Windows in release builds. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + loopwright_desktop_lib::run() +} diff --git a/desktop/src-tauri/src/secrets.rs b/desktop/src-tauri/src/secrets.rs new file mode 100644 index 0000000..8318715 --- /dev/null +++ b/desktop/src-tauri/src/secrets.rs @@ -0,0 +1,78 @@ +//! Secure secret storage (Task 26). +//! +//! Runner API keys are kept in the OS keychain via the `keyring` crate, never +//! written to disk in plaintext. Because keychains do not offer portable +//! enumeration, the *names* of stored keys are tracked in a small index file in +//! the app config directory; the secret values themselves only ever live in the +//! keychain. Keys are surfaced to the engine by injecting them into the +//! sidecar's environment (see `engine.rs`), so a runner profile's `apiKeyEnv` +//! reference resolves at run time. + +use std::fs; +use std::path::PathBuf; + +use keyring::Entry; +use tauri::{AppHandle, Manager}; + +/// Keychain service namespace for all Loopwright secrets. +const SERVICE: &str = "dev.loopwright.desktop"; + +fn index_path(app: &AppHandle) -> Result { + let dir = app.path().app_config_dir().map_err(|e| e.to_string())?; + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + Ok(dir.join("secrets-index.json")) +} + +fn read_index(app: &AppHandle) -> Result, String> { + match fs::read_to_string(index_path(app)?) { + Ok(s) => serde_json::from_str(&s).map_err(|e| e.to_string()), + Err(_) => Ok(Vec::new()), + } +} + +fn write_index(app: &AppHandle, keys: &[String]) -> Result<(), String> { + let json = serde_json::to_string(keys).map_err(|e| e.to_string())?; + fs::write(index_path(app)?, json).map_err(|e| e.to_string()) +} + +/// Names of every stored secret (no values). +pub fn list_keys(app: &AppHandle) -> Result, String> { + read_index(app) +} + +/// Stores (or replaces) a secret in the keychain and records its name. +pub fn set(app: &AppHandle, key: &str, value: &str) -> Result<(), String> { + let entry = Entry::new(SERVICE, key).map_err(|e| e.to_string())?; + entry.set_password(value).map_err(|e| e.to_string())?; + let mut keys = read_index(app)?; + if !keys.iter().any(|k| k == key) { + keys.push(key.to_string()); + write_index(app, &keys)?; + } + Ok(()) +} + +/// Removes a secret from the keychain and the index. Idempotent. +pub fn delete(app: &AppHandle, key: &str) -> Result<(), String> { + if let Ok(entry) = Entry::new(SERVICE, key) { + // Ignore "no such entry" so repeated deletes don't error. + let _ = entry.delete_credential(); + } + let mut keys = read_index(app)?; + keys.retain(|k| k != key); + write_index(app, &keys) +} + +/// Resolves all stored secrets to (name, value) pairs for env injection. +/// Names whose keychain value has gone missing are silently skipped. +pub fn all(app: &AppHandle) -> Result, String> { + let mut out = Vec::new(); + for key in read_index(app)? { + if let Ok(entry) = Entry::new(SERVICE, &key) { + if let Ok(value) = entry.get_password() { + out.push((key, value)); + } + } + } + Ok(out) +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..a48bd63 --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "productName": "Loopwright", + "version": "0.0.0", + "identifier": "dev.loopwright.desktop", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1420", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build" + }, + "app": { + "windows": [ + { + "title": "Loopwright", + "width": 1000, + "height": 720, + "minWidth": 720, + "minHeight": 520 + } + ], + "security": { + "csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:* http://localhost:* ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'" + } + }, + "bundle": { + "active": true, + "targets": "all", + "externalBin": ["binaries/loopwright-engine"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/icon.png" + ] + } +} diff --git a/desktop/src/api.ts b/desktop/src/api.ts new file mode 100644 index 0000000..a969870 --- /dev/null +++ b/desktop/src/api.ts @@ -0,0 +1,124 @@ +import type { RunMessage, SessionRecord, TraceResponse } from "./types.js"; + +/** + * Client for the engine server. The app runs in two contexts: + * + * - Inside Tauri: the engine runs as a sidecar on a loopback port the Rust + * side chose; we ask it for the URL (and the secret-storage commands are + * available). + * - In a plain browser (served by `npm run serve`): the API is same-origin. + * + * Everything below is written so the browser path needs no Tauri at all. + */ + +export function isTauri(): boolean { + return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; +} + +let cachedBase: string | undefined; + +async function tauriInvoke(cmd: string, args?: Record): Promise { + const { invoke } = await import("@tauri-apps/api/core"); + return invoke(cmd, args); +} + +/** Resolves the engine base URL ("" means same-origin). */ +export async function apiBase(): Promise { + if (cachedBase !== undefined) return cachedBase; + if (isTauri()) { + try { + cachedBase = await tauriInvoke("engine_url"); + } catch { + cachedBase = "http://127.0.0.1:4317"; // fallback to a conventional port + } + } else { + cachedBase = ""; + } + return cachedBase; +} + +/** Re-spawns the engine sidecar (Tauri only) so newly stored secrets apply. */ +export async function restartEngine(): Promise { + if (!isTauri()) return; + cachedBase = await tauriInvoke("restart_engine"); +} + +async function getJson(path: string): Promise { + const res = await fetch((await apiBase()) + path); + if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); + return res.json() as Promise; +} + +export interface StartRunBody { + goal: string; + env?: Record; + sessionId?: string; + resume?: boolean; +} + +export async function startRun(body: StartRunBody): Promise { + const res = await fetch((await apiBase()) + "/api/runs", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); + const { sessionId } = (await res.json()) as { sessionId: string }; + return sessionId; +} + +export async function listSessions(): Promise { + const { sessions } = await getJson<{ sessions: SessionRecord[] }>("/api/sessions"); + return sessions; +} + +export async function getTrace(sessionId: string): Promise { + return getJson(`/api/sessions/${encodeURIComponent(sessionId)}/trace`); +} + +export async function health(): Promise { + try { + const { ok } = await getJson<{ ok: boolean }>("/api/health"); + return ok === true; + } catch { + return false; + } +} + +/** + * Subscribes to a run's live event stream. Returns a function that closes it. + * Uses the native EventSource, which transparently reconnects and replays via + * Last-Event-ID; the server's hub honours that header to avoid duplicates. + */ +export async function openStream( + sessionId: string, + onMessage: (msg: RunMessage) => void, + onError?: (err: Event) => void, +): Promise<() => void> { + const url = (await apiBase()) + `/api/runs/${encodeURIComponent(sessionId)}/stream`; + const es = new EventSource(url); + const types: RunMessage["type"][] = ["status", "transition", "attempt", "outcome", "event", "log"]; + for (const type of types) { + es.addEventListener(type, (ev) => { + const me = ev as MessageEvent; + onMessage({ id: Number(me.lastEventId), type, data: JSON.parse(me.data) }); + }); + } + if (onError) es.onerror = onError; + return () => es.close(); +} + +// --- Secret storage (Tauri only; OS keychain) ----------------------------- + +export async function listSecretKeys(): Promise { + if (!isTauri()) return []; + return tauriInvoke("list_secret_keys"); +} + +export async function setSecret(key: string, value: string): Promise { + await tauriInvoke("set_secret", { key, value }); +} + +export async function deleteSecret(key: string): Promise { + await tauriInvoke("delete_secret", { key }); +} diff --git a/desktop/src/main.ts b/desktop/src/main.ts new file mode 100644 index 0000000..338220e --- /dev/null +++ b/desktop/src/main.ts @@ -0,0 +1,434 @@ +import "./styles.css"; +import { + apiBase, + deleteSecret, + getTrace, + health, + isTauri, + listSecretKeys, + listSessions, + openStream, + restartEngine, + setSecret, + startRun, +} from "./api.js"; +import type { RunMessage, SessionRecord, TraceResponse } from "./types.js"; + +const view = document.getElementById("view") as HTMLElement; +const engineStatus = document.getElementById("engine-status") as HTMLElement; + +// --- tiny DOM helpers ------------------------------------------------------- + +function h( + tag: K, + attrs: Record = {}, + children: Array = [], +): HTMLElementTagNameMap[K] { + const node = document.createElement(tag); + for (const [k, v] of Object.entries(attrs)) { + if (k === "class") node.className = v; + else node.setAttribute(k, v); + } + for (const c of children) node.append(c); + return node; +} + +function badge(state: string): HTMLElement { + return h("span", { class: `badge state-${state}` }, [state]); +} + +// --- navigation ------------------------------------------------------------- + +type Nav = "start" | "sessions" | "secrets"; +let teardown: (() => void) | null = null; + +function navigate(nav: Nav, arg?: string): void { + if (teardown) { + teardown(); + teardown = null; + } + document.querySelectorAll("nav button").forEach((b) => { + b.classList.toggle("active", (b as HTMLElement).dataset.nav === nav); + }); + view.innerHTML = ""; + if (nav === "start") renderStart(); + else if (nav === "sessions") renderSessions(); + else if (nav === "secrets") renderSecrets(); + void arg; +} + +document.querySelectorAll("nav button").forEach((b) => { + b.addEventListener("click", () => navigate((b as HTMLElement).dataset.nav as Nav)); +}); + +// --- Start view ------------------------------------------------------------- + +const SAMPLE_RUNNERS = JSON.stringify( + [ + { + id: "primary", + kind: "http", + model: "gpt-4o-mini", + options: { baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY" }, + }, + ], + null, + 2, +); + +function renderStart(): void { + const form = h("form", { class: "card form" }); + form.innerHTML = ` +

Start a run

+ + +
+ + +
+
+ + + +
+
+ + +
+ `; + (form.querySelector("[name=runners]") as HTMLTextAreaElement).value = SAMPLE_RUNNERS; + + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const hint = form.querySelector("#start-hint") as HTMLElement; + const data = new FormData(form); + const goal = String(data.get("goal") ?? "").trim(); + if (!goal) return; + + const env: Record = { + LOOPWRIGHT_RUNNERS: String(data.get("runners") ?? "").trim(), + LOOPWRIGHT_ACTOR_RUNNER: String(data.get("actor") ?? "").trim(), + LOOPWRIGHT_CRITIC_RUNNER: String(data.get("critic") ?? "").trim(), + LOOPWRIGHT_MAX_PARALLEL: String(data.get("maxParallel") ?? "2"), + LOOPWRIGHT_USE_WORKTREES: data.get("worktrees") ? "true" : "false", + LOOPWRIGHT_MECHANICAL_GATE: data.get("gate") ? "true" : "false", + }; + + try { + // Validate JSON early so the user gets a clear message, not a 400. + if (env.LOOPWRIGHT_RUNNERS) JSON.parse(env.LOOPWRIGHT_RUNNERS); + } catch (err) { + hint.textContent = `Runner profiles must be valid JSON: ${(err as Error).message}`; + hint.classList.add("error"); + return; + } + + hint.textContent = "Starting…"; + hint.classList.remove("error"); + try { + const sessionId = await startRun({ goal, env }); + renderMonitor(sessionId, goal); + } catch (err) { + hint.textContent = `Failed to start: ${(err as Error).message}`; + hint.classList.add("error"); + } + }); + + view.append(form); +} + +// --- Monitor view ----------------------------------------------------------- + +function renderMonitor(sessionId: string, goal: string): void { + document.querySelectorAll("nav button").forEach((b) => b.classList.remove("active")); + view.innerHTML = ""; + + const header = h("div", { class: "card" }); + header.innerHTML = ` +

Live run

+
${escapeHtml(goal)}
+
session ${escapeHtml(sessionId)}
+
running…
+
+ `; + + const usage = h("div", { class: "card usage" }); + usage.innerHTML = `

Usage

no runner calls yet
`; + + const tasksCard = h("div", { class: "card" }, [h("h3", {}, ["Tasks"])]); + const tasksTable = h("table", { class: "tasks" }); + tasksTable.innerHTML = `TaskStateDetail`; + tasksCard.append(tasksTable); + + const logCard = h("div", { class: "card" }, [h("h3", {}, ["Engine log"])]); + const log = h("pre", { class: "log", id: "log" }); + logCard.append(log); + + view.append(header, usage, tasksCard, logCard); + + const taskRows = new Map(); + let actorCalls = 0; + let criticCalls = 0; + let totalTokens = 0; + + function taskRow(taskId: string): HTMLElement { + let row = taskRows.get(taskId); + if (!row) { + row = h("tr"); + row.innerHTML = `${escapeHtml(taskId)}`; + (document.getElementById("task-rows") as HTMLElement).append(row); + taskRows.set(taskId, row); + } + return row; + } + + function appendLog(line: string): void { + log.textContent += line + "\n"; + log.scrollTop = log.scrollHeight; + } + + function onMessage(msg: RunMessage): void { + if (msg.type === "log") { + appendLog(msg.data.line); + } else if (msg.type === "transition") { + const row = taskRow(msg.data.taskId); + const st = row.querySelector(".st") as HTMLElement; + st.innerHTML = ""; + st.append(badge(msg.data.to)); + (row.querySelector(".dt") as HTMLElement).textContent = msg.data.reason ?? ""; + } else if (msg.type === "outcome") { + const row = taskRow(msg.data.taskId); + const st = row.querySelector(".st") as HTMLElement; + st.innerHTML = ""; + st.append(badge(msg.data.finalState)); + if (msg.data.degradedReason) { + (row.querySelector(".dt") as HTMLElement).textContent = msg.data.degradedReason; + } + } else if (msg.type === "event") { + const ev = msg.data; + if (ev.type === "runner_call") { + if (ev.data.role === "actor") actorCalls++; + else if (ev.data.role === "critic") criticCalls++; + totalTokens += Number(ev.data?.usage?.totalTokens ?? 0); + (document.getElementById("usage-body") as HTMLElement).textContent = + `actor ${actorCalls} calls · critic ${criticCalls} calls · ${totalTokens} tokens`; + } else if (ev.type === "plan_reviewed") { + (document.getElementById("plan") as HTMLElement).textContent = + `plan: approved=${ev.data.approved} · revisions=${ev.data.revisions} · open items=${ev.data.openItems}`; + } + } else if (msg.type === "status") { + const phase = document.getElementById("phase") as HTMLElement; + if (msg.data.phase === "done") { + phase.className = "phase done"; + phase.textContent = "completed"; + const btn = h("button", { class: "primary" }, ["View results"]); + btn.addEventListener("click", () => renderResults(sessionId)); + phase.append(" ", btn); + } else if (msg.data.phase === "error") { + phase.className = "phase error"; + phase.textContent = `error: ${msg.data.error}`; + } + } + } + + void openStream(sessionId, onMessage, () => { + /* EventSource auto-reconnects with Last-Event-ID; nothing to do here */ + }).then((close) => { + teardown = close; + }); +} + +// --- Results view ----------------------------------------------------------- + +async function renderResults(sessionId: string): Promise { + document.querySelectorAll("nav button").forEach((b) => b.classList.remove("active")); + view.innerHTML = ""; + view.append(h("div", { class: "card" }, ["Loading trace…"])); + + let resp: TraceResponse; + try { + resp = await getTrace(sessionId); + } catch (err) { + view.innerHTML = ""; + view.append(h("div", { class: "card error" }, [`Failed to load trace: ${(err as Error).message}`])); + return; + } + + const { trace } = resp; + view.innerHTML = ""; + + const summary = h("div", { class: "card" }); + const s = trace.session; + const byState = countStates(trace); + summary.innerHTML = ` +

Results

+
${escapeHtml(s?.goal ?? "")}
+
session ${escapeHtml(sessionId)} — ${escapeHtml(s?.status ?? "?")}
+
+ GREEN ${byState.GREEN} + UNVERIFIED ${byState.UNVERIFIED_BY_CRITIC} + NEEDS_HUMAN ${byState.NEEDS_HUMAN} +
+ `; + + const u = trace.usage; + const usage = h("div", { class: "card" }); + usage.innerHTML = ` +

Usage

+ + + + + +
callspromptcompletiontotalquota hits
actor${u.perRole.actor.calls}${u.perRole.actor.promptTokens}${u.perRole.actor.completionTokens}${u.perRole.actor.totalTokens}${u.perRole.actor.quotaHits}
critic${u.perRole.critic.calls}${u.perRole.critic.promptTokens}${u.perRole.critic.completionTokens}${u.perRole.critic.totalTokens}${u.perRole.critic.quotaHits}
total${u.total.calls}${u.total.promptTokens}${u.total.completionTokens}${u.total.totalTokens}${u.total.quotaHits}
+ `; + + const tasksCard = h("div", { class: "card" }, [h("h3", {}, ["Tasks"])]); + for (const t of trace.tasks) { + const block = h("div", { class: "task-block" }); + const head = h("div", { class: "task-head" }, [`${t.taskId} `, badge(t.state)]); + if (t.degradedReason) head.append(h("span", { class: "degraded" }, [` ${t.degradedReason}`])); + block.append(head); + const txs = trace.transitions.filter((x) => x.taskId === t.taskId); + if (txs.length) { + const ul = h("ul", { class: "tx" }); + for (const x of txs) ul.append(h("li", {}, [`${x.from} —(${x.event})→ ${x.to} ${x.reason}`])); + block.append(ul); + } + tasksCard.append(block); + } + + const raw = h("details", { class: "card" }); + raw.append(h("summary", {}, ["Raw trace (text)"]), h("pre", { class: "log" }, [resp.text])); + + view.append(summary, usage, tasksCard, raw); +} + +function countStates(trace: TraceResponse["trace"]): Record { + const counts: Record = { GREEN: 0, UNVERIFIED_BY_CRITIC: 0, NEEDS_HUMAN: 0 }; + for (const t of trace.tasks) counts[t.state] = (counts[t.state] ?? 0) + 1; + return counts; +} + +// --- Sessions view ---------------------------------------------------------- + +async function renderSessions(): Promise { + view.innerHTML = ""; + const card = h("div", { class: "card" }, [h("h2", {}, ["Sessions"])]); + view.append(card); + let sessions: SessionRecord[]; + try { + sessions = await listSessions(); + } catch (err) { + card.append(h("div", { class: "error" }, [`Failed to load: ${(err as Error).message}`])); + return; + } + if (!sessions.length) { + card.append(h("div", { class: "hint" }, ["No runs yet."])); + return; + } + const list = h("ul", { class: "session-list" }); + for (const s of sessions) { + const li = h("li", {}); + const btn = h("button", { class: "linkish" }, [ + h("span", { class: "s-goal" }, [s.goal]), + h("span", { class: "s-meta" }, [`${s.status} · ${new Date(s.createdAt).toLocaleString()}`]), + ]); + btn.addEventListener("click", () => renderResults(s.id)); + li.append(btn); + list.append(li); + } + card.append(list); +} + +// --- Secrets view (Tauri only) --------------------------------------------- + +async function renderSecrets(): Promise { + view.innerHTML = ""; + const card = h("div", { class: "card" }, [h("h2", {}, ["Secrets"])]); + view.append(card); + if (!isTauri()) { + card.append( + h("div", { class: "hint" }, [ + "Secure secret storage is only available in the desktop app. In a browser, provide API keys via the engine server's environment.", + ]), + ); + return; + } + + card.append( + h("p", { class: "hint" }, [ + "Stored in the OS keychain and injected into the engine as environment variables. Reference them from runner profiles via apiKeyEnv (e.g. OPENAI_API_KEY).", + ]), + ); + + const listEl = h("ul", { class: "secret-list" }); + card.append(listEl); + + async function refresh(): Promise { + listEl.innerHTML = ""; + const keys = await listSecretKeys(); + if (!keys.length) listEl.append(h("li", { class: "hint" }, ["No secrets stored."])); + for (const k of keys) { + const del = h("button", { class: "danger small" }, ["Delete"]); + del.addEventListener("click", async () => { + await deleteSecret(k); + await refresh(); + }); + listEl.append(h("li", {}, [h("code", {}, [k]), del])); + } + } + + const form = h("form", { class: "row secret-form" }); + form.innerHTML = ` + + + + `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const data = new FormData(form); + await setSecret(String(data.get("key")), String(data.get("value"))); + form.reset(); + await refresh(); + }); + card.append(form); + + const restart = h("button", {}, ["Restart engine to apply secret changes"]); + restart.addEventListener("click", async () => { + restart.textContent = "Restarting…"; + await restartEngine(); + restart.textContent = "Restart engine to apply secret changes"; + await checkEngine(); + }); + card.append(h("div", { class: "actions" }, [restart])); + + await refresh(); +} + +// --- engine status ---------------------------------------------------------- + +function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!); +} + +async function checkEngine(): Promise { + const ok = await health(); + engineStatus.textContent = ok ? "engine: connected" : "engine: offline"; + engineStatus.className = `engine-status ${ok ? "ok" : "down"}`; +} + +async function boot(): Promise { + if (isTauri()) (document.getElementById("nav-secrets") as HTMLElement).hidden = false; + await apiBase(); + await checkEngine(); + setInterval(checkEngine, 10_000); + navigate("start"); +} + +void boot(); diff --git a/desktop/src/styles.css b/desktop/src/styles.css new file mode 100644 index 0000000..9cc2915 --- /dev/null +++ b/desktop/src/styles.css @@ -0,0 +1,150 @@ +:root { + --bg: #0f1115; + --panel: #181b22; + --panel-2: #1f232c; + --border: #2a2f3a; + --text: #e6e9ef; + --muted: #9aa3b2; + --accent: #5b8cff; + --green: #3fb950; + --yellow: #d29922; + --red: #f85149; + --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; +} + +.topbar { + display: flex; + align-items: center; + gap: 16px; + padding: 10px 18px; + background: var(--panel); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} +.brand { font-weight: 700; letter-spacing: 0.3px; } +.topbar nav { display: flex; gap: 6px; } +.topbar nav button { + background: transparent; + border: 1px solid transparent; + color: var(--muted); + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; +} +.topbar nav button.active, +.topbar nav button:hover { color: var(--text); background: var(--panel-2); } + +.engine-status { margin-left: auto; font-size: 12px; color: var(--muted); } +.engine-status.ok { color: var(--green); } +.engine-status.down { color: var(--red); } + +main { max-width: 920px; margin: 0 auto; padding: 18px; display: flex; flex-direction: column; gap: 16px; } + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 10px; + padding: 16px 18px; +} +.card h2 { margin: 0 0 12px; font-size: 17px; } +.card h3 { margin: 0 0 10px; font-size: 14px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; } + +.form label { display: block; margin-bottom: 12px; font-size: 13px; color: var(--muted); } +.form .row { display: flex; gap: 14px; flex-wrap: wrap; } +.form .row label { flex: 1; min-width: 140px; } +label.check { display: flex; align-items: center; gap: 8px; color: var(--text); } +label.check input { width: auto; } + +input, textarea, select { + width: 100%; + margin-top: 5px; + background: var(--panel-2); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px 10px; + font-size: 13px; +} +textarea[name="runners"] { font-family: var(--mono); } +small { color: var(--muted); display: block; margin-top: 4px; } + +button.primary { + background: var(--accent); + border: none; + color: white; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-weight: 600; +} +button.danger { background: transparent; border: 1px solid var(--red); color: var(--red); border-radius: 6px; cursor: pointer; padding: 5px 10px; } +button.small { font-size: 12px; padding: 3px 8px; } +.actions { display: flex; align-items: center; gap: 12px; margin-top: 8px; } +.hint { color: var(--muted); font-size: 12px; } +.hint.error, .error { color: var(--red); } + +.goal { font-size: 15px; margin-bottom: 4px; } +.session-id { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-bottom: 10px; } + +.phase { font-weight: 600; padding: 6px 0; } +.phase.running { color: var(--yellow); } +.phase.done { color: var(--green); } +.phase.error { color: var(--red); } +.plan { color: var(--muted); font-size: 13px; margin-top: 6px; } + +table { width: 100%; border-collapse: collapse; font-size: 13px; } +table.tasks th, table.tasks td, table.kv th, table.kv td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); } +table.kv th { color: var(--muted); font-weight: 500; } +table.kv tr.total td { font-weight: 700; } + +.badge { font-family: var(--mono); font-size: 11px; padding: 2px 7px; border-radius: 10px; border: 1px solid var(--border); } +.state-GREEN { color: var(--green); border-color: var(--green); } +.state-NEEDS_HUMAN { color: var(--red); border-color: var(--red); } +.state-UNVERIFIED_BY_CRITIC { color: var(--yellow); border-color: var(--yellow); } +.state-BUILDING, .state-CRITIC_REVIEWING, .state-PLANNED, .state-CHANGES_REQUIRED, .state-MECHANICAL_FAILED { color: var(--accent); border-color: var(--accent); } + +.usage-body { font-family: var(--mono); font-size: 13px; } +.counts { display: flex; gap: 10px; margin-top: 10px; } +.count { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--border); font-size: 12px; } +.count.green { color: var(--green); } +.count.unverified { color: var(--yellow); } +.count.needs-human { color: var(--red); } + +.task-block { padding: 8px 0; border-bottom: 1px solid var(--border); } +.task-head { font-family: var(--mono); display: flex; align-items: center; gap: 8px; } +.degraded { color: var(--yellow); font-size: 12px; } +ul.tx { margin: 6px 0 0; padding-left: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; } + +.log { + background: #0b0d11; + border: 1px solid var(--border); + border-radius: 6px; + padding: 10px; + max-height: 320px; + overflow: auto; + font-family: var(--mono); + font-size: 12px; + white-space: pre-wrap; + margin: 0; +} + +.session-list, .secret-list { list-style: none; padding: 0; margin: 0; } +.session-list li { border-bottom: 1px solid var(--border); } +button.linkish { width: 100%; text-align: left; background: transparent; border: none; color: var(--text); cursor: pointer; padding: 10px 4px; display: flex; justify-content: space-between; gap: 12px; } +button.linkish:hover { background: var(--panel-2); } +.s-meta { color: var(--muted); font-size: 12px; } +.secret-list li { display: flex; align-items: center; gap: 10px; padding: 6px 0; } +.secret-list code { font-family: var(--mono); } +.secret-form { align-items: flex-end; } diff --git a/desktop/src/types.ts b/desktop/src/types.ts new file mode 100644 index 0000000..e895a1b --- /dev/null +++ b/desktop/src/types.ts @@ -0,0 +1,70 @@ +// Shapes mirrored from the engine server (src/server) and observability layer. +// Kept intentionally loose where the engine payloads are rich; the UI only +// reads the fields it renders. + +export type RunPhase = "running" | "done" | "error"; + +export interface SessionRecord { + id: string; + goal: string; + createdAt: string; + updatedAt: string; + status: "running" | "completed" | "needs_human" | "failed"; + planApproved?: boolean; + planRevisions?: number; +} + +export interface TaskRecord { + taskId: string; + state: string; + verified: boolean; + updatedAt: string; + degradedReason?: string; +} + +export interface TransitionRecord { + taskId: string; + from: string; + event: string; + to: string; + reason: string; + at: string; +} + +export interface RoleUsage { + calls: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + durationMs: number; + quotaHits: number; + costUsd?: number; +} + +export interface UsageLedger { + perRole: { actor: RoleUsage; critic: RoleUsage }; + total: RoleUsage; +} + +export interface SessionTrace { + session?: SessionRecord; + tasks: TaskRecord[]; + transitions: TransitionRecord[]; + attempts: unknown[]; + outcomes: unknown[]; + events: Array<{ type: string; at: string; data: Record }>; + usage: UsageLedger; +} + +export interface TraceResponse { + trace: SessionTrace; + text: string; + phase: RunPhase | null; +} + +/** One SSE message as emitted by the server hub. */ +export interface RunMessage { + id: number; + type: "status" | "transition" | "attempt" | "outcome" | "event" | "log"; + data: any; +} diff --git a/desktop/src/vite-env.d.ts b/desktop/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/desktop/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 0000000..f8046a0 --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts new file mode 100644 index 0000000..e1191b6 --- /dev/null +++ b/desktop/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; + +/** + * Vite config tuned for Tauri (fixed dev port, no auto-clearing of the screen + * so Tauri's logs stay visible). The build output in `dist/` is what Tauri + * bundles as the webview frontend, and is also what the engine server can serve + * directly (LOOPWRIGHT_STATIC_DIR) for a browser-only, no-toolchain experience. + */ +export default defineConfig({ + // Relative base so the bundle works both from Tauri (asset protocol) and when + // served at the root by the engine server. + base: "./", + clearScreen: false, + server: { + port: 1420, + strictPort: true, + }, + build: { + outDir: "dist", + target: "es2022", + emptyOutDir: true, + }, +}); diff --git a/package.json b/package.json index bf673fa..fdeb539 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "demo": "tsx src/demo.ts", "start": "tsx src/run.ts", "trace": "tsx src/trace.ts", + "serve": "tsx src/server/index.ts", + "build:sidecar": "node scripts/build-sidecar.mjs", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit" diff --git a/scripts/build-sidecar.mjs b/scripts/build-sidecar.mjs new file mode 100644 index 0000000..f20afd2 --- /dev/null +++ b/scripts/build-sidecar.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Compiles the engine server (src/server/index.ts) into a single self-contained + * executable and places it where Tauri expects a sidecar binary: + * + * desktop/src-tauri/binaries/loopwright-engine- + * + * Tauri resolves a sidecar declared as `binaries/loopwright-engine` by + * appending the host target triple, so the file MUST carry that suffix. The + * triple is read from `rustc -Vv` (overridable via --target / TAURI_ENV_TARGET_TRIPLE + * for cross-compiles). + * + * Uses Bun's `--compile` because it bundles TypeScript directly and emits one + * standalone binary with no node_modules to ship. Run with: + * + * npm run build:sidecar + */ +import { execFileSync } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +function hostTriple() { + const explicit = + process.env.TAURI_ENV_TARGET_TRIPLE || + argValue("--target"); + if (explicit) return explicit; + try { + const out = execFileSync("rustc", ["-Vv"], { encoding: "utf8" }); + const line = out.split("\n").find((l) => l.startsWith("host:")); + if (line) return line.slice("host:".length).trim(); + } catch { + /* rustc not available */ + } + throw new Error( + "Could not determine target triple. Install rustc, or pass --target " + + "(e.g. x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-pc-windows-msvc).", + ); +} + +function argValue(flag) { + const i = process.argv.indexOf(flag); + return i !== -1 ? process.argv[i + 1] : undefined; +} + +function bunBinary() { + return process.env.BUN_PATH || "bun"; +} + +const triple = hostTriple(); +const isWindows = triple.includes("windows"); +const ext = isWindows ? ".exe" : ""; +const outDir = resolve(root, "desktop/src-tauri/binaries"); +const outFile = resolve(outDir, `loopwright-engine-${triple}${ext}`); +const entry = resolve(root, "src/server/index.ts"); + +mkdirSync(outDir, { recursive: true }); + +console.log(`Building sidecar for ${triple}`); +console.log(` entry: ${entry}`); +console.log(` output: ${outFile}`); + +const args = ["build", entry, "--compile", "--outfile", outFile]; +const target = bunCompileTarget(triple); +if (target) args.push(`--target=${target}`); + +try { + execFileSync(bunBinary(), args, { stdio: "inherit", cwd: root }); +} catch (err) { + console.error("\nSidecar build failed."); + console.error("Ensure Bun is installed (https://bun.sh) or set BUN_PATH."); + process.exit(typeof err?.status === "number" ? err.status : 1); +} + +console.log("\nSidecar built successfully."); + +/** + * Maps a Rust target triple to Bun's `--target` for cross-compiled binaries. + * Returns undefined to let Bun build for the current host. + */ +function bunCompileTarget(rustTriple) { + const map = { + "x86_64-unknown-linux-gnu": "bun-linux-x64", + "aarch64-unknown-linux-gnu": "bun-linux-arm64", + "x86_64-apple-darwin": "bun-darwin-x64", + "aarch64-apple-darwin": "bun-darwin-arm64", + "x86_64-pc-windows-msvc": "bun-windows-x64", + }; + return map[rustTriple]; +} diff --git a/src/server/hub.ts b/src/server/hub.ts new file mode 100644 index 0000000..5020cbb --- /dev/null +++ b/src/server/hub.ts @@ -0,0 +1,112 @@ +import { EventEmitter } from "node:events"; +import type { SessionResult } from "../session.js"; + +/** + * In-process pub/sub for a single server process (Task 25.1). + * + * The engine is headless and emits its progress through `runGoal`'s observer + * hooks (transitions, attempts, outcomes) and the store's event stream + * (lifecycle + runner calls). The hub fans those into a per-session channel the + * SSE endpoint can subscribe to, and keeps an ordered in-memory buffer so a + * client that connects late — or reconnects with `Last-Event-ID` — still + * receives every message from the start of the run. No orchestration policy + * lives here; it is a pure transport adapter over the existing engine events. + */ + +/** Discriminator for everything that crosses the wire to a monitoring client. */ +export type RunMessageType = + | "status" // lifecycle: running | done | error (see RunStatusData) + | "transition" // a task state transition + | "attempt" // a build attempt completed + | "outcome" // a task reached a terminal outcome + | "event" // a store event: plan_reviewed | runner_call | ... + | "log"; // a human-readable log line from the engine + +export interface RunMessage { + /** 0-based index within the session, used as the SSE event id for resume. */ + id: number; + type: RunMessageType; + data: unknown; +} + +export type RunPhase = "running" | "done" | "error"; + +export interface RunStatusData { + phase: RunPhase; + /** present when phase === "done" */ + result?: SessionResult; + /** present when phase === "error" */ + error?: string; +} + +type Listener = (msg: RunMessage) => void; + +interface Channel { + buffer: RunMessage[]; + emitter: EventEmitter; + phase: RunPhase; +} + +const MESSAGE_EVENT = "message"; + +export class RunHub { + private readonly channels = new Map(); + + private channel(sessionId: string): Channel { + let ch = this.channels.get(sessionId); + if (!ch) { + const emitter = new EventEmitter(); + // Each SSE client adds a listener; a long run with several open monitor + // tabs would otherwise trip Node's default 10-listener leak warning. + emitter.setMaxListeners(0); + ch = { buffer: [], emitter, phase: "running" }; + this.channels.set(sessionId, ch); + } + return ch; + } + + /** True once a run for this session has been registered (started). */ + has(sessionId: string): boolean { + return this.channels.has(sessionId); + } + + phase(sessionId: string): RunPhase | undefined { + return this.channels.get(sessionId)?.phase; + } + + /** Publishes a message to a session, assigning it the next ordinal id. */ + publish(sessionId: string, type: RunMessageType, data: unknown): RunMessage { + const ch = this.channel(sessionId); + const msg: RunMessage = { id: ch.buffer.length, type, data }; + ch.buffer.push(msg); + if (type === "status") { + const phase = (data as RunStatusData).phase; + if (phase) ch.phase = phase; + } + ch.emitter.emit(MESSAGE_EVENT, msg); + return msg; + } + + /** + * Subscribes to a session. Every buffered message with id > `afterId` is + * replayed immediately (in order), then live messages stream until the + * returned unsubscribe function is called. `afterId` of -1 (the default) + * replays the whole history, matching a fresh client; a reconnecting client + * passes its last seen id so it does not re-process what it already has. + */ + subscribe(sessionId: string, listener: Listener, afterId = -1): () => void { + const ch = this.channel(sessionId); + for (const msg of ch.buffer) { + if (msg.id > afterId) listener(msg); + } + ch.emitter.on(MESSAGE_EVENT, listener); + return () => ch.emitter.off(MESSAGE_EVENT, listener); + } + + /** Drops a finished session's buffer to bound memory (optional cleanup). */ + forget(sessionId: string): void { + const ch = this.channels.get(sessionId); + if (ch) ch.emitter.removeAllListeners(); + this.channels.delete(sessionId); + } +} diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..71f5f48 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,52 @@ +/** + * Engine server entrypoint (Task 25.1). + * + * Boots the HTTP/SSE server over the headless engine and prints a single + * machine-readable line once listening, so a parent process (the Tauri shell) + * can discover the bound port: + * + * {"loopwright":"listening","host":"127.0.0.1","port":53187} + * + * Configuration comes entirely from the environment (see config.ts). Bind + * settings: + * LOOPWRIGHT_PORT port to bind (default 0 = ephemeral) + * LOOPWRIGHT_HOST host to bind (default 127.0.0.1, loopback only) + * LOOPWRIGHT_STATIC_DIR optional dir of built frontend assets to serve + * + * Compiled to a single binary via `bun build --compile` and shipped as a + * Tauri sidecar; also runnable directly (`npm run serve`) to use the UI from a + * browser without any desktop toolchain. + */ +import { loadConfig } from "../config.js"; +import { openStore } from "../storage/store.js"; +import { createServer } from "./server.js"; + +async function main(): Promise { + const config = loadConfig(); + const store = await openStore(config.dbPath); + + const staticDir = process.env.LOOPWRIGHT_STATIC_DIR; + const server = createServer({ + store, + config, + ...(staticDir ? { staticDir } : {}), + }); + + const port = Number.parseInt(process.env.LOOPWRIGHT_PORT ?? "0", 10) || 0; + const host = process.env.LOOPWRIGHT_HOST ?? "127.0.0.1"; + const bound = await server.start(port, host); + + // Single-line, parseable readiness signal for the supervising process. + console.log(JSON.stringify({ loopwright: "listening", host, port: bound })); + + const shutdown = (): void => { + void server.stop().then(() => process.exit(0)); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/server/server.ts b/src/server/server.ts new file mode 100644 index 0000000..db3bbdc --- /dev/null +++ b/src/server/server.ts @@ -0,0 +1,330 @@ +import { randomUUID } from "node:crypto"; +import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import path from "node:path"; +import { loadConfig, type LoopwrightConfig } from "../config.js"; +import { runGoal as defaultRunGoal, type RunGoalOptions, type SessionResult } from "../session.js"; +import type { Store } from "../storage/store.js"; +import { buildTrace, formatTrace } from "../observability/trace.js"; +import type { Rates } from "../observability/usage.js"; +import type { LoopObserver } from "../engine/loop.js"; +import { RunHub, type RunStatusData } from "./hub.js"; +import { tapStoreEvents } from "./store-tap.js"; + +/** + * The engine HTTP/SSE server (Task 25.1). + * + * This is a thin transport over the headless engine: it exposes `runGoal` and + * `buildTrace` over HTTP and streams a run's live progress over Server-Sent + * Events. It adds NO orchestration policy — start, observe, and review map + * directly onto existing engine entry points (Req 13.3). The Tauri shell runs + * this as a sidecar, but it is equally usable from a browser, which is what + * makes the desktop experience reproducible without a GUI toolchain. + */ + +/** Body accepted by POST /api/runs. */ +export interface StartRunBody { + goal: string; + /** + * Env-style overrides (LOOPWRIGHT_* keys) merged over the process env before + * `loadConfig`. This is how runner profiles, role bindings, and caps are + * supplied per run. `LOOPWRIGHT_DB_PATH` is ignored here: persistence always + * targets the server's configured store so the trace endpoint can read it. + */ + env?: Record; + /** resume an existing session id (reuses completed tasks) */ + sessionId?: string; + resume?: boolean; +} + +export type RunGoalImpl = ( + goal: string, + config: LoopwrightConfig, + opts?: RunGoalOptions, +) => Promise; + +export interface CreateServerOptions { + /** durable store shared by runs (writes) and the trace endpoint (reads) */ + store: Store; + /** base config; fixes the runner-neutral defaults and (crucially) dbPath */ + config: LoopwrightConfig; + /** injectable engine entrypoint (defaults to the real runGoal) */ + runGoalImpl?: RunGoalImpl; + /** optional per-1k-token rates for the usage ledger in traces */ + rates?: Rates; + /** directory of built frontend assets to serve (optional) */ + staticDir?: string; + /** base env for per-run config resolution (defaults to process.env) */ + baseEnv?: Record; +} + +export interface LoopwrightServer { + /** the underlying http.Server (call .listen yourself, or use start()) */ + http: Server; + hub: RunHub; + /** listen on a port (0 = ephemeral) and resolve with the bound port */ + start(port?: number, host?: string): Promise; + stop(): Promise; +} + +const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" }; + +function send(res: ServerResponse, status: number, body: unknown): void { + const payload = JSON.stringify(body); + res.writeHead(status, { ...JSON_HEADERS, ...corsHeaders() }); + res.end(payload); +} + +function corsHeaders(): Record { + // The Tauri webview origin (tauri://localhost or http://localhost) differs + // from the sidecar origin, and browser dev may serve the UI elsewhere. This + // server only ever binds to loopback, so a permissive CORS policy is safe. + return { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET,POST,OPTIONS", + "access-control-allow-headers": "content-type,last-event-id", + }; +} + +async function readJsonBody(req: IncomingMessage, limitBytes = 1_000_000): Promise { + const chunks: Buffer[] = []; + let total = 0; + for await (const chunk of req) { + total += chunk.length; + if (total > limitBytes) throw new Error("request body too large"); + chunks.push(chunk as Buffer); + } + if (chunks.length === 0) return undefined; + return JSON.parse(Buffer.concat(chunks).toString("utf8")); +} + +const STATIC_CONTENT_TYPES: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".png": "image/png", + ".woff2": "font/woff2", +}; + +export function createServer(opts: CreateServerOptions): LoopwrightServer { + const { store, config, staticDir } = opts; + const runGoalImpl = opts.runGoalImpl ?? defaultRunGoal; + const baseEnv = opts.baseEnv ?? process.env; + const rates = opts.rates ?? {}; + const hub = new RunHub(); + + const http = createHttpServer((req, res) => { + handle(req, res).catch((err) => { + if (!res.headersSent) send(res, 500, { error: String(err?.message ?? err) }); + else res.end(); + }); + }); + + async function handle(req: IncomingMessage, res: ServerResponse): Promise { + const method = req.method ?? "GET"; + const url = new URL(req.url ?? "/", "http://localhost"); + const pathname = url.pathname; + + if (method === "OPTIONS") { + res.writeHead(204, corsHeaders()); + res.end(); + return; + } + + if (pathname === "/api/health") { + return send(res, 200, { ok: true }); + } + + if (pathname === "/api/runs" && method === "POST") { + return startRun(req, res); + } + + const streamMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stream$/); + if (streamMatch && method === "GET") { + return streamRun(req, res, decodeURIComponent(streamMatch[1] as string)); + } + + if (pathname === "/api/sessions" && method === "GET") { + const sessions = await store.listSessions(); + sessions.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); + return send(res, 200, { sessions }); + } + + const traceMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/trace$/); + if (traceMatch && method === "GET") { + const id = decodeURIComponent(traceMatch[1] as string); + const trace = await buildTrace(store, id, rates); + if (!trace.session) return send(res, 404, { error: `no session "${id}"` }); + return send(res, 200, { + trace, + text: formatTrace(trace), + phase: hub.phase(id) ?? null, + }); + } + + if (pathname.startsWith("/api/")) { + return send(res, 404, { error: "not found" }); + } + + if (staticDir) return serveStatic(res, pathname, staticDir); + return send(res, 404, { error: "not found" }); + } + + async function startRun(req: IncomingMessage, res: ServerResponse): Promise { + let body: StartRunBody; + try { + body = ((await readJsonBody(req)) ?? {}) as StartRunBody; + } catch (err) { + return send(res, 400, { error: `invalid JSON body: ${String((err as Error).message)}` }); + } + const goal = (body.goal ?? "").trim(); + if (!goal) return send(res, 400, { error: "goal is required" }); + + // Resolve per-run config from the merged env, but never let a caller + // redirect persistence away from the server's store. + const mergedEnv: Record = { ...baseEnv, ...(body.env ?? {}) }; + delete mergedEnv.LOOPWRIGHT_DB_PATH; + let runConfig: LoopwrightConfig; + try { + runConfig = loadConfig(mergedEnv); + } catch (err) { + return send(res, 400, { error: `invalid config: ${String((err as Error).message)}` }); + } + runConfig.dbPath = config.dbPath; + + const sessionId = body.sessionId ?? randomUUID(); + if (hub.has(sessionId) && hub.phase(sessionId) === "running") { + return send(res, 409, { error: `session ${sessionId} is already running` }); + } + + // Live wiring: the observer streams transitions/attempts/outcomes; the + // tapping store streams lifecycle + runner-call events; `log` streams the + // engine's human-readable lines. All flow through the same hub channel. + const observer: LoopObserver = { + transition: (e) => void hub.publish(sessionId, "transition", e), + attempt: (e) => void hub.publish(sessionId, "attempt", e), + outcome: (o) => void hub.publish(sessionId, "outcome", o), + }; + const tappedStore = tapStoreEvents(store, (rec) => hub.publish(sessionId, "event", rec)); + + hub.publish(sessionId, "status", { phase: "running" } satisfies RunStatusData); + + // Fire-and-forget: the run proceeds in the background and the client + // follows it over SSE. Errors are surfaced as a terminal status message. + void runGoalImpl(goal, runConfig, { + store: tappedStore, + sessionId, + resume: body.resume ?? false, + observer, + log: (line) => void hub.publish(sessionId, "log", { line }), + }) + .then((result) => { + hub.publish(sessionId, "status", { phase: "done", result } satisfies RunStatusData); + }) + .catch((err: unknown) => { + hub.publish(sessionId, "status", { + phase: "error", + error: String((err as Error)?.message ?? err), + } satisfies RunStatusData); + }); + + send(res, 202, { sessionId }); + } + + function streamRun(req: IncomingMessage, res: ServerResponse, sessionId: string): void { + res.writeHead(200, { + "content-type": "text/event-stream; charset=utf-8", + "cache-control": "no-cache, no-transform", + connection: "keep-alive", + "x-accel-buffering": "no", + ...corsHeaders(), + }); + + // Resume support: a reconnecting client passes the id of the last message + // it processed so the hub replays only what it missed. + const lastIdHeader = req.headers["last-event-id"]; + const afterId = lastIdHeader !== undefined ? Number.parseInt(String(lastIdHeader), 10) : -1; + + const write = (msg: { id: number; type: string; data: unknown }): void => { + res.write(`id: ${msg.id}\n`); + res.write(`event: ${msg.type}\n`); + res.write(`data: ${JSON.stringify(msg.data)}\n\n`); + }; + + const unsubscribe = hub.subscribe( + sessionId, + (msg) => write(msg), + Number.isFinite(afterId) ? afterId : -1, + ); + + // Heartbeat keeps intermediaries from closing an idle stream during long + // model calls. + const heartbeat = setInterval(() => res.write(": ping\n\n"), 15_000); + + const close = (): void => { + clearInterval(heartbeat); + unsubscribe(); + }; + req.on("close", close); + res.on("close", close); + } + + async function serveStatic(res: ServerResponse, pathname: string, dir: string): Promise { + const rel = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, ""); + const resolved = path.resolve(dir, rel); + // Path-traversal guard: never serve outside the asset directory. + if (resolved !== dir && !resolved.startsWith(dir + path.sep)) { + return send(res, 403, { error: "forbidden" }); + } + const file = await resolveFile(resolved, dir); + if (!file) return send(res, 404, { error: "not found" }); + const ext = path.extname(file).toLowerCase(); + res.writeHead(200, { + "content-type": STATIC_CONTENT_TYPES[ext] ?? "application/octet-stream", + ...corsHeaders(), + }); + createReadStream(file).pipe(res); + } + + /** Returns an existing file path, falling back to the SPA index.html. */ + async function resolveFile(resolved: string, dir: string): Promise { + try { + const s = await stat(resolved); + if (s.isFile()) return resolved; + } catch { + /* fall through to SPA fallback */ + } + const index = path.join(dir, "index.html"); + try { + const s = await stat(index); + if (s.isFile()) return index; + } catch { + /* no index */ + } + return undefined; + } + + return { + http, + hub, + start(port = 0, host = "127.0.0.1"): Promise { + return new Promise((resolve, reject) => { + http.once("error", reject); + http.listen(port, host, () => { + http.off("error", reject); + const addr = http.address(); + const bound = typeof addr === "object" && addr ? addr.port : port; + resolve(bound); + }); + }); + }, + stop(): Promise { + return new Promise((resolve) => http.close(() => resolve())); + }, + }; +} diff --git a/src/server/store-tap.ts b/src/server/store-tap.ts new file mode 100644 index 0000000..21e9f9b --- /dev/null +++ b/src/server/store-tap.ts @@ -0,0 +1,38 @@ +import type { EventRecord, Store } from "../storage/store.js"; + +/** + * Wraps a {@link Store} so that every {@link Store.recordEvent} call is also + * forwarded to `onEvent`, without changing persistence behaviour. The engine + * writes lifecycle markers and runner-call events to the store's generic event + * stream; tapping `recordEvent` lets the server surface those live (over SSE) + * while the underlying store remains the single source of truth for the trace. + * + * Every other method delegates straight through, so the wrapped store is + * indistinguishable from the real one to `runGoal`. + */ +export function tapStoreEvents( + store: Store, + onEvent: (rec: Omit) => void, +): Store { + return { + createSession: (rec) => store.createSession(rec), + updateSession: (id, patch) => store.updateSession(id, patch), + getSession: (id) => store.getSession(id), + listSessions: () => store.listSessions(), + recordTransition: (rec) => store.recordTransition(rec), + recordAttempt: (rec) => store.recordAttempt(rec), + recordOutcome: (rec) => store.recordOutcome(rec), + getTask: (sessionId, taskId) => store.getTask(sessionId, taskId), + listTasks: (sessionId) => store.listTasks(sessionId), + getOutcome: (sessionId, taskId) => store.getOutcome(sessionId, taskId), + listTransitions: (sessionId) => store.listTransitions(sessionId), + listAttempts: (sessionId) => store.listAttempts(sessionId), + listEvents: (sessionId) => store.listEvents(sessionId), + async recordEvent(rec) { + await store.recordEvent(rec); + // Tap AFTER the durable write so a live subscriber never sees an event + // that failed to persist. + onEvent(rec); + }, + }; +} diff --git a/test/server.test.ts b/test/server.test.ts new file mode 100644 index 0000000..e5364b6 --- /dev/null +++ b/test/server.test.ts @@ -0,0 +1,261 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createServer, type LoopwrightServer, type RunGoalImpl } from "../src/server/server.js"; +import { MemoryStore } from "../src/storage/store.js"; +import { loadConfig, type LoopwrightConfig } from "../src/config.js"; +import { EVENT_TYPES } from "../src/observability/events.js"; +import type { SessionResult } from "../src/session.js"; +import type { TaskOutcome } from "../src/engine/loop.js"; + +/** + * Server transport tests (Task 25.1). The engine itself is covered elsewhere; + * here we verify the HTTP/SSE surface that the desktop shell consumes — start a + * run, observe it live, and read its trace — using a simulated `runGoal` that + * drives the exact hooks the real engine uses (store events, observer + * transitions/outcomes, log lines). + */ + +const baseConfig: LoopwrightConfig = loadConfig({}); + +function greenOutcome(taskId: string): TaskOutcome { + return { + taskId, + finalState: "GREEN", + verified: true, + history: [], + buildAttempts: 1, + reviewCycles: 1, + nits: [], + unresolvedBlockers: [], + lastDiff: "diff --git a/x b/x", + }; +} + +/** + * A stand-in for the real engine that performs the same side effects the + * server relies on: it persists session + lifecycle events to the store + * (tapped for live streaming), records a task transition + outcome, and emits + * observer + log callbacks. Returns a minimal but well-formed SessionResult. + */ +const simulatedRun: RunGoalImpl = async (goal, _config, opts = {}) => { + const store = opts.store!; + const sessionId = opts.sessionId!; + const now = new Date().toISOString(); + + await store.createSession({ id: sessionId, goal, createdAt: now, updatedAt: now, status: "running" }); + await store.recordEvent({ sessionId, at: now, type: EVENT_TYPES.sessionStarted, data: { goal } }); + await store.recordEvent({ + sessionId, + at: now, + type: EVENT_TYPES.runnerCall, + data: { role: "actor", runnerId: "primary", model: "m", promptChars: 10, outputChars: 20, durationMs: 5, quotaExhausted: false, usage: { promptTokens: 3, completionTokens: 4, totalTokens: 7 }, at: now }, + }); + + opts.log?.("building task-1"); + await opts.observer?.transition?.({ taskId: "task-1", from: "PLANNED", event: "BUILD_STARTED", to: "BUILDING", reason: "start", at: now }); + await store.recordTransition({ sessionId, taskId: "task-1", from: "CRITIC_REVIEWING", event: "CRITIC_GREEN", to: "GREEN", reason: "ok", at: now }); + + const outcome = greenOutcome("task-1"); + await store.recordOutcome({ sessionId, taskId: "task-1", finalState: "GREEN", verified: true, at: now, outcome }); + await opts.observer?.outcome?.(outcome); + + await store.updateSession(sessionId, { status: "completed", planApproved: true, planRevisions: 0 }); + await store.recordEvent({ sessionId, at: now, type: EVENT_TYPES.sessionFinished, data: { green: ["task-1"] } }); + + const result: SessionResult = { + goal, + sessionId, + plan: { plan: { tasks: [] } as never, approved: true, proceededWithOpenItems: false, openItems: [], revisions: 0, history: [] }, + results: [{ taskId: "task-1", status: "completed", outcome }], + green: ["task-1"], + unverified: [], + needsHuman: [], + skipped: [], + allVerified: true, + }; + return result; +}; + +let server: LoopwrightServer; +let base: string; + +async function startServer(runGoalImpl: RunGoalImpl = simulatedRun): Promise { + server = createServer({ store: new MemoryStore(), config: baseConfig, runGoalImpl, baseEnv: {} }); + const port = await server.start(0); + base = `http://127.0.0.1:${port}`; +} + +afterEach(async () => { + await server?.stop(); +}); + +/** Reads SSE messages from a stream until `done` returns true (or it ends). */ +async function readSse( + res: Response, + done: (msgs: Array<{ id: number; event: string; data: any }>) => boolean, +): Promise> { + const reader = res.body!.getReader(); + const decoder = new TextDecoder(); + const msgs: Array<{ id: number; event: string; data: any }> = []; + let buf = ""; + try { + while (!done(msgs)) { + const { value, done: streamDone } = await reader.read(); + if (streamDone) break; + buf += decoder.decode(value, { stream: true }); + let sep: number; + while ((sep = buf.indexOf("\n\n")) !== -1) { + const frame = buf.slice(0, sep); + buf = buf.slice(sep + 2); + if (frame.startsWith(":")) continue; // heartbeat comment + const m: { id: number; event: string; data: any } = { id: -1, event: "", data: undefined }; + for (const line of frame.split("\n")) { + if (line.startsWith("id: ")) m.id = Number.parseInt(line.slice(4), 10); + else if (line.startsWith("event: ")) m.event = line.slice(7); + else if (line.startsWith("data: ")) m.data = JSON.parse(line.slice(6)); + } + msgs.push(m); + } + } + } finally { + await reader.cancel().catch(() => {}); + } + return msgs; +} + +describe("server: health + validation", () => { + beforeEach(() => startServer()); + + it("reports health", async () => { + const res = await fetch(`${base}/api/health`); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ ok: true }); + }); + + it("rejects a run with no goal", async () => { + const res = await fetch(`${base}/api/runs`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ goal: " " }), + }); + expect(res.status).toBe(400); + }); + + it("404s unknown api routes and missing traces", async () => { + expect((await fetch(`${base}/api/nope`)).status).toBe(404); + expect((await fetch(`${base}/api/sessions/ghost/trace`)).status).toBe(404); + }); +}); + +describe("server: run lifecycle + trace", () => { + beforeEach(() => startServer()); + + it("starts a run, persists it, and serves its trace + session list", async () => { + const res = await fetch(`${base}/api/runs`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ goal: "ship it" }), + }); + expect(res.status).toBe(202); + const { sessionId } = (await res.json()) as { sessionId: string }; + expect(sessionId).toBeTruthy(); + + // The simulated run is synchronous in effect; poll the trace until done. + let trace: any; + for (let i = 0; i < 50; i++) { + const t = await fetch(`${base}/api/sessions/${sessionId}/trace`); + if (t.status === 200) { + trace = await t.json(); + if (trace.trace.session?.status === "completed") break; + } + await new Promise((r) => setTimeout(r, 10)); + } + expect(trace.trace.session.goal).toBe("ship it"); + expect(trace.trace.session.status).toBe("completed"); + expect(trace.trace.tasks.map((t: any) => t.taskId)).toContain("task-1"); + expect(trace.trace.usage.total.calls).toBe(1); + expect(typeof trace.text).toBe("string"); + + const list = (await (await fetch(`${base}/api/sessions`)).json()) as { sessions: any[] }; + expect(list.sessions.some((s) => s.id === sessionId)).toBe(true); + }); + + it("surfaces an engine error as a terminal error status over SSE", async () => { + await server.stop(); + server = createServer({ + store: new MemoryStore(), + config: baseConfig, + baseEnv: {}, + runGoalImpl: async () => { + throw new Error("boom"); + }, + }); + base = `http://127.0.0.1:${await server.start(0)}`; + + const { sessionId } = (await ( + await fetch(`${base}/api/runs`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ goal: "explode" }), + }) + ).json()) as { sessionId: string }; + + const stream = await fetch(`${base}/api/runs/${sessionId}/stream`); + const msgs = await readSse(stream, (m) => m.some((x) => x.event === "status" && x.data.phase === "error")); + const err = msgs.find((m) => m.event === "status" && m.data.phase === "error"); + expect(err?.data.error).toContain("boom"); + }); +}); + +describe("server: live SSE stream", () => { + beforeEach(() => startServer()); + + it("replays buffered messages then signals done, in order with monotonic ids", async () => { + const { sessionId } = (await ( + await fetch(`${base}/api/runs`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ goal: "observe me" }), + }) + ).json()) as { sessionId: string }; + + const stream = await fetch(`${base}/api/runs/${sessionId}/stream`); + const msgs = await readSse(stream, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); + + const types = msgs.map((m) => m.event); + expect(types[0]).toBe("status"); // running + expect(types).toContain("transition"); + expect(types).toContain("outcome"); + expect(types).toContain("event"); // store lifecycle / runner_call + expect(types).toContain("log"); + + // ids are strictly increasing + for (let i = 1; i < msgs.length; i++) { + expect(msgs[i]!.id).toBeGreaterThan(msgs[i - 1]!.id); + } + + const done = msgs.find((m) => m.event === "status" && m.data.phase === "done"); + expect(done?.data.result.allVerified).toBe(true); + }); + + it("resumes from Last-Event-ID, skipping already-seen messages", async () => { + const { sessionId } = (await ( + await fetch(`${base}/api/runs`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ goal: "resume me" }), + }) + ).json()) as { sessionId: string }; + + // Let the run finish so the whole buffer exists. + const first = await fetch(`${base}/api/runs/${sessionId}/stream`); + const all = await readSse(first, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); + const cutoff = all[1]!.id; + + const resumed = await fetch(`${base}/api/runs/${sessionId}/stream`, { + headers: { "last-event-id": String(cutoff) }, + }); + const rest = await readSse(resumed, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); + expect(rest.every((m) => m.id > cutoff)).toBe(true); + expect(rest.length).toBe(all.length - (cutoff + 1)); + }); +}); From b14b831ab776249b14b24bd08168ee1fa7749724 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 17:48:05 +0000 Subject: [PATCH 02/19] feat(desktop): generate proper app icon set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the placeholder solid-color icons with a real branded icon: a white circular loop-arrow (the actor-critic loop motif) on the brand-blue gradient rounded square. Generated the full platform set from a 1024x1024 source (desktop/app-icon.png) via `tauri icon` — desktop PNGs, icon.icns, icon.ico, Windows Square/Store logos, and android/ios assets. Point tauri.conf.json bundle.icon at the standard set (32/128/128@2x png + icns + ico). --- desktop/app-icon.png | Bin 0 -> 14244 bytes desktop/src-tauri/icons/128x128.png | Bin 401 -> 7078 bytes desktop/src-tauri/icons/128x128@2x.png | Bin 0 -> 14323 bytes desktop/src-tauri/icons/32x32.png | Bin 114 -> 1755 bytes desktop/src-tauri/icons/64x64.png | Bin 0 -> 3552 bytes desktop/src-tauri/icons/Square107x107Logo.png | Bin 0 -> 5888 bytes desktop/src-tauri/icons/Square142x142Logo.png | Bin 0 -> 7812 bytes desktop/src-tauri/icons/Square150x150Logo.png | Bin 0 -> 8366 bytes desktop/src-tauri/icons/Square284x284Logo.png | Bin 0 -> 15562 bytes desktop/src-tauri/icons/Square30x30Logo.png | Bin 0 -> 1560 bytes desktop/src-tauri/icons/Square310x310Logo.png | Bin 0 -> 17047 bytes desktop/src-tauri/icons/Square44x44Logo.png | Bin 0 -> 2336 bytes desktop/src-tauri/icons/Square71x71Logo.png | Bin 0 -> 4006 bytes desktop/src-tauri/icons/Square89x89Logo.png | Bin 0 -> 4863 bytes desktop/src-tauri/icons/StoreLogo.png | Bin 0 -> 2778 bytes .../android/mipmap-anydpi-v26/ic_launcher.xml | 5 +++++ .../icons/android/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2774 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 8828 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2716 bytes .../icons/android/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2655 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 6114 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2588 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5603 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 11707 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5722 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 8374 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 17629 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8905 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 11067 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 24639 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12115 bytes .../android/values/ic_launcher_background.xml | 4 ++++ desktop/src-tauri/icons/icon.icns | Bin 0 -> 119005 bytes desktop/src-tauri/icons/icon.ico | Bin 0 -> 25683 bytes desktop/src-tauri/icons/icon.png | Bin 2210 -> 28587 bytes .../src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 0 -> 888 bytes .../icons/ios/AppIcon-20x20@2x-1.png | Bin 0 -> 2091 bytes .../src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 0 -> 2091 bytes .../src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 0 -> 3211 bytes .../src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 0 -> 1490 bytes .../icons/ios/AppIcon-29x29@2x-1.png | Bin 0 -> 3051 bytes .../src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 0 -> 3051 bytes .../src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 0 -> 4595 bytes .../src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 0 -> 2091 bytes .../icons/ios/AppIcon-40x40@2x-1.png | Bin 0 -> 4345 bytes .../src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 0 -> 4345 bytes .../src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 0 -> 6341 bytes .../src-tauri/icons/ios/AppIcon-512@2x.png | Bin 0 -> 15863 bytes .../src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 0 -> 6341 bytes .../src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 0 -> 9570 bytes .../src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 0 -> 4057 bytes .../src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 0 -> 7916 bytes .../icons/ios/AppIcon-83.5x83.5@2x.png | Bin 0 -> 8885 bytes desktop/src-tauri/tauri.conf.json | 4 +++- 54 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 desktop/app-icon.png create mode 100644 desktop/src-tauri/icons/128x128@2x.png create mode 100644 desktop/src-tauri/icons/64x64.png create mode 100644 desktop/src-tauri/icons/Square107x107Logo.png create mode 100644 desktop/src-tauri/icons/Square142x142Logo.png create mode 100644 desktop/src-tauri/icons/Square150x150Logo.png create mode 100644 desktop/src-tauri/icons/Square284x284Logo.png create mode 100644 desktop/src-tauri/icons/Square30x30Logo.png create mode 100644 desktop/src-tauri/icons/Square310x310Logo.png create mode 100644 desktop/src-tauri/icons/Square44x44Logo.png create mode 100644 desktop/src-tauri/icons/Square71x71Logo.png create mode 100644 desktop/src-tauri/icons/Square89x89Logo.png create mode 100644 desktop/src-tauri/icons/StoreLogo.png create mode 100644 desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/values/ic_launcher_background.xml create mode 100644 desktop/src-tauri/icons/icon.icns create mode 100644 desktop/src-tauri/icons/icon.ico create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-512@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png diff --git a/desktop/app-icon.png b/desktop/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e999869dcd7905eb9c4fca249eb03011ec0597d1 GIT binary patch literal 14244 zcmeHuc~n!^_W!vF1eD2Hlp&x&z$p$Wf$`q`e!IGsx#ymA_DuWJrU5_*S#x5OUtb`P*lHQExxgciv0cu@&cQ zDUG8J&7AJ3T0QE}>_b017ViD_7k~EN+wlV%TYsV){bs+gHFMgbAN244b}Yj8p{t?U zPw`VXtiq9Fwf)}fAICo*GcMxo?@I$y_SnBnNhyqc)94WxUla4L|AVtcCOfq!`+ipC z5y>q3D>fV<5x5`tuhq3`uo1|EVQgyI2q1sNbD-zWd&5=g}Nt((6WE zacl8a=05bINO^G&Bay*5({|DwwZyDE8n!^lc>niF+?R^hQRt}^t!_SeVzCF>G681U zSt{LsG()F*R1_M<5n3uS6enPCIu;u6Q2MRU#Li5P@Nn)^S%o2b(H3TyyOb?t#Pg8Y z9B7n!^zj|_kigQM%HTw_8=%vekm1so?GiRmS;0Jxkk3CBl_HMsvl6pMCdnr8MH3LZ z|E=W68)c0igOfEtMY*$pT!0EgbrWqkzBdiX&Goot9&RZ-Dm~kbT|fRFx1{0iX@NiGK+6%O>u;x7;NEamRj`!L*nU}&@Y_V z3d-ZF1dNHPo3$8{=Odw?at=<+RR$r>OtoBA3@4+=$c96VZP8222;5)60-Yzbg_f{F zY-q#TNY^rt#47LBn!@Snuv~q%^Kubmj=P%Qy#*mbl{v<6)cz)s{dBqeDne|Nrbw zn!v7F7rhOBM!0l)MsxzyFeKcJ1K|Wvj2MuvL!2pfL;UV-#R;)nJwWG!--aN2+ssnn zhyj~}z_I#Jr^p%J-MBFRdp`~#p8N-}Pyfjfiv9HR+c=neCB|q?r4uTsZPHsh2+DI2 zC$G)K6d`$>z_)R*5yr%3lvP>8q%KNgQVdTCko=pMxkl`WemX+olLd?bLfA;m^FJNP zXb#rk^2zUjsF+cwZ?hw!Ut%!{oNErOatYMnmj^+qc_#3b$<-)U=z}Lb4no3Pgw*+n zkv9ZF07qirbZb67FtqP}y#tO*`LhVV9W5PrAhI!w3oBiwd3a6TgTG}xR`{#=wN<#H z6sH9np+S%{>7Uj3$7Nxw(0gmSmq%UBqH*SisA^oLrFXC|C3+d99Z$^umHyd0Q`GmY z+^ZumCnf)&^qFCLP*zLZzBWP4Om=?L@5Q55gPQ5Nw}xgmpT#1L2(e&5yj8MQ;y?(S z;nvg4N6mC)eQEaf1T%9zq&81aI-q^e61+^UO`ehGC6C$d7)GGv#7Q*-Zgdu|{>8Oj*fvx2&$&#M$67R}*2Pldnl{K`W;n1>Q?$vhfg zFs&BR-1fbQp(fssTCvaf+}j5l$z|iji^^*|O*K{BWrE}MF@34n_R?+0cXJLN6NfEu z?JJvgSvW(9J+wESN)`OHB$Hzj*#l&?O}+aun;rOvN&L*0`III}l46q0LHihE_HOL} zKWr}hWL*>UpyFGB!qR5BH8pYSa|@%yrfMFizAX#cEcgDJfFeL;mp6;Rf&pq|4R}Qa zAnAO>3B?T#i{8ql%vNW;T=IR(03YQHdRdzhli@+USF0Vx%6x1qZpJ`yVtG^%w`VDa zfR!;4#EBTokN{&xs8~`dvwRFeHuK_FbKiRO&7df^vN(HjZvov{@6x!+dEOC(C(X$pXoqwSbwj!I zsEhp=PT^$9sRDcItnWgm+ArOe)P}Q)jd2Z^X3K|&c zb4B4W@#INabc5>ZT%MuNrmHzwk_GY^Sg0bIk)V&RN8OTO0J=}_M{U-SSJl;g3^>zJ zc`q)wr4_b>l0wmt*q_gda7J4YVW8m`$R=7#53lA5>r94a&Fx@vzN<5N@k>$sM%;KB z4RL6QnBey?#l&OQeB{}qW)X8{x+N@D4sRE%BsRGCrP0B`Ef{+cq*T$%-s$i@DCOOut zn^;a2qkO<(OwA`?5o^Gnm`D{QsgV5Q@5pIS=MkWv;b@90o(;Mkzpw;*ijYnf(iAA3 z&+%2wQ8$Sy*MmFJkY{ZqZ?Ww1E#g*TjD#3k^)7h?R}cyW839a31tXLosoWhb;gcl7 zmBZKpj)ZdYd@*5lF~9-HlVHQzSA8jI&$4fviCd#)?Z1yNwfke*rJnG#PRUQ-vmM_} z#z`?@ZO6w%Zl~n_KK3v^0K-Yw@YlQ-X$Q>ZIm~#1_ws>|w2LOcN$=Hk%5~@b-#)eXv?7n0jn?#9}y!@!dCjDt`gyf3IOb5ZBS3!$7;jKnyG8w!dCO}7j)khd|7Y~7=FPN zG+=P!duGktoME%Sayqn^|gG zceA=iCl<3t+$TV_FFc@B4DFE{eH*ob9lKR6L}+&21g_7v;2BIjXqE*W{8>iic}uW{LOa zC|uREzTv7XA0DqcCK@Hzy1ZMJeOd(M&fS`Nij_Ei2J5+MHvP%x1amF z_DNa|%iP~e2L(7Lgd`1{M;`rW=5?6)X|)~vJz~QC@@4)J)%1e0p>7}DCis}yV`l|d z1jR(%bFy#r;>mUT(qToT=K2?<$33i$G16J?4WHH5X5?xfq`6>7j^vKB%TZHL1jv~1 z`5Ckho-p2J}74 zFT{kTBpJ=No>5R_%gk%#FWfT0Td)AJKd&3&HuY-n1=l8Cz0yUIdr(OrioU+7Xww{V zG>F_z9*e{gsbQ+4`PqBI1HPz;$74v}a2i5)_HqRia{EPhU=uh^XuR4O_%@!8TVUHF zC`lNKedxEIHQ;=u_tJ?+QP~(N^GR)lV?eXOY2~KKDHsjydgMXS@X*9>#!t0hi2158 z_3D<)39N-`8MYd1OGE7(Hz&f?-nbImup9|yty-0<+))Rc>{nBq#6^5lE$3>%;@iR} zvjYZYx??AcIMp0ta}WMFRtM%uIGobuD|3b&TU^Zt?T$-6nIp@ZSfD-vq)EpPbb-^) zN4YJq``#md2x-CaYEM&~{V*XSx>Bc3z-c(;CTLT;+zoVV=8$Wq2Q};%$|35pNt|;rS_E*fbBdA3dYH*n(!R$6p`nmP*W?H^zm3Iz%W2(MCSEQ8PAAP_{dswHQfKsygDX}dDg>A(B0fZwc&768v z6FICuUcJm&^^%odZ2Mb`Dd0S=Q?ps2zw_8F{5@S;=za88_b6o$=y*Cp9A0g*E9{01~Zgxd%nSw%Ojcl1!m z76&37u+@AbGA2)`0K6Vz6Vk}i4!C6y_e|I=9iqCC;oKqkW_((h2Qd@Lz?1Fv^~~D^ za|tF@9aQm!LacOb*usEg5E$;IAj@P@DjG=D@vy8-H;>qN_$hvuq;uS`l$#3P&L(0~ za_<~JM?{Uf?TNP%p)2VI)%;IP<;{E|pd@8{!NmKxuF>uk-QrL@k_a_Pr0IBXF{~zj zqbzN{PUQ2o$2SkqHk7a7shJn)+TO!@=tJouDu3H#P-e)nAC?BzQC z>5F*k%IXneKH@}>NFo5j^o+0()Zz#03QUKkXKCAr;FRDmPRhag^&jyE@Io4lG!x%0%K>S#!Ieibq9NHq zL!F_1`R7NR2yjWzLh)oG$|Uu!#an@|&VmDcBIwlZUX6s1p9D4s&xhfWDMa8)?!7v# zOegUaR4^)6Bl!~p8u}9l~G^e;qp1@=Dn{oyK6 zjTK^NUqL%ix~p^j$27cCU4~mPm41?Bmi3BHiu)|7#1&wg;K*T6+HfPhTa>SesmDeT zi&PC;M}UbF0D}JqC8~gqQ*~CZ(=s6*)2#$3G{eRRsp_=D_?jbtZ_x-WAAiVKEIpq; zGEuG;UCP(+8}^tI^sX;PA534{TCb{Z{fGzeM<{b2oc4m5H!5oJ!iHgFtm_UaJbvu) z4#O3fs5<)`mJad>F+ftdoFB#55LFUyu19LuuuhXeD0p z5gg7OZAbDMggi($<;mOiYb%$g5Q>xg78EyzHKb#Hl9z_64mAg6%5kRolH8uK;2n$BZOv4qf z!y5fX3&4=XY|HMo?(VX0LgA5weU&{68R~W`f%hl7EP$Wn?*EHObSN_YtFC$cWX%z=k|3wtz#B&>92VPl(YF6-IRi z9-g}CuA)J{Dcy26AIU+}C*)Va969;c;ID$S7mm1_&*9*0|FV)W&i|0f0@C?Nh^PW* zxM}~%YY4Q-uLh7KXY81d0!6cYOz*p)s52PaPDUHX=-(qaVh8caYU0fLcg3g1a-}QW0n=P5>Q0B~E(3#ZmWw$81W(r~# z>9(#7^%Lly_SSzVgcv1m`mfA3G!YuZtn-+HL#v@U|8`!C2!1ouQK0C+b ztHzNH-`DXV`{TaPWj3%$!BLVb(Vgg!^wk|(bpCphxDbH9{w_o6PM&TK+(iFyhn*YU zuH1~HQ~EYh)0cJMv*(qr>~`j>2`?n(K@tbuXg3v2zb3#}kcyL{+-M&wDv2Del+lRY z;AxMy{yNLHbg68-!#HAb7sMzD=PweERl)=S}2g#T+0#+NPO1`T{s8Rj`hP zO?>+pF~Q@NyDRB^(u!lS50tCvVH4z{$>gq-U~-t@*B9M2%!CWEH1c`hmbX%O<&ND- zP8jk`TSxK`C(}R(Kjp?x3#Pi!ASpaJ^Wzf->0DA$f&XK(RR_;8?U2$^(XQM6vka_Z zU1U$RPE;}Sc{06Sn&6+P_j6bA`4%lg0t_)e|y?JbclfB9~yBI=C) z6XNP^jCS{V9j~!O9-`i-<|K3zrt@xOUfXPf$H3Q9u;Ov$1Y#q>hpF!K_W20K9;yg& zCiT2!2<*jhHB(p<&(c%h#D0&&;mqPfjD2%2idy6o|17d=V3g$j?Qh_tyV^U*0~giq z$(lxZm$c%WO%E{n*{`v_R@g2?E3`s0P&?l3PP^`D=N-6OezUOM)CPP8x&a{{0!Ce&i zq*@;ZYKBvY%46&HU6$2-JZK=k*%WfzxR0E~+wvp5ZrV5-kwgstnrw3nF%YDMQ9&jD(`05+kkIMS8&g@VEVV_0) z=W474mE1vf35kXNqlt#7?x=mFbgS-cSmmI-w>FrM4)(0YM{IneeJ%okl81E-{4RVl zc>Dg=g_B9Rw?TOaK^zJdY&mSue z^t=BQV-?{Ld+KTR_r)s9_cG=XDx;O3p zhKZ9BD)NQ^2iQALPsHY7^Mn;CaI1jaa8~G(k7tnexpVm~q)35{iA)z%x-V~;scw&$ z=C~YQLy2jVmtzTw9$Ay}fKzNiTZ)b06dtwO5Gl;23SfKc|68Z~V>fV=$xP!yS>SN(oo-kXT?JM3$+)81RbT#l>%CK#T!;UP}V3omXv|+B0Oo*Mb zkb;y2-&b0Yn;MG3%pH(Pzcep9hzv+6gCW=%BDcQj3V%R%Xa}JM6vzid5KL-C*GnSU zvlMDMog859f%i(c?r2?x2@LCH0e*0cD3=%`U2x3zqEc!a4jjWlF~kUj9{tQSWNQv!0A{K0$&X<=YP1(|D>I|u?@D0Ss_Ia;#v zBQ?qh{EQOVpNvf=O+4+%d4@P;^D1U`H0c9@YpPxcBtZd=*S@yvE$QFT{{WRb=}jV~ zMcQ~QyNiZ^#Bf0i+9C2GF{Ocw6G`GU>M0p7x1KF~NP+>~452K2Sz%9uPQapXpKimX zhadr}D@E1%R6kAj&B-`MAB6lB2z`4o3LK0O)CKL2AIm; z7o!`YF1DH9T7j*SdfmL>F-a<;%xF8rpJNrE3Gx|DenAXvmLaJ1#_J@Cm^Mezda&6l zUwuS8bMb?E?yMFWmDsw14AJ9NCs4S&rZ2kkF>#^ccu zQ%D*UxoJ5ozipzVgpfNytM;W3QG&0m#a|4>;>v+rIsYglC< zVT>(NdYR%nZ*fQE%~igYr(_?1Dbah(4;T^An#dDNoP?UT*v=5LPY z_4AYpg`p7tFUJg>r}4fGE|QdzXqN^Q(w zZ9o@arA^(sgODAqK>n}3Ul#hTSloouMrwvz^n9wD+0UsDtMEujo0AuO(DG6QW5aCi z=H@l{#}a&DfJ(G}WC64{K|yq69#KCe&8r(G4Nr79jSNHc(@2e(XtMe<5y(-Kp5-*C zcw)!dJCU_5_6=D!W`Dl8;qI#(KaTIUlFCVGcSpQQSN?^aOAsaNKWB|mTC5{_RvVQM zJe?XF@qrW%ud8;Xy0_1uwb~!~&_3t1se57Zu%9mYN-vRdCPJyWI0`<2)!ukqC#}Jd zkS#(PUsZB1f%uFx80!W|0g-x1IwNz?O*Z1k!_1**#WhT1v|i#4>E;prWjEnle9sKr zDXg#rp+Z$2$aPZIW{SVa&oM`SG4?4xANuOuIVLTg8z9l%3@a1*?d*1M2(jv8rXIP1 z{Ev*Vj<0eVgc`o**_9NABMw)&N+`^wXg2$&> z=e##AZC-=9jkDoQ)qqNdJ>$sQe`A3o`$QyFT?k@jb~eRiSK7##$TQjurA5t0(Gk$- zckn>9B*b)k#dm9=cs0LElk8rU;^pgAqS%nO2u%5O$d(;u46QVqZ{}l+%-?TmjdIkg z(?@?A#Moe4qHz!kcBD@c-GqVtL>UcY*x_EoR6H5_AYf&=zpjXlBB`Fxdk7BMv-H!8 zUK~E!;UEl<0(5q{zE8TpumQR0aHu-G0ytT0W?rGi?m`Vo(5dR(l9Cj`dz)K~6UFJyb!PO3=K~%LEzEO|=dC zkB%kUWwfROXH)SQ8)*D|a=Hz_zK?GpO}T8)nH6P=PFI-euiE{Ogd%rsXMN; zXTW>$WrKNFZ)rp0ZP`_i4RED(Q{jZaLkG?E?;sa)*sNeQWrqv!cJ3v4Cm>2M#v2yPYpLrXi9~~Q?OVW60D9#qVYE8 z6|tf?7KE#g=hXWJ&^?soRQ^z6royy?tnzKp7HNiAn+oQHehfv9$I?h!|$@Z` z%rasuL~R2YguQk8h}{`!l)EiGf7Vb|2NlY2{wZ{-o@b0@c$+vXpvS6*Seh}hXDx-& z;yxzHW87@_x}{QTOBqXJZ+CFAcsc~g4g=1Hhv)&S94Q^g>FO|jpN?vLC6KRrU?lx; z5s-wAOXTF3y68%71_LUgG<|q1uI86RcubTmg&+qJM>9VSOwBctK3aLAEpQ+f-u(S< zZtIt9*w8W0ZNG9bnz%JRv6t9WA|h547E*Vj<82b3@bKj-yxt44(c z%b&ckn1)c&EM8k=&-Vw~*P*bJkVKnpZ_TkLu*uPS^+2CA5gqCV>V}w(l9gD)!S`fy z?d8q5omI~I%C~Ov6Qd9sJSlmf@-^ek>q|yO&apMuD}ja$vpc_F{F#jTbba|dn3K1! zQ^c4KG@)@t2$3Ta-^PWfQG*Yt0?k3h2*DO26|!+C;{EG;i%kk1LL;qfQ{(~riJaRl z3V0>nPpkq}ilH~ICkB2=^Z1+<#G6L0@4o3`JA!$-1@s;r-qWZ+1!JK%xzkO-EH{KX zFbWYL*|J110wRnvmz{SZwDauv6B*lknN(=FUI-0QpCla%v!QWkWcBM(!#e}QHduSd z6}qPgzC_2d8HU zg-czJ3?5AgF~q%Ep&#c*t@ekb@R>rs<3!hnewfRd<33IrM?hM!i4>q9FAy>O zg@h)=;PObS(gu=Jb8w+wxgm7lBUIPJ1i#~gO~u)u1TiChcm}6?@$Gv|-eP!_tA3Hj z*b(I{fW9hJN_6(+S-|1|jU5Lf6|&b*2q6mDItU$zkQVDtX54>$S#oz*_`vc?n9?y; zL>-zjn{&RwB)n8*Z9Ozk4>@dBKW_tC?_X@gJj^k4vPH_|xXyqkN0eI+J&5b9VM9Jq z9pX!N+Ht10DS!cVT5=Wu0uXxqm;b`$UkHH$1Rlu@=tm{z``#-Yw7N|5c#NSH<3&pc zX^xzyv4**a!rQY1A%K-=tY~_PY*o?xRl<`q8=g*LNn?MbqWh;L-_^Z?uDt-_xX}n1 z5I4Zi<#B`yx=iBB1gwJgi(s{T%9M8|rto?S7&ji?{!(Qw?*_8$RhC0`oCN>@@isHF z$v|G)ppw5x9*ffFbmyPo%90t55A&(UFm=2EFJKTRgW#rWA;Yo%GjSMH*Fuz5N*IN| z*D_TBi3?|AH_kIg>|lcXp2ew3?;atUZKjPQ_{rCSL0VzXontJ^5c@v0(1aX#o|1InDEyw-? Dtxdr2 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png index 40b01cd268614adcfd2286fd329f10be20894fb3..6ba45a6ab9b51082257219577958ccee32ec6550 100644 GIT binary patch literal 7078 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 Qga7~l07*qoM6N<$f~Kdv?f?J) literal 401 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 delta 85 zcmcc3TQou0pTW}8#WAE}&f9Yf85tBf7!2NRyit2}`X9-C0*4O$N_snaZj#S~eSf?h gkU&D*0`KYt49tRI$HOEa&Sd}sPgg&ebxsLQ01kp87XSbN diff --git a/desktop/src-tauri/icons/64x64.png b/desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..7a8d87105b56e3b499036e6c972f37cb57013776 GIT binary patch literal 3552 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 Wd(+LEmR0000iJFc3nTLMg?iw>elI)kn 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 Eg28Plv;Y7A literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square284x284Logo.png b/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3233db53a9cd3efa54f9aefc6d89f23a08af989d GIT binary patch literal 15562 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

-L~{+>i<`k7|4-C+a~OUv#yrqhI1- zCT_J$76z|-Wm$b4pXSKDinLW`)N2BU;MSWCU+q%EieqYvNY&)hDsx-b=PcI5z7we0 zVg!4Jb3y99n+QC}KBePYe|tx-r(Mx;o@jAa;Zj@8>Uws-7VEyw=y#8yj{sNLWRbj) zXG7s@sjZh)K9i??)#KO-6XB2|cEE=GYz6CJ5SVE1^Yz} zHK9NwOk0c!hvZ8CPmfJYvQ9$M*w(+u0<)m$b3cY!He4{ZpH|@x2OGZ;B4BS%J$P^1 zdSsfKwRL}#F#T~5c>f708$NclM>(0V*uc*y-;1&s#1MGhgS!EB)=HXVJ; zpzAsvgqYHh--<$u2akt@@DnQeO6wbZEd;vd!1X-(myJsszp8I*!nnEI(IfuM(TLQYX z)cy1Y+cTNPG(vB8Z=3tTai>%KJw2H6t*Ej4$Z>th5Zn!sG2;}iN`ed5XTz}A6RfRY&V6U~} z>6a2+iv4@nE*GZ^Y+{gyzlQwfem>tx#q4_96YTVt;hFm9sf7$XW_yw{tgeH7)M^7f zszr}-wf9GS`;pP{;zEq=L<#U1z-1SaMjD#sUeL-xy_hKg;c$JmY|8I*9fasvW~VZk zj*hNYDp1j@LWGOPzPr<6_cnjvyo@Wq)b?G8ky=VBiu~9AL}y8W4v9_ zz@&QU;;G6QUO_eeGE=U7p(5RpWKLu|KEjNqy+#a z+#sz+M8$an42mCj);$_`U#K0}%@b9ULD!%>fD)XP`K*SrXRZS&XEvPYBR3v*rdYXGKtHwuIln%t+##P z(h#=as-DFx0?-;xoo>D6oF?UPCz-(Z(*E2|le0&1eVVc;`UvN{iolv)`2P6-#GEeP zhVGI=hZ!akoE;c;&`LlySUGyFb8qTQPj|HNy>F_P@4_M9%Cw(6 zExjR|>6jI1**2m+n%Rc|oDi>lcUU|kdjyb}}bEL0Hu6Tai}M!@1%H(sP$5E1seFux5U z7WEcBs@qriWwZvxlmp35eWOmre!H||AP{tzX|V-jg2X%ArI}D~1AVH2;~&RY9p(_| zDXuC%W>pAx6?}>uwY5VQ>kNtu&cVH8x{m%*xz_K3&X?QlHy*&*reRGx5Jp=6YSqWH zCvV-VPN1_s7%tYfAS$xdrVlBJ!u|+86p@!^Vmi;+{KZoMAOPdktjXL!Sw1}GP8=q&4uw_R+LR^8eV@TdR@@r9b`{s7 z$<|-A!`Sk%jL9s)FMBq`Ue*65r;u!xQkhi)=NMm-vfE+fv~33dEAq5HIH9zFE1Oc&2$pOb24wTB6^PA>gvQYS;PzS`w^erFRV;#J)3GF%)bZF{~}d#258 z!Eb`2-9pdGW!kLD_7{2=Z^CV!GgHqDG(XLT)iNp1|Bb-UKwl7lVca@vf~h+Q{{R`z zI`_;mCD2}oghcDn%832j_IxQ05?WG0)IbQE?O*yKq=rJjB2eSMvROfq-{b$ciroK| j)&Bocw2MUk#orb;fL4evCjYNS8AMW4PNZ7M!2kaNV*9C~ literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square30x30Logo.png b/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..79902bd519fdccfef9e2020a26f1297e5a5c70fa GIT binary patch literal 1560 zcmV+z2Iu*SP)jXj&pG?iT6^Eu%)NI~+rzoD&)#dVZ@t$(5@s0N zwi$l$B4cn1qqEc69Xq&bv@h!|t~+E!=Y^RM-pFu*-@{n$S*EA&v3j5-!u6ln4yM!g zAP8;>FilgiA{aRM7RyiPSr?cjBrVr}n6Bo6@5H4pA$fEw+G=HD{CC!9(|z!r;JXs< z8sL&t>pw^~huL5^ttmWet+v&~oZ}C!nBppDe#02IT+xULCW|5`6X}+N?}mG-HD^;m zQPUtJkEgLFM1|YJ@n9YxP5}z3pG)DHD(!{Hpf{qxgInt!`qg+^29bcYpdKd8OIR+r zD3Itsnq?VWA}3i8^jrDe&h!O zJ$)AX{;}}akaboe%^Bm&8jY;m<2hLd+c!wK zV}2zLhsP}pj(A_sn}amX3`oeQS8xZobzkcCtZ~4oxw2e@kxW!Op2`zuRUQY?R)< zm4SSbw`x?moNE!7OuXne22XVP?LG{eUHfBDs6h)dRi>y7k_(iJwEO6*#91sGd23A_ zcFALAntvu(DQ`|n``qvOVMCr5mu~bUJugPg063BB$6K3_%x_8r>!Z84KQ5_jTJn76f$4UY|+OG8k?FBlLSpX zO3@=QPdPMa0HfJQ`;3hNk7Sz|yt*cXsb}C^kAnPw1|guv)V!i_>T;L}6!wJ+e|YiQ zS{N+5Q@}U|mtfY@OnYdJ_QtOpN4sW42%FYs;K5FbYgiY+)1;8r0@E5X(Fhs^7Eb+| zV!8BEzVLYD(qs~~7TgPGiVw|cjrNF!XkC8C7LJ`y953SOf;O$AvGXMX;}fC-dixa| zJ{7z5j(Gxhy_j_XLs%e`vgH;;b6TT4q9IxuN6*CY`=843B4A@0n7TThh;SAPzW4AX zT<-I+xV$rfeOp`L(XP<##ffnb&1sGHh=vB8p|iB-w6%59DObL(2rMAZZ^ecmW50M~ z_XvEtr3IEOl(48>z>ba0a1{e@V8|EZ_BIZ4TPpHj>QnIM?olUH6fkVHKAj8}5M3@H zIgq)|XM8U<@c^VY#o)a7#^z09eyt~xbEo`!-`t8#_bq(Cv{x~s7&m%Vj( z7s@fc;-&u0B5ZY+J(Es=EnVxZFN2(yGN2fpRqrl-SgDF}#)GUcEXl~0)1NQcQ^II? zy1601JCz$Fs|DJxXBAW{#hdi z&V9KsKb>bo;4&e8`@sMFgrPukKF!_0Crs_ZuW#w2&86uKg8vKRn1Gu9OOs&$0000< KMNUMnLSTXnxATJl literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square310x310Logo.png b/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3bd82dd1236aa364d2586b60f70bb44de383148b GIT binary patch literal 17047 zcmZs?WmFwa6D}Iub>kM?-8BSvcXxLWPJrO zTHQ6()6>&Wbv+fStSE(oNPzg|%NGrvrUm&>uyWn9zHK$#_E5CdZHIxw-QU9@a z?vIc|tdaWAsd3k~*?bwwNLfM#Ex$s^h5#xcHziVEfPl$SQu%W&CY4*Z&lANFM|%`= z#$4eC{fm^eAgSdn0iKc)eN=RGQ-&tj(5OfE!fUz~Sa0No({$@wZ`l2BnVhVo-FN0` zc{Lse88!riP1_Ip^Bx)7XUv+hg7<$B8`(a!0K$;Z2LFdh%?j5oSab_BVf`jE!n~kcf*h?0%dKK z5NORSHa65R2zhWt7%&zx#hkbKsKSb)ʄ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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4aefe4de0ef08bcc139d1c6455fbb250f5712399 GIT binary patch literal 2774 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..50ba9397895dfca7d4fb160daf0dee175f233636 GIT binary patch literal 2716 zcmV;N3S;$&P)3&26+ZWE;~B3QhBd*2h?IaKf#OIhYDHS2&~y=M+NM!b{!}fgoF7$b=#n4hSAWor zN>C|MQ>j9fD1@d>(grCgKwFwhA;Ey5Ex}+Q2Fw^^&v==6%RN2k-uGs~GhQK;Qbu~7 z_ujmF@A=O8&UT+I_y~Ng+aNw_AnN9|7?T;7?s`W~b#3u~Yl()&C6*O;YDfoy)qpq! zEwUQbctn5L9q8*l<==7;U;R0@HYTB2NJxSKCV@bdASPu~5?&be7iqMF01Zg>rl5WQ z){h_V?S8NAmeA>$7qRo&MXw3a2gAJiOq2nmvS9k1_W5g_{;u6Wu%EkWAT~UYZwMFG zWd|}Oe6@=bM;%(}R~&OeGKoP(2?m|2np!{y_PxGwerqpWJUb;2PZ0pygLQ-H;hYdL zLkP4YUnm|N`Gsi$7!;=jUCSb0G1zzLkQ9r8pvWJJ07kxnD15}6tgeg4JRA1CvQcap z1V~L9#MieWW(S9Ih7q&rX#*-av1Mu?LoPtp)#s+6g=a8<)IwJ|00~E*`@twuA>oxd zuc|r`%MSZL+4IB7)Ed>XH|``KIG-j#5(cTKEC3@^L;ryOo<9>{$$SA#vjj}972wbc zF2$Q`8HDrQ2&XO}T)3(Qt*KIutpH1T1Mk$-jxjuthUrapPfkm&*mFH~v~5C`_{vtV z6`&~PfAGBe3k*l1{X5m`0YbY zL!z1GTJhY@;Lx)zI3@^AWHO*Uz_Kel8zT}_KP|YdNx-JZESTLWl$Kdg_KnC7m_|ID z9o@DH)Z=IQ{FBWF9RRQWGl29U766wRLxvFxk}MYi=o^-r_aN?YVt{#M}r=5A;bOFm27|={df~?{B8W;oE{wE*e zL?^x;k^L1Y^~Y3l(WjQ`v`kUWWmO9Fq)Ciu5Kg1Cm(3A{>I8i6%Nl?Z($>Qc9+U8g z!#;svt_wwtNV{yAoxTr`w3x7o#yXaOL&pPnVV|!++N3fLy*K)WT0fMhVq~n4J0UCu zKqWRKbO{p5^$?zZ)P~v9v;`CZ`i+Izeb5I^ zLO!8`vhuvV1gj>5sCpZ2s(?|1wWPa<4{ghNQRc0thS@FOo(V*yn#*y znI=Fd8WHsqzH}1*WTI30>$E)mZru?^8CCGuU8UcahyAVnM=CS2x){R6QI^G*ZtdG8iaf9q=+)ZP(3qi zxAjYLx@W-d{aH9kbnt?3`zG4$2sEIj3oUqJZ52G+5>snN<1h6{H80N})ooakwJYPW zD&$Gs-$pzPt1lLW9sJ6*0Ji)#13zo6B}X)rCSxQB3--XhHngAf;pk}}E_4T~s^zY2 z{wxz#Py}kGU{GIUL}-pdTh=s5$~hK#=yJCH@3WxfwuQOHMVpw zs<1xGgB3ixtWOXb257rA6$D;M(kqTItg&OX@Y7rS;J~{%#c4ZKi3}@FE>jX!LVY9T zHL9ND=Xo5D=XrR(@{x_H=<)V%isf4Kxg#Q?{iv46_ICK0{@!-ITW0W%1D15Zs2Vit zv1wC`9Ch=r`<3>;_HcseVnbC-cX);7MyR%xwLC-_y8WC7FTOdXoR|S|uU@`9X9$K0 zQ!Ee5b@2Nc3;qoLxHivXGnU`Kw)NN_`>$MX62kE4u*;PWMz|-N9@2^B?mH~FZ;=g) z=zB(ksjR@QX-{7O?|rU;8#TtL)4yUM}v_p26YAmWsm=65uyzIljNYkn|B7{2glCw%(@y3Z>oRt-ACFX_4 z5gU_~SprWw@g%Sul>m%uLwU`!ysaOs%MD)634_G~tK62R-qjE)teW8#VbqQBeeOY+ z9YGjw5wQH?+IxM9EHYL-Cx(u>?;k%E8{DbvidRE&;i~Um-2u7bbzb%w2-9(q>tZNh zR^km4i=ult+M+`L5yGUh+jZ=!>BQ;yz9YYw**XZRvr~S_LbOh<+}Qn{hQ^ssrc>l2 zs=KobK*SUpQ^Y&~asf@vy)nLA%oO&zd1)lJ_TQ1fmcFyT{c{15GAeC^!H{TD$cR%I?7k+=V&;5LZ=@#nv& Wej)z}o@g)t0000{%9M~L{<7Dv{BPk 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..0023ecbb0e4b191772eaef17a2e3b01d47a54bfb GIT binary patch literal 11707 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 UE6@)A2W7*^NhwQKiW>*~FBOIMnE(I) literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..2f71f00204abc9a9a5a4e08d4ce661b8666688d5 GIT binary patch literal 5722 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 Zvfs7@XF|91?>_=SR#Hi#M$9Pa{{Z)tfRX?J literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c02a16c89cbbb303026951fb32c50a7e505b703c GIT binary patch literal 17629 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a1872537c7698eb6bc4fa139045688868ddc4753 GIT binary patch literal 24639 zcmce+WmjBJ@Gd%7aM$4OPH=Z85Zs*v4+I}PxVyW%I}GjwcL^5U8QjnO&VQYC&%Gbu zzL-6|dZw$MUAww!chwWFsv?VuM2G|c08r)Sq`mMpf7u0RWRv@>1d& z9&6_ycoUMvWD(zsKV6h#4b1KCb`|!$O@IJ@WmFneGszR$y0MTq`-V&lzaE-ZYTKsr zy&j?lZqr1ul`nw|@(7?+3V)78lL?%En~`?9Z=m${k6#ymK00fl(?^w*_|C#@vF#sCozbOoY|E~Y{ z*uVF`ng5yppD_UTzkB_^$Nw*N|9`0Vw~&m#obU2_AN}#8ZQXlAz^6#ScY`^20w#%Q zbE8c))@G$-|K@o9FfI1(MlR8Etdvlj9#sZiZbLYe*D=Q?%}HHMb)qRl`0LYaaxIPn z9e@}?&$c7n+D;tQA~UH9)yL8)zr4I`i$R2da-{uebFtA`T|IwTriQ98-4iLaCa7j< z^|nZ6L!0~jzHyRDMkqTJ6YlUGD0|+9EsI9v2j*D2Gbm<0;2V0;&NL>^O|`SZ^*NV~ zqSKg4vw}pYVY~A^WmKr{^9Aw_DAMdEd}t@WfFMqAnf-} z+#QXFfC~GG7|C=pQ`QFRGON_#TaIQ!yX4BD-}6QJzf!-S;1>6s9FEtr#_w0dnG35O zK6Wk6k8Mib6)G>?ma8devJL!lS!3xSM8LlJ%1Q&SV7L(PV zm|jMbvAIFMrL=Wy1Jct)tf)0 zNN1)BU4iGmJ8bGc3x1!xA;CsWuk{Wh5;w6_(Q{9qF=3kaiCAIJ~H4h!hO(E_t>v=<$|g!c}cq# z%Nu+pPjG!>@+eD-BU{s@m%Q!6s@cRvC|TS7AkFKgATS|4h_DtaJ)n0S_U1sh@kdVc zHtl{&7^`!tx9@DbWJq)mV`6PPrM|B4;@BrT$yO7D$bpyc5qJej4TX!$eh`em?;bCr z8G+e9f4I^)W{NK~FRI-XFLg6ZT@5RgEcPIeS`~0 z$m?zcKrERbk~g(#YEg!UkIQ~JhK9UPCkAkdTGDq5N+Jj_V5?bLyy=`|L@Wgx!d4`} zr^M-?sU}3uPdOQr7A4fLI%g)FwvI-ttDCnbzy4(ab6;nHcv7S*8=XzcH-|SLwbws! z#ZAh|YmR%eD^FRlxk^8$`r7ZRm3o5*p2jhk4=X})sq@A8|8h79sinFgnSL z95u8c{z=(5D43{f{{1v&S#YgotL1soc%$)b*0Gi=0MA$-b$LpgoD)NbG3<11nnmc> zXqq+eO8RSaGWTL~1Cb6*ar8kdh#75Azl{VChx4Iy-+YOjT0g!FE4P-BvEF4i;;7HV z_HkX0vhUf689&d(+?>h$b8*Jg%|59pu-%2X8kF)G5I2QK zrf@RKET}JT<0wVT-*7zf#r>_0{2mZ6RNS2CxE%WJ5rmbR!l+>R&lyH6-C}}ous;Fa z)>vwoXTUUbN8A@3TF@+w`;y_i=^I=~8*9hKdpURF^t*%aXqat-C$tk3AgqcmzTT_8 z(DKDl*%r^|E_{8fr=b-UO3dUp%+z1!<%iMJaii;pT0ftoQFDhs+J}uw1|qUZ#d34Q z!O#HmZB7+3ofb37Mu7}Lq|Vmrwx>Qn;bZ;R?7-{c1E-@|iA+X}6@Q>%H@53k+pc1* z5l@lBiJ!)j78zADMfYW=g3JQ>5FDV_C<9JWxye#DX2HAV+|Fort__TgzUe$Z0J290 zK%agXEUT~8^oX@%PvxjP9ngF@DQcdlO^3(Ku>3Tq3*!O+_+aZp6Pt;xQI|V@-Bd@C zz4WIpv2}E2+eZ!Xd}`;wAh@gi@?O(<;MM>2@}&ngRclGxA5o(^*Fa z{Mg`KJ7xm}tl#i5@PFK;>klV-y7tIIqWPb!1gRPWgtQ38`Gg>8H=v62#*ofjkd!Rd z-QM?nIMkQ@<^^Pr4S+6u!=bH2_qG4~H8eW?CkTf+)1b0p+Z77HE{7>?{LG{vr*^*9 z_1?1O2i*O>XN@EByBCdm>1WMzsWP&SOe}wEqvqbV(AJ0&KP0~sEbM+CYX-J6PFSxs zJf*+9d!E_(E(N#2z{-FKuN4Xwd>$4>vxR&QvbMCB7XC1YjmIG{N&)~eE|L{yDVh=X z%F2p7%QEbOi?g+{N9>7I;r;pVnUF@O1ScX9ECscP%XjX~XTY zh-g|A>{3=(1CcQIXpnb<8b78RlO;oASerTxwk!j>0D!P~Iki=XKBj|8#*n2bp(iRW z=}FLypVvEDy(u#O(hGSVCvB}Yrl)HgYb|yhJ^AZE6o2@{!+7k{WZ zt89dJ0teYpjbm1tO_`H|DlZp*^*42Hw+@+ot}2-(G*?WMOEp9=?EE1*^Md3B2DQQ` zvDRsLl#fb!2ZtfSBKthQ zpV93uzt&NY{M*x%78}n0mM2FTuKT&()NEp~B;1~hC&r$H0zRc-xT{<1E(N^xvB)i2 zNyK~X#rlmE+&S&v2>{47!7Xd-nH?(q8*Xm>`|)?oj{$_ex^(RP17MdNzB_{l?s$z< ze9w1_qF_U$3WPwG^Ga_@?6J*I-*|=2LSj+sJ380WhFQ%*uwJsmsb`f`14Pa zf~^R1GwDiKX%oxCc>^2h{yd*bHplSG8!GM|7@|QteUwMVE_+3&-p34P2(KTeaBm;P z`m%ePx)dXNjY(SluMs%OrG~)|3n0HUN|QVDAvOJRzEO7zLjSlblAX^$S2X5}KI~;_ zVs`Hj;>S&H#`ZJ2UL9~%-bC((_<{!Bd5QN}s4g{6oc#`>lF_*t(OAP5i7==8?vvl2 zhDv>&Lt9bGE7LX3Tq3`<#7Lt8#6E`$*!>ThlrTf7rY)kOL17PWrt zb=3`VD{wXXO;(n9YwMYw(`DD%=U1itK`ktaFq~WmBJg!Xk3RUWf{?a0@bteYO8=l* zup&_(ZbqbNPa{{L^C(1ld}te&kGKW z3%ec+9zu1WDlnIE1iVF)_y0RZgrdvZ6|TtDC!2B2&sZ^D!}7ef{!->_aJXgQwQ_!S z^ZtsBlmhvqmsR8nL>!L3u8p9$iR{cv!Yq_?L0Q>ncm7t@ovu{49f-YpNr|mi&8UX7 zA|`Rx(_Ps9$ag((=ay-8)pjWd-xr`m!+g>>FS@Vx7g(tZTmljuFWexzL_>Xo-6){a zkFU3dVY4%}kc5aIe&KAdjmZDU%GRWK6gP$}NOq*&El6ErWU=Q7bv??M$Xw~Yegnjc ze$D(HVE;fZzK-M@J49xW+x2OE^ViS(P2?x2-u%9X|B#@7>Vjot**^$l#r_jKu2uK2 z_~{av?@IyaI{)j@>2}|<<}TP>Qf0OWbB;i73>Vy`&v+w+t2^|eHD44T;hes`W#zH2 zOjvxcPeJJ8c&?QLbqP1PsJmC&YVAuBv4)(nCeSvQWIS-ulH+ImUTqFPNL$-}>xvzQ z%BR6r#Ytk6rcecr`-+0^>#&nfMVou$?IEIx;Rt{SSM3#gsMe5qC`sUfv*HrtX0P(S z=?M$|jMTfn)@S_KW0!8$lzOVdNarMjhbDoeV6_*yu%lUPQJjylDo~Dob>bCW#`4CQ zOI&6tqN&xFo2-XMR~b-;K`Sr@MJXnI*csoP>3-c5uyy(dVyDZ$mQ28wyt^`Y9M$4W zc-cjc%?TA_>yPNd5oLegWf&_AL@etF{NYkvFm}crT}_iApfyYI&Q~O%W_dy0BI=qr z)a|Sf4}LGeF7jp(WgJYK{gX_`qC9ynbxsmc zN{ddxL(z2P8ZhH;R1b*#Wg4@h`flcAp)PLnW!s*Yx!g3(X>*x&P32v$)r2_$^$=N~ zlad9o68EnIn>2zoW$4Q=`r*tCwY2&O{Y`Ta+=H~LT8=IH9K-dErem)09VnjeoQ9fU ze`L=`;{Yd`P{swEbRkN+GweznV$iOuKA@*e0iuELbKS8Gk;==H&!srJ_YGg7-50= z2tx+HW_m2P41Ju^p&-=z+k15BZO1m=?MVM)GbIXy}`L*`C-*kCTip*sD4v!1* zmdu`d87Zn`%)}&il}We-$g9%T17}R6YgO|wg^nE97+EHVJLKrS9kps77B@c!5vEol zA=uyH*>~}*az82Yrazf9aN5Qny`+0D+1PG8KVNnWds$iNCu$7sP%CxynX&8}KCZ$w zHfp-kUAN(&f*EVGQsR>L0;yBE?c^{N1aCZ~xo8!`RL>LZI+B1DLPa0Nmxf`lLE+;< zHNv@kgJssMgvyay3%A2w%AIw6%kA`Yv4}FMIb;hE z>`rg_ts=G1GQC}6hKRE5F&vYEF@8r|f2Tk|kBEB1xo?=t&i^x_HsHnijoME;$*5ld zhpl-*GIH-x;uhR?`(;zT%Edbw)bKsUTCY3i#;W>ngwi*Nx~qXM1IuB|%yDBO+*O?* z40s0<%L~bmg%wqfBDV^C=7{z{_!NnMOw`ov#AEAWv&nJWt8}n~G`SB5xyLyblH24G zF~D{!EGryJ{o4?pT-gZaF%p{~0(E_y0}Dj%MA3F zfkZj&Klk>2<;-L-d437#`gX(}wr5B>Pyy|DnlR6d&Df-9v16vh5~tM@r+aD3FnC9J zI6(?r<=tGZS?Z{{-~OHuBj<}QGqgY zi+oPFshtrP3GJvDbvPI=b(s5OmZnrsMp=#~qK`h;@^{-ai;`_+jwcsCeU?|#Zm9i2 zWGrZRJ~!o=c@e!^JN|qX4py!^gDR0u(T>UQ&cecWC*Y3An?qmx@rp>05WMJ0>s=;h z5>CfT;+BWuEO1RPjh|C+)b&|QyESsOOrnBoRB-CD{6`Y-_Leg@$6IrLZc#v$-3kT< zno`kAUI#P;u`aqZ=4?e7nPmLkku(9#;llBxb&wJ8gv+)7~DNSbE;8FMp zP43r!yStUs^-&pd3WEd)n8S2~KT?Uwr9d8+jUp_E1=deF9u~90a0$TDmk_{eK{)QYEweLPxA0-Z)|6M;F)Et)_YeXGA1|AE2s*CwrDU2U-5D=(?Icl zg3iZlpM+#@)H!a8PBrStlBv;ce(5gu^%{uH7HP*SROdVDHx``bae>;r`Z_bOd@1=$ zp!Y%!dN5D^UCIkPXH=oTl25~9=~@dN)1QI!lPAqcVvAP<-yS)5vZwC&iPxtiRx(mi zw~ro#Pa}Kq)Mg~#UTvm7G#pWFnc2yO=*cel4u1OVCOP2kv$nq_>MnOa%lpIF+(khm zDl2QD*fRc>qU#;R^R~F(@+8DqAsxd+^H;4uhRoTK&1%04CpFSct>fVP`A-wNsmyD~ z`H_~zhO7#Mn&6Y}GVhf#M?6kOZ*I+mSH zo>t&^4B6)kj74_l`wu#)lI=+|n;c8V9`n3={)I`dh*^Fe* z;<)9me)pw2oH-@Z-QA1n0M0~#c}thzHe}X*~8F9cvRq<}n(6V3$9rI-S0X9zOfhZ#ZAHuD7sU zGqEwYfZ(as+y-}1gM`nKwxn;q&wq-gGwi}(JH6JF6YXztJZq!bucyipM9P?fktIuU z^|2l7=?n_rREcYCW{Y{{{4ej9B0nFIvwrq-tN&5S(PGVrbf6tX6miKArkXX`)cCb*~ zs5ie{Gn)6n{c!aD>FW2u>^te)Fyph~R{K1+xe1Er(@JaTbC_LKeLD>g0jwKAXh3Gt z@lY~DlLL`rd=M$0+21dFyMFI5JU+*a>kD^MKFAbv0JIUJMv1;5UovY>9(^SI*2&8B6n@C4MmaeqsboZ zdGrmPI!sYj`w(!6g5Q-ga;%znx$#~ey+VZHerc|*Z6H>(WAnR{u=o!KOe;vzmdaIf z#s*`wGWxrlJnuD8tvveU`FFc|P;twDPVPb6W344-V*Z`<-`<~cz=Vtc*r_n{Qri=oxDjCJCnJzx^8lf* z)la>Mq_f!?9}Y~2f&K?Slc}Y30dD|CFw6GXtz%CcmP-SJmH?w~SVd&a$Ri?*JF6!C znw*VXCHtJ}?Yhet$9mOw^GxY0EH6b0+Fx zO_%3w-_i}=Q@jdlFubcEA)0LSaPG9yMLP&UP*=|=Y{!OBgpsoFC zXf`kVef=+;Vcc?GWVCDTE}G9ac|gI1v$rOgI>zb+%LV>%daLtx#w<4k70R>ko;Ivk zs0JzH?pV?O86i=7K^JCd4EO+WLF6y58=CD73wusGob7mVPkj*%@37l=`5sH^ z-B(H&hi~Ty9xgW-LZkMyj?@0yFeiNTy8>TVI&L#gf^R+Z%lU3R`DfYL8a(^%%~X7* zomEWk2oQp_w6@nh!%Z_r!NT8kq1d&T>AQ=#Z>$jmqC$u1tES(|P(DKhn@NV%)xIU( zB-EKQc&dF))RxleNUGBKO4X6Q=&))z zD!9kSnsAa$jKYpfaN!;MdE3HR`@;fN&CpfM!8{aO*=!#yS2IGyp>a-8HcL@no`j_* zWyA(L&-?_M<0JI(_~bXvArbcG@@Yn=Yk|yW;DG&5b7~J4RWs_DKc(9*-l}$^%9H7j7Nj+&Cj-?44m5 zwMA>LAhm9~9(|Nn6c=Ls(xW!S2wKi^<^7WJ0bsn|#I2y@#YHFiXsgT9wtFo-N3Ecxfmmx%|L95=~QKFKBBc zUbM_Ew_k^Wk!@3_T=^Q^#G{I#lmH}Y{P=*ae$vGWrY|hM^WUAX_@xLiq?Zs!6}m`q z-?}c|g}ne{uEskY?XqUQHx)13&Go!(NyU3?J?H7mf@buy3-x%}o?X(VB!xe|0PSXh zcan-0U1>pasUBU%oqxG)dFUh=)J~;kskKShj66(yE||)>cSC)S>M&y@!y}RuPi^MT z?8kuk1BMEE(>*&xv9{WZVm1lCD{w9um$T6$7p;xmv8~-z(s=E0Di#9ppI)7Fb8Hc) z4|e0TT_t$arT^d_a`4T*CB&Uf_P9pFYZ*cf?Zg`ahwlS>%g8bf1IEyrIcoP*x`jsM zed3+>?9<}1kh&^TwI6Q>nMhf5kBKDNwkEi&cO($!fyjx;jI{&0SjD9)C32 z$l-`Mv7|f3{Z8jUSB#dVb`^LVQHyWY8P3lu3O~=Eid3&vO+HYETFNoqHq2QND;9sq zJjNq;F;fJIvfVxDJz8b7L=Ev@N@WhmstP;1%O;E}=u5p_byI?R_w*+el$ydKeH$2Peaz~MstO|UY8^6>cb&w#qx34PEKjafeVa_Tgr^V6xQ$mMJy z8}PQ~AM^8-Cy#Qj1qmirBqSa+&lCc4+>}v?`$OtL<){>t{&Mii4+#d-rERL7c)3mU zQbuZJ%t*gk_5Wq!NlF;h5MiY5wRUk{x1imvc~V>}(rb>s>Eijwf~S`fKH>+qoZvrr2vx>WI+wJSVqa%Nf@X?JH&aEk zIN;jlwTVxRB)Q2vXA6ymkJG(a8NT3wCEP>0Lvb73$gezNK>3PaTj3XwBfE~=+Zsoe znNw4}Et|9gJR$&#zy}oOO-PGgZ~U zjAF>te$+I_g#}@biK*N+_9_F zSy>20BeYD@_A6iniF-M&anIHp^*1kuQCiTomR-`4hDf$=)#BwQa8JIq$@ckVI-%}Vd4#n$i*rJ!Ix`W|jNs!dhH=?V@ z2D@~*>GH@_EWL2TvPLXpu;2wn&OTT^bmbR+3M#<1#{$(J4YKVqxe)*aLL;^#PSFGbc6oh*JzNbt-Ts0yS8YxLE z(GiA4PpXT{heWFc(Y8*x54T*~Pf}VTK~Y4a9;fnXsB^ptWtV~Fb7UX6@CNg9jN``8 z^5Cg|j=<{2(A1gmLiZz8>Kn?H$GssHv(RrcMNAUWY;R>^v0Z!hNrE0|l+k_ozvNi{ z&esA5*D=C;brw*9fQxo1m)wYV7mDf9} ztGuqFnY0qV?}!-w@rll5V~dLRV7s}=34S)Qw(YjN5xu95UoSSco4OcXx?<;83I^32Yd5F?fK9-DVYr$A#ZBH86e>2(Ge7H$8jZVdw6f+ZYaOw31{H|CY-YoG*@S(#PnZ2gql;e(4O_)51s&H+RHcde)F_mNM7(hGR|#FE z3I8|33men=;aI_F%4(i75wsV)p5HQ-DzlM7I?lfec^=Ni;G@F2inNQH!V*H}INv_+ z!TI+p9sOC_-&~RgOLAJhsi#_Ct?Eift&47I<+U!^;?Rz~F0kbHWP7i8oLSZ~HF=QJ z96L-+_@3VfTI15N^yE=0E3Wm2gcVL_M!?itUivr@<)CN(n`zq#I7|rI&}Q)T+5Zlw z*Dg<#E;P!7*v>wLo$$NnqovvXYr6~2I&Zt`Q}vw)(6OzjTwsQ}_g=3}nFHSe$5JUm z`qz{@7S+ZTFn+m0IsXu@*p#zlfk+;7ke_O_{I;Q=ow^Nl$2)G;{8ko~p9Da#`%2}I zT6?b=6%>`72J~(HNp;ad-a~zOxA+Yl=%lE@tcFI8-h+XmW8K~C!li(^Rmj#(+ZFwi zDe+=nh=xKK)9tdcjS*AUXQ4H}vvh#+OrNA`{}?TZWOa`{8i~bJZcLl-tmzwTkapjB z`dH*{X~->9d&>$dL89{!gTTUe7cwH<{cEc6fq)xpc_wiYSBr6G{nX_rr|b%Q{btWb z=ZMrdQ5v}*&;0NAFW(J2&Uf-AK3zCwxZWaA6yWcgdKud@XNsQlMNZ{dxknSqLp$uK z(a(iLn-K&4f#Ij1)<`Olg}Vy!n$33Q_(cC_pytg6f_*B zzVKw@um3nfTy|LrJL&S@Q#^66$B=y{L^AT8D7}2G_z|Xb!5c9%FPzAa$mvDL5TNsS zaX4je>oU&rrctHkI7nVAHs2q|yuHhX^JD0rbVZd|ytCeRI&Dt`9yja&>&N+K-b9G~ zKqqVLBZU{mJrtX}p~T>XrB^P;Z13}9A?`|VnNX&kO*8I$X{#~i%Xt1!VM&e*4g6FZ>^~$c{@}tE> zra*2K(X{v$`XP0k-ajph(lm_EOb$hD);a5IM?A^P^cMb)q2WAa7(ImKp(IoQB^m}n z-GlrDoibulD31BQ>B7`Wz@eeno^*N}@_)@P^18ugnp^w(^b$ptR`YWo>Fldz$N3Vx zGRn$rPbh#*F9KzZPdJ9U2!;~%SoQ{|&8}SJV(9g`XPKhWa?cc-s;yw`)+m(=0-&6l z1Cnq9v)~2g>hg{Q)z5SugrdSucsFEz9G{pJa6-DO7`eaUwT7!Pirw>YKDr;G9=(i< z6P^U|e}c40^Cbb~O%kvpDUC3+-)`?!luJo%=SNM)_tJKce9Et6UT^y-U0ps={i9HT z@yA*J&r+8-dFsr{EzH;5XDi)m*mKmMYj@^$c>_g4S-aQ(a_(ekK^U`9?!2$#oDZ@P95>6RAGN!UkGg;xC zlBQ&bDj$T-=TfpoEJ<*hpaI1l*r6AuKu9rl$iJeDP{;?BKd4R$#pzw%Emc;A<7}eM zt&)c;uL4Z3^U2@Yns(7166AbtdR_LA$~3}x^#s4TKeZf}acIQ`#5tXYyS)s+*&HtG zX+rH&095&5F}a1IOC=@oqR^7cKda_Q3(bkD} zgoc$$kE-R5$~wN;pxU9~7l|W`{|JE?SZPxKHiZiXy$NJ5Pf0zD|KkKcn<>rHtZA8^ zmgOgpN2!GCKeB_n372|bp#8xjKSs9h&GPm!uN;tAXOrsjlnJbdzKO7ToYABrNs~-; zM?o5c_gDf>#Gmo262o^E%sUQ!hb(^-2_ z)Vx>kcfxIVxHGL9B-%+fdd!gRY{7;?;Kp8Ri8dl$JdijAGQC z{0)pKm_2=vS6;@y_ix}6pa90_;3RpU->;CWlJ0;RL(NQtNtr)71C>7 zEj7RsMu+kzzaCIL(`PDo3FiyKRzV*Kb)pOllOwadhU^P~6!F`9xMV5AhvwK*?LXRK zMIVAY(~XU?<3(IjO^fz6!Z}X>{V!Vn}6mI7&%98~h&0W!i)Ej^%>RrFoyl&w45TZWLY+ zIi;gv-tz|ng4CozIjop<%u_00Y&ivMV*7z8ux z$V9vw5y`|fUL+IC9>lwY8xYDz0vx1;_A>|zD@V}E`w!T7=h|}BCbj{sKNR8vFfjL& zQe)pch1qcvukyRl5J_s*$A>1+79cTarMI?{QT}{d45@!00^NspJ z0mKj}pg}lc#FAB&S{muv!xQ1^HP+`^TPW31&k(2{CT|ygWrMBTaTvR!8UB>JTKd-q z0@Hi_+ul%sxGxZ!6cqaA))k3@#Bpn4J|UQ7>!wl+awTm-G|Xjl^pUQmDW#9%GP=%s82LK3rr<<{{@s^ zC0y)YGw?7%K>>QjCISMg6ZDNL`yo-vJVDolBi(eBTf*|?!oOFw7X{Fp)57~s8MC5Z zmAV~}(y=@<$`_2jj-1g_A2v{rI|td*XG8a1qXBn{1U&h!CIMlR^LszTEVGh)Giq&Z zfk)8+eE`4_O?JzFM~6~Ep>ly|)xr2_RTP@rwLkRgkR}xX0A#R`L8#9)G6}SFX2fwG zWT|0LPeUHM@P=@W#maXYX8xp(>q>16;eVDLid37lrMwk`1(46S@M3~65;u_4o>5gu z&0Ue{HBh98UkTkBg1Q^SScxIXs8BLVQXwj_PZK zaNJD@>mw*2g@7=-myjFsW6_!s9cHhi!n1hJXF z&#DoIj;PD7SXK;%A}qk<>Qf-5P+hmgDiUprQd0bXL8^o+2tm~^)^pZ`6XN3%uxKSP z-dXI2_RR$-UnA}@^56jE`Q_Iu z$ZtNPPmy^?!@RrJH3W1@xH|x8e|s)VC~0_`TDw zGD__rTW1jQ-tCNG^P&Ka$#wXoB`N%ua`B=gBK3ZsR`dq!x+#RfD;%l-=( z2ZLf0z9&-C*6eF76S0=S6l~hnqi_W3n69FpqcL?7fpfml3MO3S;;@@P8OS%%?8VAD z2KRcmV&w>*^vufi$byKYyV@anTyuZmUYU_JI)pB^{&yl%3v(PfG`8g>9oyvW@rdBU zLd7u&+V9%E7~VeFSRP*IV}-`8;`D?uR~~mz8~^TOo|Llu$!WaNKZo-JgF|#{uDq-m6-8->R)Ht4yP1 znbKBx&aSLw6H9pBbG{JA!DNk<>h_U+GhNOA3okrD=$3J_m0!w=srRmV90~auPWae$ z==b8Hfx9@FM&9!@$)RyDav|`vwmdeGR+ls*#jan!C2r;>%1#Uv=$2Y^$er7x@hdc* zyYqO-{GrYtp{-euQc!Hj7aMH(&7!Z8FN;SZ5v+3$DpKHC7*-7@Xw~F+qYwR>lMP9M z;VZIO_lev<@&YFAp4qh+1?iyMpam%eXYj&>AC>8_@>x2lofSd2=D_+SlgP(r9|Jgx zErBc%8?sQ9Fk?@ju{W-K{f$z_v+h_HZinD_IZTKE!}f9lxqq|4 z3f)plP0Fg}M6>YNop&VOhd)H}70h?H()%T%Lk+wv@q=kq9|-*Y)wb26>9@4*qX;5 z#*wyHvPpu(RnWY?6)Lbep_up~l`nh!Sae~xX0a}G^e1YSjm3bnaXXpvud_D%egngb zB;ntqmkAhkYcj6a)ajlnJ{s+nYwHa_Uuelfy8`XH>NmdG zd^*IXmMUbQVdwfN9Y_p0z)iAiRpg+Qzi8eZJ zDl0AOw)m?RYY=PZsu0;vb(oX1gIyc~|7mazCb5~pw#j93n5Kgg==H`keRkaxw9QS+ z`4CD}si}vcl%zmeY}_~$xkwu|dK>lc_g-5hJ0%7eIDwnNR(o-U*J@Ck%W^!h_iVVBfiVOI*#8 zrUvi3V6)j+3TuG_v$WhkRwZIwPwWBm%ec2-6uBC@h~ef7R@jlAGe}pyM9CHjA|DWMj9vYzL}nFvV_Y|Zw9$W>tOCM{ljj|_ zD>S4YDEE>}IaT7ikqxxb55>DQ<5;6cDZ=NcNvn*=q&lKm&U5>LPo|_wv?Jrapti*( zsr#LVM$V*v%g0nqsKk9BN?X+>vyBoysTT=|#rEyS?<3G4Nui1?>|MZ3|J zp3rKy(#iRDecNATE7HZjE$3xCyBE^tY25JL@_#>^Ds_4fhysya5n=ZD4IZA;nu3G2 zq3NAqsH^TDJx^?_oeiPU-lP|(MwI7N@F^8$b#oWD8>TPi+DP>%R4HDjhgtMSY17l< zM4=?Z0s`)xhm#43g1lsis$aO#k2=XqSw~CC{q-< z5MH_dIWg$BxcI)6T8f{7KWY2PKVTC-xr@zWQVoT!UKXfvUv*}mB_2}>wYY)R`_FH-+P@xJ6tZM72Y)f{_8Z1fKbU!{YKRltsg=8ifZ zA8(|s!_J7`l~j1M#>+Os)H!i>Ph`uQy%Do7*@ z45K2=q?*3I9WiE1t4DED0@aPyy$)G=su%voJOSKk22a?^EhSWy^PyIro%@X0CW9D& zUt(#O$?GHw*s?D|tyzX5N*1#Se8Kx>b|7}mgwgjCI>vIw+OngNd?|X&E54x6s?WND z4IvgR(v*nO*LU3(j^m`spp1&XMeZk3i5a@;S6n=U0KwgCe7&wv}CqtD;$txs>`5a3;T+*&sVX>m9~}kKsgI zQ6Q<3?=CWnY0FmS(zcyiTU}&Mw_-Vy3^EBSy@Dn>cw+Y+0wTHmV?hJYSBYY2(gIJzgBGsv9%vMUl#5p$-lv>#|EIob zSQswgPEE#mig)x-Fa1P8`X=`*?{2-ThDRlwibEutSA0d>8sBbII7X;eDqeeKO5r~3 zf0mBF08l>*LkI|8*s9VcIYpl&fxBy;q^a)*dPo%JX$4j3N(}7jA9OBU6Gkg>8b&hK z$IUnKCBguxT4tUIxd>!_^gj64Um?9p3yTj|1l$7TN?y5A#no1oY6x zKuJKx2dxXIq#Fe!PtW`|DF||oRa^O0<9#$b`E0&%VX_0hi1!!wbC-jrsAH;9S57bV z2#Uys@U7=FjG9X8V$s+3c>*NqpdJ6Djd*(JQ))Yp#V7upQ_aO>MTpE~Bwh>0G1A1t z@y$*0Ah|O0i_7v!T~e?1*}~lSdf^sHwue%cD5F|}j%i==G@1jiAlCb&KQ&1BgiC^>1W{Bo^P67yWPJ^%5TiILznzZnl{Ok)focFRgEQ;p`Y z4}G}ya$xvk8Ku1SV#(C(6f1P>w(Zq6l7vy5A0@?MxG%~*7=%{%l2No%GP~*d#Ik(G zu&PhGWRi+(Wu9l*<5H+?`8TF;Su)rumTYrzJR9V?OFNhm_g&|TIpngZTPP`p(00BC zPjobrVdHeAMi`gCyItFPdT(<~_zU<~a+R8Z+L7vg+!>9hMBkLEHBpSiI^;$9H&GqfJ2~y2hPuuGCVg)6=QAugIY0ijXnq|>;C;G34lTNHdyyi?8-=ZoBWHW)(4+A|=8d-d%40@8?3%EYC95;tVQT#h0zb zf^Qz?vcsndwrp`giU+BSXvEJIdt2zMTn+AE{xY;3DrlD?jqtha?SSus$K9f?F7aXs zo%~X7M=jrN-gp3j{kfqf}DPje|h`F z5}H4#o|NNwb~f-@#Ggyv%3_M31KBhKOOC=WQM9X(pXT zmnn+Gp{<{8^AyAT%9*taugGBQa=49kHk4Cv6h^5BTqoDX5^SW8Xv|kScG5P{OVLtL zQgG6JnWOJLDhIo~k&n<&SW&Pd;^sJ4&Es0lGfc3_*nZUGBUybN-SjV2Ig*LZx4CwG zVVSQ~nSJp4D}gNoMyV;xlA5wCc)L}P8r4H-B-mDusV*sIGW5D?SMjL&EU9X@ zTOl50>rsvGx}kriq@c- zgy95`60Ik8h^hT33iiEU%os$&7fO58k+Lw0+rN&Fyty4zg!9@RQqk)(-?4;O z@MR}dO|#qxVr^{ zlp@6~NbxUypXWQ{y*oE&U#+o6ver(<+H3#!Tyy?n6qLoH2|J){JE3e!-7({XQ?eyC zs%#5Xy+zX`YB+xUXdsD8DX|yFEOAmd&o+NJJo|)Am&z;$axauOxocWEUkeiFO~kq# zN|&PkJZPE!Iiw?AcqQm=T7=Dsd0IN={cfK?bbsbhXBx3W*5^@8Tx)GJjaruvNs1Mr z!YY6AceYkUYJa*BL%$4N7y;xxUtg7ySdqo_6ap-{w`J!$4xVUKzBx`)gXaX}7Lv(vV*ZCOIKy@Q8k3&HgRr|g=0zswjCV04?t|DD^ls%L-Kd+Jmlga#+SX!S#9At?i{JIp%n~u%_eS={cw1vY2$0})~Y{JuhRa0hkeyYE$hyZaK z%xL+)kYXjJFqbs)U7atNrysisM0(Dwl6m>d+1QQq_C`H>EW0{svL)u}G%wtUiOWns zQUB|bs_323i#ThGt8gJ5(|`%Ewo^fvxXQ^uG7JEu-k5o2&F}Q8Px>G!xc@D-TQ7SDZ`pyB&Ur z3B0gvd&L3_va-^9D51#Slmh~sQ8^+^-un+q*EnW4x7W?(h+vDiGz8|zlQ&&2w}{X* zn!^$_W*rMY#wc``-gn#kexoQHn%h0f&kOwHXN2uOWG1bk+%9n~D*Z0Avbor~TOp@d z)oT=Y$b#uTuUlqgF#V8G+}tOlxvkNt!!vNUic>VBt# z-(3@0HLBncWJ$a=8Ad9HJu)rC7PE^Tz6AT)H=S)#F#6YOL~l(xv}%bYJ_BKy`!jKyQSyx!b9q91RpX&H z9!#3EX>@iMj2ym?O+3CsOH|Q|zNw<3{}m zvbirjBp+0$N~m&nPwS^anh#)HB>X^zcY2VTp@i5Id-uMnw?=8aJYH0sRt(ShrR%F# zQ6D`zf#er&SSyyFqpd7lK#=BP#~%IEj6+#U{T4?*qD1{NrtSUn-@Vu|lcjA*4eWU6 zpPPvA#%EoBQ7zsxKDqL2Vg-f^v0|8=L$}*2@h4c`aA2X)A@>TH;_GN?HPtJXBVsF7 zv@B~A*5eGW=v!959v^3vL;b0?CVuF=piRT6hC3p+?wg>eOq}Gjy&?3qhCyp`;4dS1 zo&`kc7%soW1)agEo55XgHRI=tOR<86K4B&5`NDxQ$=tY@JCB^0;t{&&#bH8pjonp)zfpOevL9qYJedX;nBiJ=xJKN>XTwtUo){aBA6f zl{^-xA~#=BD=LZ+P0Xu0o2J^!X==(8%jls^qt+k!reliSsivfgXkT;~*BOJSSDi$I zQ>&)@2;1J*W`FEcbrIgJek_W*MZ4)Y_2I;oP#IL7|6ZvID6G?!1fssOiuxb7d9X$y zBr9gVE?}2`?NCM$K|_je2c-*dUkEB0ip@ufdg4mM@mI{Ta?HN`pr3*|j$!WJcbUFq zjtg6F|H1mAAr|qRYrvEgb9WhSJo)Pz3`@VIQ1u2Hkx5VQJ9f*b^f&Y% z9pFN_(0RYF)&!fjUCjEHMZxLQXwGdB?T(G#-|`^bwiDMV31s~!fq^os-ZvCT%ANYJ zSRyL*$8WMCqwl{={5R7+TOAhz2?8yY!UFGvGlE@1-=;3!N_HX2RoU+^9-Cs0jA5&D z;glEV5}mW&;rojJ$RB)WT0n}!_2>_(-|cmZl${XNMTy*4m%nBQDAyHRIu0S~=3N8}=-&OrMgC6$ z4P9i&U-{$dbb1sgGYD+QA2WU~kIDTYtu{1${}V^udosBx=Dbl|Z^mxGisl3bTe*}r zF2y9ULSgN}xBfNbyHGtbE}rmN zj05_){r0FRuqo^s_1Ot{HSX6Um+V}nlRD0mXgzLDPx^yVLBvR3BaN`~$ARL2aZ`f?+&}i=S1T3qZb%5kj^o*F)sEn;J89~I| z_$ObMa(OOqXVZeat9RYSn&5~q4ch%-J|r;%^GVt%)8pB`^-^Dtcm+$_!Yf?z=LI?r z@c9nMEsHgH$Jtd}5>~Nbf=Ge>!fY1Ov+pZVX_7a#K($Y75KuHpl4L&TBE%^*2F@-% z2_ERYb|D&j&$qAzRwQx71zd+?6lM9&;`*(#sG!s5CFA~_+J&l&5Z1p(Fvy(Y;F+;-ZCUy0&f1e`Gn*o zW!8ggC-2OQ(ynni0^?@5 zU={x0`Kgb&SO^FGh1KvN#*nXqUbs3?SMgX}z&X}%wvs62>kQA^MIWQ}`}W5y?3=zG z#|Cq#&1dELry?ixp9NSA9V-9!t$aGp)fWg-$nD7U`GwV(GI2l9#J!k}*RfERq!T5+ zi49n{Yp++3EBfPl**`AM02g)0k+4)`teU>PD$&C@XSr;ZvC15IhUF8WutBg91n^QL z!cc`Th#+z|=GpE~#|QsbMC}@|`o>hTTPYuY8M-I#Drd#hSX zlhuUyXRL4wSA9;ZElS_bt_ZP*EcdkK+4AJ+B@G-asAe#F#H;VTO;M-#rCA?$Xy%R- z`1>7Jg)y!zYQei^)ve&Xrl>dyQU?A_ZJqp4pzjxV3-!~_K4p{`QZYiffG<(XviaR= z1n>MEWv7;zUh}?f3>tK2@I}jnh&GMEV468G6#C-ZU4K%RL1G9sX^7jocGG9Sn(sa6 z@Gyv{tp&aMR!6j6o}pw$JBtlj>l=0AuXpK$v@xW=Lu{6IiH=N|BQZ`(`#$}lsoGo! zvTA;)a2y#He>~sWz0qE z=hfoo8b6%B+Lm~$j!eFosQ5OcKA1h!US!4=MtaL3LprvJpP)2M4fHFN=nQJjMWo4o zMEYBY1{nv6)?GFBrcH*=(8u~AjIv^1YjDjM?6G=2vygMA(Uti1@rU7QRTyAmmLZF2 zazVdO()BH(bFwy@J3FW2RA{7mi}@e}C=SZ8gUfCdiLl+1rT{-1sQqW9?y&fkdmf3K z?ntj<%^PaBKQ13h#pu%r*OnBg8;`K*j`+=FGG4{v%Bg_$X!;x9i(FJM?M&w^L+>(J zsWOKO5@LkZy|x=z<0#F%qcE5cprzVZs5|Tw0_&1(@ELpub7ya^3MttSbhOL5RP0$} z&w7ix?}%G%zPEYzK^mW8W-mqd78+oN@d~Am%w(x+xik5Tb0A^dkht2drfscZ20_U zhUK|mu6?r??{)tO{wrY^OVu#-^ zMJF?XNW6TFLT)XN;chlR$qXDW$Ta;WEDHPgXr0IQIV)ukt$bl}VO|Tww!kP`q=6lW z#orm~9U5k)=c3@^D%~~v6AB$KDo&^2Wm)0o_ByGj0lOha1CGSRXZLJ@S2ylW)Z#pK zV8!HZLB+Q1dTS1sX0a|d$Z<20dp6DXoiz-vOhlF%|2?7Xmjl6}3`H|A=`Q@ib}I9I zOtI?FBtXS_rdZHpmNb%ke6>v}@kr6ul>x&1f(VZ|{QdEyrEA&@{CRBl#~Rs@F@ml}LlJ`s3l)P! zL~>B}$h(w<;oq$8P!$`(MB)g&B|yn!U74zbU74foOV?noD|W|;PB~wjpw@NBu-Smo z02I%7o8v_V{*I&|?e6DVcEZNfX(M$513*9O>xrkh8UpAougZo7^3&33yHN4s@C0Q- zQ@g*7bd{f{zMJw57kU?fdsrrGTASmkETtx;d!{k_iQ7h5>XRd7@(8+i=r6mh@y8-6 zaug3l#t0NA7h=rfWO9z!U6AMJvpc5Ck_&q9N8|@A2#b&F#x`Te1%w1_g}?bsg3lZO zQU<-v*U!U3D+%adX#1INFHfv|z8H@c`?Ze!ln*2CwcmkD%wB|n+~x;EO8?mQ0;i@* z=K*6OrJd!kgxe~h3LCPlUWq~I zD3{#LngA!8l-k@P2q6_L>AZUnhOxgKE@bxOhZA9A!o)3&%v**os;{U&Bu4LtYp>=q z{f}9usPvsVShe_W+@`|cq&~$={e?IB^t+_6iAvO!tYY=oZ1G1jbv0aN>W4qwzrr^c+BNEer=yUG8D)V$>EHh=b-4Q!fwlW;?aMpF=61%eQo+@y_~3lajb^$V`c;2%wPDX%*pTf!+z9!x zZybdiB?J#w`3;kvW9pz2psUzvvU5xXI6g z_z!4jFo$EIO{1FJfc9SzY-_8uZ{9j@zLQszV#Y%f95E@qAdZG@N3}tMX7@L3a5{Kq z@l{s0WMcX&hGDFI2lyYr+Ha?g!TAx=Y)S!8pJ7 zG5gMIWtOFms3%@%?aC76`69C$8cML{N~0E0?lPy%6(pk*F>L(%kv=ZJhcFUXc{Qiu z(3)}p5LA265}p0l#vO-~P!P$%I{LI_6xWk{Ej#ge9P1sKqN!uh=3`KH`Mo;JS!7hz zdHJ?BK3#RptGIhT(3BMM0!4xhM0sPr(K{bC#-onL&)I0k#4_idO0E-q+wOHLpxfvW zutnDaABx0dIT=No)igK#q8tgxnILKod2jXAeCk)v7&-g9{YLnV-&18GO!wor1l%0- zmFOmmP*H-+!h+|90N{YxY_ z3q|%Hjs2!o@x5ky4&=)9UI`R#E@X`4yiUsly{r|A z9T$Wr+D|I7^wO5zzbjGPwF+tRUNlpI3Ej@H05{brc~-{dc4w2i#E`cF{=8k!(45u# z>;*Q!Il^N6C(|-MxgK%N@Lpg{Dc#QVZ6_e&+jb=^YZk=uhX4+bSu@aT% zjl9XjZsPh(mAA0BYX+CeO)Q{U3I%17hHpD_GSz=tm?x zGg|TWE|RvVWX(ZH=q&n>wC^d(k8iOuNmXaXT*vVdq(-`x{|j>? zgU0*aQD?Q+0e5qqRh3zO{0q_7*%mX=gnrCVQ6|-Ai$rDdxhrdzvUS;)&bFb{F>(cA zalWo@XdzQ(3(-TrOU40z4$IeP9;fQ+C^Q||&Rn==W1gT)E@aYHU{-KzmM7`pI zabywI1}XI2u*q2F+1J2Nd6SZg$Nek>OGd%~kvablG*k zrzkF_SrLp0P#>(&|a6 zj$`yBq2@~Dip@7uG}OpCP+KpZ+00+HV{32`zHrpGxxNZ{juL`J1&fYotK zGm_AIdAEBvvBySDg87VCc+_g>{(RTnGXQ#aMN_xTsli$YE8I!GWINY;7VL4(MsIbU zp603#P*@iC#QBx%M23ZhxDLj!6M4xRBEx?_44SAU^SZ-I&xP_E@)c{od7)yBl-*Br zYg~j;Xt>zEHq+r`&7!2FCqoTwSC+Ry;cJj1Kg0kQjuDi!_xV$0%A~lQ)2%4F661?? z2icX2ETcn3wT^iIrZ{TK4!i(TZY|^*OA}G0t6=bDc^FZcyc{q0&g;k@I=SithiqM~ z)VnU?%!f2fn3mCVKXAwjakpIZh@G@85~UGTLF)MQ1h2EpL>l7v$%Kd8;osXLnLyzz z$Y+^WvknU94v;uuV>*z=c>!{$UDwHw^41`;Ia~K8y!ZtiKEKv>fJ1G78^uLG&_~(z z>hIOQMFM`U#~{S)nTP4TM}w+CkEp-5E96Z2+~6#Ndi*+S-USyY%U)_3M14Yt(fjFb zyYMP%l*wasOh+$L#ZaaZUEPQ|Qlc|T0*3YFc_sD)- z?lbDdpM2I2Q)XzLh1uGRzqvdMMmyX36$aQNaF?sF3;st9D3URgmcEmhDQV>2^NtyA(ZLuL#R* znOT!y8~JSDaxR!0-U2@I>ud%AYGHTg`2p~P^tpugcBkD0Z_VmmLK-$)Qi)q9Em=gf zV79=r79!OKP=3W&TVi}ySjI=d@pZYu_cV5I>Nva0>_)LKzQ0zj%8P=f0H%Xr6_Ry> zC9%dY#xw|gjz^IISXQen!BQ(-)BwC1_)yb=v+EYQCima{DjiM)B^e9+&c0@Vu}q!} zJ}}63>}=rCpw;4h(D-_p=;)K5BS&heDfI4R1L$eUlSmm`_us3I?e#x51s31~jw2KT z`}5hY1Y$|{M()Rghh8f5)%A2H0s)quE3pl~0r|9t^jy$f?aHIvK?2)FXnt z5;}?QsSqL$xC$^p3*tR{fZQGdGw(W^+Cp-J!{hq(7edz9&uG6Pa zbyfdxe#^`LLW0MG2LJ#_65=9?|JI)W92V*s#L(G*(FagN?AGVCJ{T^M0 zXDWy)og`+y&(Ddq>XNjIKa&)<@es0rhDDvszN6chr!*O``i4HFBLB7G;PF~hB;<3t zuDI12RoWc(iIDw9UW`oB&?cTmXE|>sbiP627g!|#sS2ZJSbUb z=+c{^Jq`WKbB@DyxPAHS&a3-r#lHStSk#D>DEGNKsoQenkB_<1Z$ev2ngY$OM^{_l zuhGs=J?ieGu937iin%tmFB*HhO+_sh0j$!-uWR|U-A=d;e&^Q|3!|^}R?(u8iA?^l z=rpoLRv{eL6;xI~+`RmzG|HuP;`MrNVWIgOv|dM}RB};CVP(zgg#dD`ITCXv9W96p zjjy@sB{416O3ctRHLu#wI1gKykcp0|mB-sorb$3&QElSJH_1l6Qa_+&Zzn>iG`H_; z>AY+XDP&Z`{zCTyEIaYIf|EO1(WDePq82RHJKMFASV&?es`xtUyBvw=#T9vyDy~t` zNo``I^RnkvW%bALgXxyuZPrmITe{al%3ugE6MY$R27&b|#MI`5<9*>QROUNN)dS_C zzAFc!L^B;7NuT1{XdIlYmyj9fQ<%{(w!|e?Uu91u8g>YY^g7lQ)oje^ll5BH?(r7N zci)FyQIx$d@l`C+CFb5G#8{HHzn0@Rv_z~o&Ap&s($=6r5nen}T?i>j2%3F>PVA5e zJ5Ij=^lxDy*bKKTrhZX0>atw*&1OFQ3>2#_-xFWGcYJSfBcat!5y? zcDA*S{kW+a%Kg_qjlh5dT3QG$m4S=+%`|%Kp?(kZa8c0UOkS;Vmyja_3Dqtm#gy(Me=Li?XMB6)dO6JF)7SZ4o%5}pE`mFAHzyp0o zU)X>q3J11KCWouw5AN%%HM(i*vj#-FQ)5>-&I_WhI|3sgl5qDUY_zEO01AG%zPiU^ z_0cf%BU9uKwZbC$4D@Ju+%>qV#=jE=EQd8l zSJi^eTZnJ0H&_$8AkXc#lqroz`Nw{82M6$beCE%Ra|%{UlBzBon;89Qo0_f>z<+Rv zIl>!rad+}FQa04NtPo+E2aWSWH~lJ!7EPyShj;))+I@f z1?lTtS>gL>>6$Oj8F$_^FmA-9Y^SHYD6GrUPXT9^ixD z!qyW`JNIgH;hQa0>=@OJ%W{2;r_Pvt82W%T(N40gHp5qUc`JL&6SG+PL#k!EEdfl= zY`&~tGA@Zh*SH;S&}C1w?fYo~9wtL;`H)rs(s5tk#lVkYHIeAce04!a0rM}k%>H%? z1vaa-2XPa@^dAgqXaha2a$~V@+bTfHO3UW1Oh(xBH z{<77d*R4@>nPKbVPCH?RYtVu`DB^3L>WVrLI0K^w!$(-yt&wItYJe930D2EJW8XGV zz5Nl+b0<+gV;v6XetQ373ij?}_B z5pu|6$x+m}^*+YxN=y;Uuui$X^9xAN`F>)Oc6^}uZql8IH;`H+VK_4i*;&DmAmD(h z9%FiJ^~>si0EBH{u^qpJfsN#{t!0PjJU$;@<9Wxp1jvRYy}{6$~;#%An@d$V5~X#Wt> zF|C7w&nVY$?wy#3=)1`qNS{V#hBN*|(!hcvicS1jJy^>U{xJ;*sr=4hHiVN<%_%iE z_w=F(s$f#SnO46l4$8WEP7%_W$D!##?0$$0%@ar$Z0YjuvdhFLHPwVWwOwx}%QOT} zbj2q9ofFlMNk(WKAr-*2bLt4W`NWoBqpd$vDv12dr?F)Vm7ke5qf;lzIUQu$&$6d+ zGvVeE2?pv-Wn>rp1pWP~YkH%r5Ajb=KU`o4I5rZu=hPRwiClTCRx>{xL&wYDx*=U% znUIF>?exomKW;ww`Atqbqqm42L0!r?E-AaX^_T&1+m zU^p3jOXtD%mw(YvC;o-N34qD7)((J0Xr9Wd4x8qxD|P;ACjbUGk^$B2D*DK?^MM1{ z4^%rwF<73RgCk6FS&a*GlZXuI3fF9RrD+_i)NAp2Js`=S?;PkM$4a0RkFB)g{V?^R zS9WcmJk;SaAaePAH+e)#=OTrg=3E9lc+qg(0#ru8td8_LCkjPP@gyRm%`r6pM6sAf zB89)3PV?O`zn?f~5ZWR?Tl0+nKm$RlgPHgd0+Yc90cNsb7U*#nCdcqCPN^eJ?(=q& ziAXZ>LnthXBP7!$19cDCV*b1_SQINIZ6966` zTBtGjpefpCZs(yvOuEVP^71P9_JfhJKws!eVlP6Q=95Y)=#MjO?jDr1 zhfkZkvvX#wZ5w z2e7ObIvlw?a4U{xY0P8_y-Jn6{@R#)a|*~A#kd^A9=WeBLoonfo?TVITF*5Pw0DmMOxRHa zv%RE08ocl%LJ{McIZh?Y^K5LC!rZfJML>q8qYGgZqf9WCf#RKs-k-ep?pjL{HTh=5 zYy4Hf$G-W5EyaZA;+rYL#rE{d7(ajvk}#lVl&lpTwTax25LuZ!tB%D#gV{5z&N%Yh zMqnOKtUvS4-bMHgkmtCHYcy|>S?3CJ|1yAikA+0Li^-`F;OJHv<0FZs0T2hkh13t4 zAbL7e#ITy}g+((WQt`Oh#z8;-`#d&%-)HwG1&-S81+k~uSKBSiv)+svdKaE$ zC_FEKpEznI$|@S!!O|7IBb8i4LK<|PKk7}dpc#jeu$|5@Ns_Wt;ewYN$o*sMVP}91 z;jXj&(cdD@X$z@eTMm?OT@LJJnFrqkbYDooseKzxXfcSf`^84s%>*#3$ zUoXp?o$VUslnPph&cpC%ZqV21OYY>=TdK9!FR-?Lp-=b`Lez~UI@g}WV9#*_T@D4J zF`ZUr&z?bO#M{e`^b+p*)?G&w(O)dRW~f@t%d7WKD(o`&)}^L*EXPbd-EW@^U&;DI zO*}mecMRLyt?ny=d|-ugR+f%!W$q>E=LeGz5%5fZi-SpA9B{|V(PTl1fM&ZS8NjEj z)VKQ~7_6kAEU)dLB=471mkQZ?(AP|xHM{kOaYfzQ=JT5M_SDy#F`Q;c-yXpMZ#CsE zKRTnZHNUeWv>8!MKRU72NNSX4Uq>HJV#ae0UmQRE$M{q?BK z2~o2~yBwrCZkXt``ze!9Fi7Vmn2ru8uXeX0(Qds{KcbQ#Q^g`Ln>8}{VtV4Ost=tdo}ol9S=W`5jkAv)$4J5DRb5LO z1v7PDmKVqAjkibg2%i@xZEI9iC=gspfQZ_-f|~uv7b3-i6tlCfgZA8s{bjHn)F&h= ze?js&ze|Z_QIqV`*wn4Xat-I9aP_g{t5G1m;hjEtPs|)C(2Z5hmdVeqeCNttcdmMx z)cZB+y#6>Vn*-wM)puB_W7o8YK86e6*jEv8-G z+6h|kSl)#lxa%2z+o#y4hJ-ZF%I*?sbM{Qe-7yMW^GF_+MLW2TcRC?R7(&LBxU!PN z&<%radWgS#5sqxWXi?Gpnfims@wroqtPelh>~79NiV5;Q`RhqN^08G{*Y;@o$ew^h zpdLVAP@iCuEQlOxjqmJ<%K5l@CuE++O~RauvFlqm(QTg6en$~dXRsF}1pdyGVfAn> z*Ok6Q%rWZkT)iTOQOWE@sAqglXdoFXMb2ZiU;tioCde-3a}$q>RAyh!QRp#2Gdnby z=;0=jxwVm+>#qXnMwG7Mm&+TZdbExCr4j(lbcBlt_?1uUPlfN#A}0HX2fe-fL`1C* zmn~;7$BCb)q2sTP%eSh(0jwVc2=RsbB`hFY(y{uW}yy#>W~$!6LAfX@dC%$1QcryK6b`Ka#z+GfNSsaINMO@?k27GPF`T)`enLwDfv&!|$ z3(Fr~6z1AlU%X`x55i@DxMaz@94%ef?b1P(SnxVE`R$JNPgwzN7G+RS>J%K{N0gV| zIu#T18XdN6iTvhd#R~MVXi&_I|_9AX3kP01*fxB&`r}Ib}YX90~ZY8U!K~2yG|=JEFw- zm1%-eR@l5ntO#v=;HJ@uqD4@gD&88D(8p@Y z>D;@6@BO%Tq3-1pa3d++NiiMbGUksWh97%DR$H5IRyPVs$b}Bt zn7_1PQpIJ!s(B3O^9q`^DwGOKAxrA%9@I zwz!&Ja=hi`9SuQUt?VqMifQPNtQ`BpJlUc4=_mf@2z_;qT*E0|6AW2MD3UfdQT)W@ z7}xx}S@U>x?3dCO!AAG`2E7Vn7VZ)Z;Gh+E;qpUsZ1n*m4AB*M1^1xZz$Mdp2v3

QI~#HFQuhnU!d}vo~01< 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/values/ic_launcher_background.xml b/desktop/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/desktop/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..c0300d24dc3c256347acac6f55b7b6a7dcb3516d GIT binary patch literal 119005 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c8eb88d072337f5dc1cdfdbc1ea0a2e36e75e2dc GIT binary patch literal 25683 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 B0@VNj literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png index e0f355e64775a3be159e990c9ddbc833cb718cab..b93f909e067cc1f4c7833b9a5bc7e704514c7b53 100644 GIT binary patch literal 28587 zcmcG#xLoo*IC^^qP^nTxtHtGD<;{W(YeqzW& zr!{^tM&7k>cgcT5VpT#20*=n&IpaeBJ^_)t4*-t&-{b%J561t7X}{wD0RErqAjBS zgL#oAr^^`Wd=4`a9d0$j1JhpS%#Hveq!admisf#Jf}X%X{c2%hmxU5L+tH=7Xf^tB+#CNtJ_7-670*ZW zo0g2Bn($jfT;Sna_$Ur~L53bVJ7rK)NxQG%dhJ>SbMR}k>;3t1zU@O@7e{vAma?F) z)5wsvx||NF+35!L_)$^j%dJre5cm%(A#)$uWPW{9ojKg9=V0sav^lHhY&X~R&Y<3H zCCzyMP;fWv=C|gj=PKD>ywTLJDH?Iz@&44vNV(vx0B|3PU$Zp_d__Sv_0KK>x;~&f z>86o6)%tw4F<)HIW=*TkCZkGn`D=7AIUkJOGCFRy32-*&Hk$wYJi zntY7ciwWGi`|Y-FdAjX25lX>E;^5YbYs=d%lre8`8w9DZdUdPDnw?es?Yk`awa(W5 zuzO2n_WP+&FI{cczb5XW7%+7m6c=<}s=Mzc#t4uy=R0SdbwE{_p8p&2=9-p}(33cQ z^;k=k!zlT|;5kw27M7P;Vm>eF0Kam0<}UL4D5HW!!7@}Fe8ZDKxR!ppee z!4|}r;lqZRdeP;H@9p2qw!9pT4;yTQF50Lx@=0t#Tj!$rGxUu82NYI8CwP4>%KX`T zF5#%i#9D9CIIExN^T&RJ@NSxbi2UHJQs;}fkELzqY2l`r1MQjsi{nUeu1A8J@BL{) z#iVwjCRlsLn~0pgFiBrMs*@YN7yH8`dvk|KGt^PU?C1&eqfEiAks}AFDj&^oy9*$- zym+20dgi5>n9(M5edcglGn+P8qjRRk?#yf2>R`3B(bl2INPcPw_xT6FLj>^qx}{zO zdytN$M2E-{e9g)kYqpik9YeNfJsp8h5|%Z_#rU2O%3_17c<1Z;HutRrs@DDNt0icb zI=|K08D{wF=BMclJ}>3!a%0=HPMxoiM(-IUv-p7(K1Ph?9Dj1A?=rSIavBd12fuHL zN}WMn{He!=1))nb^Dx}M+TewSfJ{5n2Hh;gLj!ac6Yfpx5zz)(S8$~AQ2o>B-%j^g z1^@+7yOpbQxjjNG?69jp0lA*&lpA{Q@>WL_FYlEKEft&%zy%oPx}ZTjdu03-ms4j= z&F0LbIwYStaz+0hny(*lRg4HLpS7CX@!jVER*I(xlUR|OB5zz8AlWaxIEv}|8urYx zoENSJ*)L$X^+b>VI7a+OGk2Bwnd|x3msHq(OX8?s<^ziEy0boUN?hk zi#tpoUDoq5iF3R}0`TY2WG=!Gx-9GTW}okN7g^yct?5D61Gme~*m0nrcELlcj$ied zX+i(-XA&YbH9O+BumP@<95`o4=Rz^t*bm!v9$a~>7j?XtB0y5LP>}9ax~2Nqh^@)> z=wO?!;G2!DHY$Ll!dgHC?z4$aK~K83oX}&+-&j5e;vUVuZS*Nx z0UjQ=wykbu-A1dW$z5#>RF<_hH3#hpy9rY1J{Y~NL;d-llaCE3o&#(`=z#wIKYc^O z4J|zWLV+i32Wrh;xETo8L_l>S#QeO$`Y%UNtl6 z^OwsSiZz0QTlqu!_K^Nehmj$dBB{YP$3aQ-3_9avx5D-j98}A?$0KkvsvBNh9c7s?VF;mlLXoUjy4=XIahP}*o#jZMb^lnn7Tt(%paTG6ON@m{sXnkr+{dV#-j9&N zZoQILBR%wcBW&o0H7OC`_ASz2!aIxC$ahn38u@Ol?;f+~T>qJPuhqPBl1JGgF(%MX zW(7gICj|o5Y4x^x33cA{b7spInID*U{iwl(7g(@WNn^bKTUc0JQwzEg^D@11J)Rm= z&$j9+OV_Y&n%7B|3Bf_L?{+t`Mm3S2j`SUH-FZ3vW2=x!ZIYPIP2RtB=uid*_BgF~ zNyw;k`!g{6*!Fuyw;RXcY>Y4-8NBTo%AG!|jN!SJZd*6ETN^aQ8y5hsYoF$PF0JQ} zZSmr1_Ww6_a#@r{c25cmxRVe$Ga(aArF$hgp|I9T0ATV!6(dm*o-yvGJNSjiR(R=w zEU>L88}#;{v0Bh&RyqT<;9ojD8uxCYbU^kTni?Wa3iobUVDO!G6gLvphEZldOr zC^Jnv0#RrA90A&o)UMoV@0yQ3?XZwbPM)pt+^;;a%>)qk3AThUg+3=>Z1-<2^Pd*% z49cy>%LVx)Rbt+;V!g=zIkCoY#6h%WlQ6Z0@qmyK*y-1XmhYoy!-NUxpZ&$*RD0?x zZOx5T1j(EImXa*p97?G%8gUUF+4@Ym+7x`Q6B;}$`r5wXKDRY?;4cPY;5I{rZC}uT6F2H+kBGB zZ!&VeZ9uXDsMl^E>}UHO=Y!IOg>v3BfgS}&Amo%9@erNgj8V{F$;C@x^+n@4i;ib{ zOclj32(PzZ%*_sz^`0-Y*fB}>LjmBY#IN!`wokhlWv>>);(qzes~`|=4H>tOb%(Ty zA&O@MW6ETwpczQ&@xR@+>-NIU1vQp~HHE1?<8`tWzIAXFZ(ejV%N$3e8wnI7Fa-Ux zrp|olm%7+!F_NRPzP!BDLyyL)Vg>-kmB3LC<4S#r?_LYrcGKfKD&fP^LMy0KYAkv? z<^z9JFLRclN2f0@eIbto?9m|}ctm-q{`m8&U+P&hME2R@XCKT=tGS-Pl_eg3as0w01 zkXH{O2G|8p(V@`IgOf^Qi8nJpqYWMmtaqxvmB9q0uG27{+_n43afu@K={RhH4KL;L%8=b?88wH)n23}Z1{qw<@JvNqd)E8Iwar=Pkk7zt&a&^(p4 z#tnPHkR49D4(9(P;r^(d+=xX;+CgSg7vytn-z-X8{#AxVZHd#yR>7Za3FuujDav;8 zbe4K)htQd*+FI;1yXIRp#HpF2k;L0R^=VheFEp{qpiSG6V0;dr!Kxy%V4I-C=KJMw zQ%UDBWUCJ-|IBf@0cT8#1xaED$;hVV0XAt)Xq~;)s{Gz2Sjzj*`#7fDerK0bKM5)Y z_DSmRSum7nQe*Q?s9*8kU!*%|wf42<5d*sOUS2Xkv{uX>Wp^@?{lIlrupY*_#7rG7+_STtyJc|SEzK`Y85e+j=UO%*9N4Pm3mz=^X zX|7I=z5AEBVGT%zW9Kk*MF5f0&!&V*4On@{bnGzBngPM#Z*5sfOu;g$S1nUwZKD)c z-&pN+$XBGlGLH!3zn8SSy6!B0#f39%P4Gm1#x>CLNw_Xy=}>vK?>$?Cih`8bv(FF0 zQD@I&Hrzw-9T6+{F-VUw%< zUncMJ+21b=x8(i?jg?WUn%7b1pr+~9)8F@97My_g{I%XO==ZR1zS?FX}dOfY` zvl$D;ZXaDeQXQ*E`I=y6bya~BC~?^XdIRU}^y}>ohq2j4n;T!b!Rg)O=~nB8yg%aO z^%7C%u1Rkw#Bj*DXF8eZo4QNbBdPnQAHSBS9p{rN0_?EVC7s}(%7dUStmQlN* z%n|ii#8N-nctzFryq9aYk_Vcle?^61w73?2=q9@UwbCN3-%3mC|LoYUw9i-|^kuaX|` z$&5M|xIu7?1dCwPm9hl_5x{Bb)z)HCzs+!cPwOD)yRJW77G*@L24kxK8_Fs#1J5}Z zd-38v!=JfRN3`MwJCtfs!EHo;(>sWkiDFu51Dj@5;!Iv%aK=2_ZcM~s;?Jn9Y8aBy z+lW}b97{-*JijrY{mP~8S-?bmR`-G8;p>iTri^zph1~I=8pAeO@a8?EnLbq>? z+K?;)mu@ZPD@?k*50IZ`XVmDe$E;8y!Le;3ke&xIminH|x>tjP&j)XCG@|%uaa~DS zP44Ns45m`{Y~%y+X0Xm4VcF6S%wdD_r9UY` zw5oPM=!nk>Y>s4zatvqgW?p42XYlP5*i+BX?mUSP)RrK5k1IZ%O?b}d3@0qx>%lA$3POU zEpR%5FZgQS1)nFZymCR*y=b?dv!TMFQ+o?9{MKQOoj!a=PmcSjeo^gKYeApTz#`CZicjq9Z4Y^QIAaqr=>)j zoE?az>JriVVML+Z#|8As^F|wP9y^jU1U|d0nOVse@0irNo@CE(E;tQ`)v{vku>2+Y z``pL3Tp=1GLaO%OhVY+9FKEaxTm+&ZMi;_Au2a#BLYd_Hgl`{~ULFuXoot+vIBA9= zJojQsvyVLET*%>Uq-)P^k#!p<>_fafKEGzRvn7UezVv>XySME(C~SV{-~HflGPpa; z0!dSOvi)~6Dy8zmh>Tr=#S^n|oF7{jAP&O!skQ4jvE^yJZ5<643Qxe`f)>8cEFTTO z22&5oURDoUk%t>GyWX^GHJ3tgwJKi;z6d zRKK|g2}!{su1&9cD>26;IHg?aSuE@S%{Q%8F4IeXB2U5j{*e-8R%Ma3c8I16=^45% z>1|=u+{kH>3$#$2!w;%#)Ba}$t59NwMSOKmWi7X-%`81PM-HkuckqE~7~mIktXA5p zKH=C8|M-Yh)Y7-D{`b)k#D_uxka75s{EhL&o9)R6VP1s2)?0f#Zs5_cLxgIWt|I!q z@KKN|;-gt7Qd2eP`W}vnN3~K7hiBxmPsa~+{^v2cNif@Buc^yknVra|{?SUQL#&3i zhU3d-=i8ftZQJVBBL^%l;}XCUAK{AVY-k}Q<{=R(@gSlLJxGeebv7J6xc;?gb-j2M zFo-y`3gL)*{zB()JtaGXGMGyA-qiQ8v3%ageik1Th8KDabXKDYb6*1=ERM;M z5b1tJ$k3`A`s(DjQ3d)}?X?EyvaE7E?E}W;9D6y$&oJ2#)1=F#ppVjKjRScEHfamq zPDzBoOJu(anQT}p&NQ9J&XMt?=s8!0#caj_Z>bk$k@p_0VC8glE z`+-bX{AV)urYW`;!5UeuWe@{`Y7Q&MKKj4(&j1&;?!rh%G6sq|Q5RqUe)5Ml>tI!^ z$f$l{EnyOqspm)4Z<3>+!+lApbBr3}8{vn~IyJ&8+3 zNCd5BnmMVwWRgObZfp$+zJ2;_O4m6&vp;rq98sV%Q)R$ier3x2?-Qm`)XkE=Bm?St zB%FEG#Dy<0cZ`gdssN|LuT%*Z)R_HZ(m*=u6JGXx#9IuamrwE}eE_GbGx&{E}Z9J@NpxVZ2 zzmNW7-9vljsg$y31=tftxGX~Q4yu|vz)-;kI@4nQn=Z~3y0+v(No@MfzpJ(*`sg+) z#u?LxwFF%xemi)fy9!eu%eW1Us(Y-;<)-iFoK8Zl0*pQ-y#U1iU7?!_#Y1Ox<$fo8 zc`fa1W=0C^3Xan+j=LbZT81Ti3+zX#JlcdDt~ynT7>Tx?8>`S{SXJvMDS4y1z9OEU zo|Z+vH3V#5Pi$e(7T!(O!hPnv(X|pY@*?f*w7^Wz{=9rW?u+Xp{jVXbBsa=`{{18u zVsX5SV(a=LlCIyrZu)cgov`(i_=RlG8Mdg7qa`;cK2B5V*aY4JBptm!djY~Mo-N<* zwQKeUHDiGT83q}KP)aE%ADG%do36}2dTcg*$q~ox$tE}6u)TgT^G^5hW-pmKa4WxF z2xaD^DXQ0pre89cN_GDq|L9*qA%>NK@`40V-c_4&X9B2t54;X zDH)_!%eS-;n{1BeZU0qaC3`-JM;6i6fVQywztmq6NiEl0yc|KJ-^+Bw&U4s2>Q#IK zU;-3eUECB*X$5`p*hUr5(TOgTyaxu9mcghJk_15!iv(#jN;3U^Ku7-kR*Rvm;&k>8 z={Nd8N=3wFS>8`nC|MaT3XDvvP&cI3eJAAU6m`-rz;%bv!n1T^gPU$-+lK)LHioWt z8%5m;v+(m;+=ih3vqAGx!JB)lK~mOi{5)H6B88l4eEN!!T$YF`Q#f+s^8?wsC- z4RPgf9(wj1@0y|3)riYCJLG2xYwumF3k)R4^H+%;`@RW@X?&f)w^jl8+*LxdGQ(mE zGnw|9uqgqK?Keb^kx;)XIlT<0ygrg7Usbnd&n)B~B=sL`BtMOj68~lno3FfVRWxkz ztgCuG=twDFa3mA3DWd|u$i<_aB8wK@eJySOF225GZ;7?4b1(lsbS{TEpmVAR{l?M+ z!qpI|)A+jO1fR`d(eE3v47%pVq}f^e4N?PlY|A!jhQ3PF!auJ*63j7(*;IbE8Cs9u zAD-vi{CkYX~i@755MC(RQzQi*{niR|KIU zWYO4jigKQ!P7c)hM}v-a^RSgSulQ`iXzSnvE~C>e;IW6P!Gsj0xm9{G z^jzaqPiRZ;t-lRfDEvAtF`KWp_xBI~VQnVlRxu&cB1)B175|p;wFC9#S)$4-iMQB2 z%y7(D^f1D^abNB4Ih5og)2$dop!-kc-8^K0>x6VsW%1O9G=B)swI@$4L(~Zkk;ef! zI<8bFoV+!E!4Sjm29gu!_3|+(;+o^)`oLKyX)sDF$E7E^{|(Dxe@8v%yXY0*w2DQB zFEcmt`0(=Vg5(?j_U=KQ-5Qq}p4E~yQ%niNA!sM}9ZWvo$(j|1U!xyMtloz0b&YYLUymY*)ZdxL}=h6QzA zVW3ao>kR03X7~(A^P9i;y8ld0s)@)skiwQI^#t4XNia*w=`A+Cy-D4uPQd4a^3EQb z2En0w4#C+L!5dAj0g z?p-11Gn8KI?>sfR7%0ajL@F~1_0MPEYfHA*w>D~Xw1|(x6TE_-I31AfX+uJ`bIaI| z?~t2sDD0L;tE-ojZS`?OUqD!DZ>}D}c>Pa{dQIr{F_RIEF=PDXZ#*2tRmT{5%m@QJ z9$c@rIQrf62c?^FKLV~LCNsv!a;tQ%ZpIghwNYStJb`e1d4%Y>SpW$G`qPkRIy0dP zFTLmQXM%C8CNv6N-xa#XznTl<{a|9s%`T5ZZ3W>zf`RE|#jK+X^}jHW*g&f%Wp=Wx zk4y+pI!cDVHvABa+evoRRh+$7|K2Bx^r3-u@o3byeE9?gpj_LBbfi;77 zQ{%A69mR)Ia5&t&B_y()Y#dN^op+Mkt$#e%rZ5gBYXX+OFAJZB<)I1ZqLuxUJ@set z&Lfe5^}kyyEoc3{3T|~lZ#KxU<|wy{i2}8TZ35VYJmAu7xC{bmW^6k|FPuWrzSigM z#lb1Nx_`a!a*k6+(&)iV@~@P8*WXxliM=#EH?(SlT!W>3@{*PeC?fI88rCqKew?q(6Nk2si!qsEoS5)U7_r%ky46nI&)%=S zzH0eN=Ia=pm)B)RAcku(v>|9-pk&!7;fHhyDU3VG4B?X?ZN2v?F)J>icwkvI@3>(^ z@5&vtaIko4gbR;-Z5h41!}%MfezBYNOg)%XbCS<#ai0PC+eqB7>4Wpd9uVo5*M+0! zbaG6tkee1;MkaWiXs++4_`!c8F;cjBCzK37nkg)*?0~V)LqK>ez$YkKVf4vXR~TaH zo!5Q&YD_-sGU-q9>2nWgyEWbyhj7{VD1PT3Q`(FtKvCBmF$2j^u+sMc?qhl-=HzUF zl-t6>p76USQnl`ScGJW;?YjN$(q!(DhE$J)~s=VKB_ zq-2)SKli^7-7AJMolO#3nl-cw5ceGq=mr9t56h+Ip^pxt>-k>1eP%3>%}js0WqS5@ z{GD3F#r)jueCx(^U5D_CJS=B@#=?PZ7Hu*!W6yJ%&6@%1_u3lcmCt;9V^uE1*LMUu z%9J=VgI?O&vh47&=WZXIU#VZIWE|;%QL@r!y3_F+$11t`>9XxT3f1eOWF~xA@ww!f zPZ*BF`fEA0CtkCMzO|Bk*P5?VH^tjRL%N{r$xn(Ci!Zu}+Bgl^G#}DZBg)sC!}Ub3 zT`zvg_I~GgIhOr2l5IEMXfFTlK5Kv{4qStbk5p$WM`-d7mXCiwa0*p2 z9xc&Y&1d;W?1~p)EA@_rvDrCSh4cQp7>4cfX&lExTUC)#e{w=XmWck}@IX+`!P_ z{Ba1nmz=EMI`B4l>#FtBg&Hg5DP2B|6vY7%zad ztzX>hZa=)q<_%n2XUJAiJ1TdEE}uW1{rr0anUIS#rM>+phWR@gC8uKez}bHyHzLdX zuOt7H7tY~Izv2Z4g_uYMQNp(sD@A_)5r;P_v~G~zOm5acol>qjI(T=PT6VIXWou$G z$!Q_J_Asm=kc8=xLelHR`5HV@B?J<4wCP1Pt6a!|sc4<}l%@8jS`766^U|aACp5#=+s*F#+UM8a@02W6YC~miCpxJGk(wKuzDy4s zq*}>MX-Y7yJ8?yMTpmp0B!wN3=%7UQm`jiymtbWJzGi&1JS3x1=qh)V3r z+UZjXYTut~*ni+_uLz+D-p+%UU6pSxZwOtRAJ#tg!{=@L^Ov;)^)dTj$uU?Q0r%ku zV$$oe_#J=xrMK@MS+}dnGc*f?p}JfU0SHzf541ZmLVT`jdL1*%=nA9jK>B^&a^Caf z$omEx?{IUV=Rw*Pt0^~(Ci&_F>)BCtwr^((7FTI*4#@`M*R&-=i|kMv6@*iEF&h4?r>`{JaSrlWE5b zs@0rPW4nuQ?WUjI5-PBi2!88CfO{N>uc9=oVx|)obNnGKZ0uAdzYL{cO;E2;Z20%? zA=3mqEvnjcrhr;XHe|^^D^I#kKDz)TpBw>g6Vtwud5Cw{-?@tUp1JZS8ru3e-~aok z``0Ay{l^l@iP@BHA22c_8^C%WGCNbesJz7&$*1zv%+-&y{JG!qC8FM79Jai^4Kq6@ znuupfp^^N7&9Vd3)+7w~^49%rOza=MQP>)T4&rR|m{EF;n9U9Sy!ZYnUV)Nj@TD|t z%c6bBv{EA((FEhVq`0pNmI-hH_2-7Dg1l?xk=w_~0qeZW*?kP?Q_~34`i0y(i>swM z<=V4u9(OipPB z_deLNmKAQ-SHFJ(E@QiD3Cdu-s*2+56-&RDK3mZ37>uLJ*^+iV;k@TyStAEqI@e6w z?EXVG{6h>C%mZf)ft>3;LkzKR$VH=qUQr8yVw@i4hr1PYFx>WN{U=xuzt9DzwE3?) z&`&dCuv(v8u?8x;zq=Je#C%|G=0H(nseVzM86FK;4jM?T!wPRe8-WoTMEwfWBop98 zHGJI|!f5}v=T)UWbMch^fjyrW%LECGoKjgA zW}-xg=WL(J^);{7^E$=;h_NR_GX!&XMWdbqvQ`#XrXO5uXK!0p@mr=1i;A;lIxPTd zFwIzKL{Qr>o4vHB!iw5N+PPx_hPWK} z*}v-kyC`Xx6L8f=$7=5Xeh>8;XSU8fhu!E%?=oyW@4G1aS|N9Q8jr;j98q+%KNbtql=B}(q${pxA3-CI)M(ZY_w98Vv0a*zaqwah zui!sE#G+BU|LYXoWA+kV^s0KhQ|qJ7<@E2%!BC-4=U+wy>N@6QHG>jUIO@(mpGb5b z)D0{<%9(KC{}+6yYZB-!!KAD4(Y1y~H0$yA9J!tEZ3uq0#KDd_qpCCWgBTj4#1wxF_on7(dV>>j|$S`vlEsSNUqFcJcpGE{X*25E2&3i=Z zbmPLm|GZ@mZ^%ajM)n4V{#&iiYa~q#PJsSl#CK@qBh33{?0hB17h2#nxF8%-DRi%a z*2LG%efe#l=27xQufx~OE+xuVOZE>IO><)YmbS60jsjz#CA|nzS?Q-3#&kj>0zd1Z zP>^J(3zk=}6c&)wWRbk!AUnJ#VLSif?ac|R&b+(+hOHTa;cYuZEmqqAPq5f+iqtHD zi4KY~LtaV|0{;%dHTOqVBlrYw3QLgN$mQoeW&XL2~D9f8!ZBU zj(K!T(-t6b$SeZH^QVuwYB|mm{6m7to@dbJ}LR@%*v&M`mx4(+7v!wind3 zuO0&uUrRctainILK{)9Lc_(z<89K{-@2xINJ^xZP@)r4-pdrntI(uQwB@**Rj2}?O z0Dx#G)H!c5+8H`q_)I2d!e>DQcOJDRneuLNC2t!nwypqD_Y#&^tg;5_J^H`tmN}!% z2iN!qO`7A`P3%Kn$^!tr`~fB8<-Nld3iQb5cM}EUHk`xmQ3j2=taS4iT-SfizS@eB zI&|>k07;(~r9sWk&js{;^NRQ<+Fh5JxNXaLy<9gcvji69R{*#$IV!Q~PjCFQzdW4! z?Cere=G$g%sW&t)YF)l62;$v>xMW7)uxAyWSL*T4taHm%AkBOm)@v#U3n+_BhXQwd zoip|GR9#Uzd%vC}S^_jy52~T;yulvv{tnX&BHz-9Zpl`&YhKsB?SuQ^qqj?7C`sz? zLkH`;EyZ{B@|QI2>0VX^w;%fwVbzWyBhdc1x)n8*rRi&7%x)oL6a?7Dk;MYW7}Rdl z`6db|;sq@YkTe6=%Pl#@ob~2EdH-W#(FI`4y7m@|K(ikl_#W+FA)Fzlw>q?Uu!y@Y zOxSHJ?}nFHYMWG@(9*5}YGB3Lo==F62Hz4(rD=a5-nvO)fEk&Ndl28o)9s8x<3C}p zV28n?r~OX*SW|b@FL42QD=2xd2EQ0h@joh8Jn*fN?=L$4%Kw<4=`=xeX;yJX!GbsL z*!jbPF~+v9udH`~=<0D(=Na$+v^IUiT>ofbAKg7|TrzOA9}Ny{goz7y&Z<=FvB{njy7a$VI|y99S%syb$}9QprjCvX z&=C!{Et}^DYv-WxfJn*KeErtSOxQ+bvg@Nffq zB2r_ez1~AnDKhurnd@B@|0M&<*$?(#iiw0$Zy&QG&4z`y8eYZeU7BT3bDJ(S{>ta1 z5z{uA9lsy}2}v1`n1g$edN}IJ)c1^P20|+q(+vy%Ql*I|FtTO#9nTFMR#pLubb9Xr zKzaxsko3iG!*9S5(VFv?`U1reO!=LPLhp6k$outpWciT18~P9w$RR)qX%}A+Mg?`h zjj~uOcxF@P!QH8j<{xC7`!1u6ej!^2wi3c629ki~jd>4IB=q}S4)^*R6^dk+@;+=< zwcL*l0P1mw0g?CT&^(Hsbb-dFlKZe+<=h2iYT5b}0>2hCDA15SV)bs! ziZw51RGtdf7tc&S0SCw>B31vgF>c>@dl2e+G=ProNUaPUf0z2G;F81_2I3~LfKwLq z!PPCgH~xbf3Mz13D(qhysTN6S8tMv<;yR{Pup>(W5Lbu? zR}RBefAe?Y+xou#89HJWHhx>#rzi_dgDo70Spm2ox$>wDVVcNX%!5n z38b(cZSL^`2}T~)DcduNHV&s~9lo;F*-b{&=wo_yidu9BDD0oJf550Gz9_ddUcQlasm*NgbxcrkH_0D7>A2H@Gj`?Pil%M(v#xpo%wlTG+yoHvl zrUh>>u(-Z%zb^}{#GZ9DaFN01l-a12wR5Xd=Mmdzqdrhmq#){Ur5K9IHX8&d+d#UP zLHKMwlQ9qh_=doIlUJ}kjZ%{c=pyb_qj4DF&(nr8)P>tgAxv_0Iykd7;=0C{*5TN2 zCJd=QP`n@AZtsr%4vLl-`-nVN5(xcZ1KObw$qzirGh&f}*P@_zcUvNFyC6Ws*7VQe z@F)-0t~MzE?;KPgo*aCIuLq!rWuX4V4G*qb?zn>W>R9Cgr2i8lnA*%Y)T4@j*UP6_ zRjQ%kpJ-8JjQ5`tne~7~YEW4!KO2bKUpn|Z2UeHCKVo3#88N0Eq^yrTwm3c&r~SM& z>KGgLkPk!vy@TA7gKzh3A8EJZr%W(O_QizZNL`+N=}__I!oULBFWE3asHX0I^O3&K zDrwGdBjYRxFb}FT62!sgwonl++$~EoKuUpJmv?i*Qd3)R2dzNH0*V|%s`Yb}#=U*O z<&wjsO9svnCuC`fX15-n889pT8xN5O=Oe|1dABIXF!3bypG5+pOn@V{9D}zXcy`c+ zgM>kUfI-7gec}-lf&!c08E#x&A~yJq6aug6y#T*}gTG9^*9Mv{aL)_y_~Lu0yukv0 zDqovnw3}yUbo!+oTwD+T+N6-)xihEH%ln|6s$&8P(EqxJQr^+Hv(6xdBEz$2B-bHVEk<;_r9WnT_WKFCobSd;~d95W;jAlNoxoHqi>4s1{3TD2E2hYw77(K$^w{N6-_vHyL0jQ zVzgh;TWXF!n7rfU9Z-Bh{>~L9#mu-F4nVgu6V1-blNM!K+CDz#-wAu{+Yj-~z1}M;+B6HX2sPtZ z;OQY0>~bu=vvx_|$7yMK;4AI+OFNFrSKa6C6$egH#hx*suTM(nTKRET@+T zJkm7;C=`selll8J?vUD6nS=K=fykcIuu|^TQ)*qzg^88%Z=Wo4ut`OL{D{xm!^G_< zo3CPYd{m_He+T}HFYP@%P>iHXTC|PW>{65v4zV^y2e$~%vkRf_^>o56%Va7X)io}u zL>ZtM6D!B|#*`328|a#IakP}m`espwVsfUh*k$kW4E-daUTrkI!I7|OeC_uSYs}P$ za6w44cmrS%8!EV}Bvrt0(SqcN^|j6R%A@B_9&b&^Xf(?dGF>F9y31BYaJDNo!P&XD}^uaM~7!dPL*W#Ng(~lhy-^&EZ&%0 z$i3sc2^Z6E9ALxTV zeCnMID5i8t0LC(rh`$G4{yDgZyf6$C^DO=rr6Lp#tr z-Bg%ko?XOCPaam>$Le{v|Hb~Oh?zD)Eb_=LLa(~mrWP(nyIfc;#&x5)0@7bL{B5p!1k@7M{(2+Gpko;SREqA4n1Rb5#Ko; z>^OkSI7gTOA}ZUEb>gdeX=!xy&F(USV2Vp20iQYhjIsMj2l^is3vp*H1dchz8}&+=Jz&p) zg|ONlQ6!w>^Uw9|J;F1f@V@0ZL1i_jI+-}{o&-RQw;4SXzn8qd^j8)4R{a|$z9!j3 ztjmOKx|w##?re8?Btyduaqy1QvTdv+6@FDC+ev9#@_T3`IPXteyMr zC$_pZUZIyRn>zzwjn1As?I&!MhH0N4KL6%{#X{xJpbXzMbgm#b#eK6azW7`hjb9qJ zE=4;a?ii&yXg2E31gERTWF-UO`jlo{UF#AbfBS!J=t3*hWJxi;*P9X{%ZwM;vk!A^ z=R*bV1D9H71)6$M?pz$lS*zwpQxyD$-D78zF;F0p|J8Zu9?>k@Eg1^B&}k*zj3u=1 z5%MA1?l}sd7}&nKZ?19b$D@e((;N|&JJkPz=*_|JY^BAK*2^QX$T93V*f#W!^S5?&YDwHbHNV3HiX%?gs58 zA^hd(C!g9DKEC4XmL1V=bkU_IA&29wW<}1Mrtm9{u!?>n1&g1M^K|kA<#?;l%g<#E z!*(6{`Ky}u@to5WbB_Y%XX^)d@tD?5fuyH@p-s{>KFour2@+&QNv4q*(r(l`Rh>oY zh3+B`mP4_yXkvvww~pRCEHOzAw5^aJD*4w8aaYZ@1ow23?$DSDXIuV4D+Ru}DI z&r(zi$Iw!-4bHOtkLRxgmlT?Sy6sLcGM#W`rk$ciX#dpW7cni5OY`aJ6QZ(bebP&$ z1(XIi)wou?Kfhh_ira7OC4$q)(^ka{C1-gK-2Mx8A8~}Lts6GxDXxd>{j_*0rBBEu zoO#6ds(B^p$>K@8mj^bWY$;Z`x9pv*m3#40^f7u|`WWpt)i?X==RVAC5rIzzj4kI$ zRhD^Kq>2eBhpEw-NW>hqvpDvprPlHKvpA&=9F`^4;I2VI5ku=SD;8EJvN)*Hz1k0f z#LBY*C8117DxP4L%yL-uxi!i56qH|b6((xGF$8wnhuw9O103eFhi^LvEXoWc^L&5K zZs~jv_Ze`DWLfl2oo+7kHMnstS^kjq% zTh-<+&;BaW1dKS*tlc6Ab*1aub>dV(NSQv<6F1C{d~5y2L_F`gyoa5Or0W*+_B!7H z%1KVWrWF~8>jnJQDeK|hzvs+Z+-^O62Kim+rvboAd>|>LSW&AquZX(0k^#=DI(C5I zQpm?i*M8EoYlGFE{>^M4Ev1gfUOeud+v*oq+n5YN>L<9lVDcwkfNz=a4?^=)r`h6M zTgJ0JF+j?+d*E#`HlQ?gJ2;neH(RG~FGRIA91{8EN9e6~!07e48}O(ar+jvA?4#i8 zZ!#=Sui=MT4LV~5D*b@qsr0OI-zYo0-HP^Jd2#ccyt+vtpEZd2``7gzUU2ke5B zK38QrqS<(rH(x z+iZ+WBpM~XoA&q>OxzT-iC58(rmB;UFl;yDsLg%RCwPn+&Jzkxhc$AGL6P`?(S?^a z(V$_R<*FHNcf$Y1Ot^`_1O{e|DmO4P^@K%E7x1C@q`jhxx(RIwC4XxQZO6Q@EM`+4 z-;Eh%Zu|Ol@Ben>Yi1w@BBk+eQP}%cWg>-&$}*Rof;s}PG$CU+KvmP#C)1j0FYcah z3!Bs$tWU5NAcrVt$jp!~LE>!7*KoQdwn8#tZM#mP4$|h~NVQ2Y0EHdK96ef3Ez>Y4 zt+rX8J5iGs&=?^WmDzYQ-TYgCa3kDnXkC zy?awyTrnqQ&%!mB0=+}US(o=19CC_3-q=gvmmMJo1DK!cwA&w2qig+P;$gvg4Ua|D z9Z~#-+KRbMacK4$LJ9?(~SC^4f-F(7Aa@U2?9{$SKDV5|cfA^uiFLO^Q9U zyf-d0O0|tivxOYXB6H?kvC!2qLJWU3LZ?g%Kb_V|Jhl>tjYX6T{aAXkr2g>&JtpSn z=h!oQnc9g)?)QIrV5^|2GYM%&y%@VorbOfBD(0$v-SzKeuBOXFNM;s29xcg%{Fb+? z5bkc|`5W%7JMqE`BP*auD&!pz^&<@zh!k^LkdE8;3n5>RrGA4}j`amEhAf)3AA7bD z@*{+zVo91aTDYQSv7s44ALwwfi2n@_ax5*Zj7y;VdiCy$e6%M*pkQcq!A$Y~rysD) znc!{W&M&K&4MM7^^UK{`C3&$#A8!mL)L?c(`|HKPyIRsbQi5AYDCAqob0)|th_B0X zLMajvnRg@$IcYtw0wKWV(x&#+f2{WV>C1?ZH*AJtVft6RwsL}QXz+yIf2e(H2#TPc{Xz1Elo?lhDy>Y^z`si-6UL8nm*px(rq*=!jDg8{ zb&OX%gVxxUhFNl`K7#!hwKNx8fLwSIBXp`TUD8;USkHX$!p=f<<+lp&|8S=L|EHC& zjEXwyyZzIhN=OUR(hb6p(kUSgDj*;s-7|!sC@tO6(j5vzh;(<3NOuiG4Z|IuXT9s* zZ}-dlVb+{A>->JF)~s{RK6~$sq(+*#d~>`XcS8#-4iQ;Y{D${=*!W!U71^O7c`=EF z-Ym`4FJi~Wp}~2Pi~ctm<#`l9sV|(%&t-rvsgc83XVxk)U}N(gWSkm}m?f5xzU;}l zD-dX!8Y<5=;Fl$cjajzgmu1tmz6Bj%*O30e;{@P}r)3p-JUBY9(C`g`s(1(vQP8`E%?W0vPf*G6OeqYUs(Jo4 z>}+7Xpr)nbk*SQjC~87h;9n3``i2D>69C3Z-skzpZSOW}$6BjxvkwvA6Jddot!Cd} z%X$ur;;mT9)EU1S>0eYg=(6^@AG%}pXMqB=1;-$sf`0bP70ox$4ovI3?!p_WQ5 zC0e87mvTL?6ugUJf8QvG(=sTo&YhXt*;V#uBs3=ddT7uxXLmgGh$XiNRWddE#EBa4 z;%=+9T*K9es8mz!9+e~-k^2eO=H83l1`#*Cmu z1y(ppYhn$`SE(DK2ZiIs9y=?|s&O4>K1s+e{hNE>y>Q-kGS+jWTTW=>Q3&a>mC_2Z z5M=fsxqU{8*F?$gmFMwtouxVZIHWC7F37QnY3r|K7YUhEs9_bD@v#T8OYR>vT0 z$?WHdKyyUff&ap-hV5O+0%Cq3fbwOptSse5@rxlG-La>9{WlIh65wp zfZQMxc1DrOZ_Ka>#h7QeKZ|+B<>vPLI;Az59;Tp5&+nEoO-x+IO-vUr6Q}Bm%snV& zo_EXg1zMonxsLtHJR4}{mc#J{RHYbQjUjw{E3r)W(=Ii$-%$P~Etdq7)YV4k{Vr&3 zqTw}PPRQ12el_N%d_}>NYG41~XBC_2L-na*EY1qE5gMiLhTQNmbpQQ~n~`gKWSQaO z^5k!^-bhhT*W|vy@O>gdMXvE9(hQ?7h7wB-Y@L$T;rj!CKaU!Tb@6ooaZRGh{VEg`HpR3=17T-=8TSW$62CM zq0-tJx6j=Sp>L#u&$Kod2LC7BKKs&clI&9$^k5fD=^lM8020*X`JKmgk6nvqGA*+? zwa;uR23a&G=2|T z6Bx=#X^+W7v-JSdO0rSB=Lzh3(EAd5l-j2Zs*~rWc%NhmOA?|ryl>t8lsO#s7` zA067Gp2_$}o;}@6AFNMysOSx*WZERireEkmS&=H&{P_=Zy{gRNH((aE^ubIcN}bL% z+G?DRJDdFeRsicIZj&N!&n6LS?NfuimeaE<(*cnr2hK6QoT6th7!UP#KORH`8Y5~B z?E3F8HC*Xg@(LKFzamP1Y;QLTFUsgtkGCoheftHY>p2GhvPs$sDvacpWY>hYG$$Lo zc$7twO=xi}G*Z<1{CyH&1b4eb!&P2AGUYp2=?gk8?v5vG-R(Ec+lv=WDeYf=g4C+8 zAg(C|t2~v@piJ^SQJnQp_i7WIb{mwjHiF5qJkCxZ1;-CM^Xlr*cjA4w^+M((8g8q-P)fA>`8V1yx%vsZ1?qEg@ymZc>^Tw0Uv9H zYpUW9-6>c`g?jYn&u(UqGrXZ*DzjXlcJ&B7`X3TMq*OZV@->|NP+~lO%K3Mqz}m{?PuYvHDkx^bQ7U@R~FzcxaU1e9AH>K#82YG?|L?)2dL$B~$?iSiDrj zztNe|U-k4q-h9+wAMe0A_MGOrb)fnLG%rPavsq_iH#VEMN!ElX2K-Trr~ZB;9nR#} zlpSk#y-ZeX--gU`(cdE+>W+tg`IJ2O%}Ax~uJ#$9g;gPIoKdo3xjV7YPW$v1TGSW2 zou-cc)tmxTzT$gLnDoUctX zUPs$JHzck>h97hxR9kQ`Pz|m!s`sAIm#1VY}#ZYi5RmyyuBPYqkb0VwWAq3_1dh{v=}am}gv8 zpJOg}nzMiUcm4Hm@U=PbP8tO@F_>nAR0t(JVSp$+(Eb$PxqvO}Qu=9WGPOXleckiN z-Xh8CB*#VX!S%X#YK?l<8@A?G&&54oMRwdTm|5E>EARLBdRw;a8gW|`i}+Ad_(!yDix{i^(KjA&eh*)u9^x+x|4q0&m6|QK{CjMcaC~`?GG-y2 zIkt&wkel|dP4|PHWsbrZPoDSPvi?4pA&JcyZgTfmkikk^PZ{u{dQ|X4Dyz^?fp(E> ziF?VvJo-6Wc>1U>@OQ)0Y%y6)&M(cH&*i=zioN)@I`?3mxvJ>?#BU-(E`dCN*0}Hxj^WOjeRGh#hvTfC-~T$3G;Xy@p0SwEZuXQjdRDo#hZ&--tS(g zvoEqZUN%*!Ig86kYyUY3G#@C#ToTvt+#~x){&k9;@!IU0L=aU)5-UAYob+(1n0Ylf zKr52HSOhLkdaNnr2)AcmMf5t>)x?u7pmje>X7JUd%c+Z+)4XSy#(477P#mK?)I9d9 zY1}gmucZPPQJ*yRdVx|c7YD$*{zFZ`XLjD!{*2m^T`j&`?#$qq0B3=N-2#cxhqBFHHSeJQdr*!vMk7k*?8)Vw@TF4cVQt?Ml#u3p5f_QheVWjbHDxZ!wzw`>aD-gkw=O;G?(v z8q*g!M&lo_aH6w->Vcedn-hf$Mu;r(`|qcDb^YoZEjO9X;`_S%>l794m;pz`5&p+D zld@j;EzCW65V{ zgbA;tQ^8Y+6(+HR%eQlKaZQIljro?16Pp2;y{*1VWRVG6-Iw+ z65AKG=wp!lK^DMoe1kx5f}3p(_=- z{urOv?dy(yU3F5r^WdSgNVeMan>0|`OXjW^J`xS?xzkFL**8*cefg+cfSSK`|C4x* z#cb&d%zy)4_z*_^q%|V!fM9{XPW4T7e-8Hw@_keC+5*R=&Kw0jHeTK_^qn{|I_K`< zYjM-vzIC1Oj9EH0Z~afDEa8TYPbxTVr}xg&BEx)=7=TMifIxpveEZ`-Z0@=<69t+~ zwkFkg@sl|jWoV+WH}-S609Oj3x+?}h;U+`-RPYJta*`(}niDT?{V!S@ZpmUdGPIZ~ z%Mz%7h+x_!ijw5e9mjvC8->T=uJ;6`eb>DdE!HC8+Z^yd0Mj46)~b;NKJ9W7K|>3w-DK)FqI$37=Fx_bd_W z_(hM8aaE*Oh+%Nr^XMGqaNf|`Rpb^_ni5NIw@=MgK)>e$d~L7GJ&S=#w!(fkezG#P zEbQ{;aI$_?1zoTWv|^nG;6iA-Xw*s0qYl3EqYy2>}iB>9FD%#(#ANn0~*N@)2R6>jw6RZPdF9M4rZ+0 zy12CzM@{}UF*zhkVf~gI!6aV>G@LD>HCg%MAFm(f zIyXiBTLotDAuC@08m;tUFcI2YDP*v<7jefAjx~cs*x;^#%d+GGpQm(q1(U=uIX$KW z2G3>%Oyc+~`fm!BGIr6Yqm>EP1c2-}NMXymH9Z=`cs(|C3v^#1GWLK{KZrp&;|J8-@8m0fs_ zo|Z+<`aW+N@B zP%!;m$6@C(NcZ7X*W)hN@pC1`KZz;YU<&WNYQn9v{eI7RCvPu?p-? zNksPM)sI+QEhYzF0}HIY=4qrd%5LVlD(r@)-K=nsaQSA+1N)Jg%PLRwKiRDphTgq5 zCizIy=*?Ags4Mzt4Kmv)lwR#3R;sW=jWI_+WyTPfE(vn zGmODxr!|^o7q?8VOmJI6X8eQ0lvm~bh~1MHR_k0gMWDz6i2g;sQ@mzE(Q`lfo%QZGm=BJO9$gDDyec7X;(Pa6 z?Va{C;@7o#ba0bbk3TZ^`rOFqP^~)Pq#-V|m~r`z1^;W)4bYlAfYJUwN|TJg|6DbW zBevGjCdM>_mmJ}Y{ z>TXIqP+YE+y+O7VgDYGbBIJO~^FTKELQ^W~gnxTV3cdT&p~_87p4juVb!5&8x&bE) zxbHO*^=UyLr_<6z@kw7@eb7FTN1Y}Z2VAuxCea%8FnVwX4ME@Sp+>=YoAY_(<|PBv ze=T+CZ_a(mlR}_d8q!)kA@0;IMZ|;mCG$|)g+Fy`{$=CWvjzyr+47Akv%j1!bIUIp z3Oc>lK1jJj+)3G3Gn_l*Ph5mVpxN*?8CmmLTrxiw$sDe}e&or!d)B=5wqL%!;5mea z<0}WAKF)`4XMcR(AAWpzwITwI5w9DLVD<^M@vMwI2i&57=frLmqgEGpQ zmDfjU?v3?D&TgMobfv_Tg#BcpJ7eyAu;6oexl`gRHpP6M;@ee51((wy`9frXWvSG~ zlD^O41urz-&}G;IXEi=SoF0NQh2V6jHdhLB81nC~vlR;{|9K^-J67s+U*_XLR&OSZ zhCIXyN~C+K#~@6Ar2N1XSY0_U`TR#?&>!mn``D^yKN5?-G|PPI_?QnNN{1jj=1du! zHgThl0^4Gk{wnGxq^9`DE$xwKVCD@0EMudFnb%ucGUiQI5=Yc zz69|*qZxXVYB7>lO`ApptSTFFP2PuuGEh#7R2aG}(@JK_myw{oza#yOT583uR0cGU5p7 zZ8g)udWhydO&1^(6kMZvS@rLLq)||jVlzdKD7X2|MU62~8L>IlO%qJ*kqNf*R%kwz zJvERyTx*Nd`YnN+*a3hQ!g~iPB&@LKUgWwYt1#mJ0TqTM3yIBpItV(mEE&CtJ<0d) zEQ2%;S1yz@M`?8FoTskqJJ^wj|c3ExgB}*7ip^3F^V;vxa~qr4Qc(=6G#U zHPirvDeAt`;0mU0a0PNCmFE_%k5dw(nkj&?%F+`$0^>@{HJ~gK&Fqbj*L~&lc~8OZ z>8`;wA9Up8G-paT}T@|^pcPM~nx?PS{KOKkVx|O6_^X}Nn z3MWeR=~z^CgPBN)L3ur{_w__BfeqT~W>>kClHwAHxG&oqzS&O`=;S|g1l)f`Jh%L! z?rgRDf#WxLmcUfnIUr?JQb_Z{drQF^{|`AsM;E7b4WB2PF+`L~wUb_i!_;R!t^+~N z^6>A~kBow;A#6nVb7_Yo=GMW8xujxFK4fkSzPu>OcoTT#uys+$^J+td4j7*>h)G&u zj%7&E{{=QrD^blEa@Nmx?6Zdh-FzEHa%lIC(&zVXTjS@*)Vpu>*2tm{v5y?59G8tHH)%7AnNuf7!|Wy= zvtj==rRCo1%;T3pR_P?rUKQ;}H+Zxwjd?Du-nfv8)%LI-Cr4cN?$CGD%eykeM^BhD zd+vV_zS2y5f=b$W2(ce!n03r?_H*-x5&6!NX1*6|c5gWtFnu|l*NhYSMoE3lKOuNj zoS${Lwr#o7EU8$xf7~#ZJj*X@BuweicT(b)CJfEzFe=|>BiJ6BM9`9Si_&;E>GU9p zidoZWK2=bUbz76#ZN{@fnq>aL;rY;)WC}HS@ZPqIWi8kEnFh}RO9A9A-&}+^yCckj z6>qEJrI0Rj=Jj1Q%-9l)oQbrx6$f^b$bEK*4QnR84_~v2B=Yzr7GQz5LzTshigWlm zDZ(B-aLIVaU!Y1I$K>RQ<;#V|y&HaV%&$T;$l^r4x~0h+ekmVyTGo^BDJ1{z#yi`Q zJF!egv&LFA%~{EtNP(ow4D%wpvGLd!u|2m2z15AkRNc3E0*nElpK(^Zs7)8MV3Ie# z>Wr%lF8UU5CHfW<5)wyB+wrT0R`?V7!z&;2u1KBeIb6?Zn{KYmSbCSK(#O<{V=hqz zQKr&;E7@F-eQzK6FX2HXvfgjo=x%mkuxx*Vvs-i17q&w>saEU!(Um_e%$j@i8_gLP zM;?c87r)!Slo!3)jC8~K9y}3%cGN7tgtpuDojHKT)(W3c-?K|&lJns{-JlOiFK4`E zL-)wzBk7KI-*f&RD%Y8=Lu1&-1+^;JmQ!$m&_^_qV{v&!AwJ_txHhk!awJUKqoZKi zxJ&cs48}%^Ju$&-Gi@2AK-?lJW>7fSZ~jgM%xr)La*R>^kC7xxjFlxUVw8VR-mqs? zvoSJC1Z{dt(pG)Gz`SA1d73HK>+_`XN??})xFcRzEze(fh)aVb+yq9AN$9tv7+#CA zY!D7gXt0z|5fSGHeN_vUB)~y&J%2HM@GCU_p^{~Ct_FFJ|F9ZNh&=o$MMOfl_TkHj z(XP`hr}(%%67k?GXG(3q7^QRDeoIul%MaP$aGEG3;&w^ivkQ%#j)a&%8G8TgOJ$V_ z+s7ZC!gh)rtX&ajvY+bF)XA2W@Ux3qS-ZvL=lR~-qcd^AF7`c<{!2>ciND316{Y>n5W=nxtn{xN8e}v9J(Z-FK77b{I&D5B=+PKf3w|WY$(M>mr+?&UfBl{ zL0C=iTM*(Ktw|9$PMP$V0@sIwYj$tEGvDt=8>3ee>T*^p9n?Z69!U~m)$9+n7UR_- zX#=M4f3R*`(ib(UUz;mZ$rYX`T>F;N@{^%zmomgrT3?`UN{Rlrx_;PsY=nZZ{>nr$ z-8S7@S?P-faeV_=xGBP`m}5TLE6mMGHb>1iJ8L`^J+s`$XbZt+IcmN~%rcTxgDyTe zF3lk;9}hHxH9~ZjryGGYmL>XT{Uy#66?q#0(Pl%Mra zvyAlq{23z2Jw|2~vE z?zV)A0~_rpYeqrO(j@;bY(BE{d8z11^ow2woy%b{lH=)1xqJ7+@m(64rAu1_7IgcB zoG2sWh77tCxwF5yx<8&YkT?dA8I;6;e5;O^58eFMK7T;E&WXAZ33H$Y3;gE-|Fz%L zUjp!0CYT_*>Ir>&s-Pdju!h}CpI&*CdQVp*06-dSBs~zHNyyFfi0`A&q@fLwfj^>T1|tt`Am}?wB4D2) z=YIGtXnkM8!Q&#r5MBL=44SRbo^}1-PR0L);A1MDt-;BlHx_n>EmQL%UXoNcZ2uVK z1eBrr(dQ^{;l0c2?U2CkT}RTpsKSmr7sP?%Y&6xm;e@0APoJIPxO2<_f2#?RFGpIs zXv(_Y(N?ZG>e0#xPxfS?ShNkHt$j;3j5$G7=Fx(&z<9}Y|U4-eiT?eahUTyz- z&PvePWX>=I{ojEz>Ba)4?c&#MOjRxi>Rg#9kuVxX_E|v^c+5> z=ET)QllFm!mu;ZL6p-eqkJS2RMXCt%F|iZCI&ACkw#wqxvn1Gk7j5p(k!3M`0lZmn z$3Syv=bckdi1!skI|Z*U2n1 zZ)yQG^=^1PsAnGflLSX!1} zDnD6;UH7|$pCb5&Mr$t?Ddup2;$?kkj%$y%dDW8FN|VE)pEMW~B7GeKiH?pLUCX8? zg9qtBN0V47ig(l6tVB)lfMShf0lZtyrOGAum6UyP%jUz1TizVZD@jI1^RR+2dQ_0g z&;gEw&jvOBPZiSGv#)X>MbIO8^oon6=Uyi990$xn7Xq&;Ggii`E2ms;_I0fW+CU<} z-d72nqo6IK%o}N6x4J^BhGl*JY!INel>kNAwX}`Qx-`4?EWrCXI;Z{&ytQV+`>Fu$ zool)m5_ag#-hHF15%R~~1+X#DSjackL8?uyljv5p@p_JC4zKbgt!2I<`19annN4J2 z$Lg`)b}e|s`esBdKWxn6PZ8b|Y+4HmAkCuZ{_6w&_mr}DtA6`#xp#GRIh$3`(9zsS zkf}o77<=(pW#iz$vDu%dc2s4`siYXgXthV-F7k7BXwGcpA=uQIUEr5$;|dE9l(8P> zTzPObfk}X|lm#!0f(56ptpx-wVw~y~0LcfBJDcl_3c|?AQ^@1( z(SLmouMIcN?Oe(Hb7sf#d_O|8YU!-&=J%vTTE_&Oq{Qu6FqVfm02$2%1bY-At6r>N zj(?()0oSX7bCahSrJu}5KUUn#uF~|>^SAIMAE*y`h8s6^hys6}GMtp>p8G1+A1IIA zy1fIx+SNXLmGy6U?{f(7_i%r0;{KFI=})cu==?H{+0z?5_r}H;w71TKc+AtA+g1?(f(8>iQW)!=V3%I4 zH9Mm&y^NkpuIY~1xV7XCh71Fx1R!xqI#FAO>aJdl{@C(Ch*hZ2SDE~CBQ*p`Cfx2JI&X0q#I}G7|c=q_H=Xi z4VtZ6y8(nRpr67UW35y4NnoT@rdTU{GIH}5{Vr{V$L{n)=o_MzM%$K`l2S~_Q^B8g zH_0S8o+)6}K>}WhY#VU^(6OBsC<<~>(x`$nE?kuSZgW0PPJK|YXv~#^nf4Vt?1+Y+yNRZMh#!7Yl=MXFx7sP3}~01+y`>!{i?dAl$EqB*;bWX7e;^~EldDibIP zvu~HuQyF>dm8swCX!?5a5N4qlv^%q=wu>o{9Z%QO!Rw8UZw_jE%7j$c}^NJ5TfV;cF#%g7y51(h3q&^HX?rExLJ7tE4 z1H%XU(1a!r!YsI6wFaW8(ygLhT)gQJ8$(|E@Er5IWpHcis)acPw^glB^IQE7nFlE% zSd&!;WN$cdJGBCvU}D-!b&4uZH9smJug)0OU$!=q1jxx7&7B5;Fl+)W_oD+>o9M+Y zDJouDTHWp4=&fCoZAEBSc-76FllU9TKf=ZUroe61OkWZkLp-_OPirGWuEt~AoM-oQ z)#-QD=+jA2bojtjeJq5~9t(Zm+_%FyH@W8kBN8)-r;K;=JG5jmdNzVF1P^1+X11@E zq;Q&!i;V7Tg9Q+b0f$kyR;UT46_|+$Z!DZ082+-)zi&Q!ax)vfWYs2X;l3)+oq(hH z6Q1X>@@DZwHVlC5PMp#g^LpMjIc|SfMurD}>Iet91Z+i?%$zocr0SywW&bI_-f$H& zI4lo2VD|oYHuuB%7T7wh)57uvgaxoM^Fn$=LcNgLpF)+O&H-(E+h;E%Bqww)uZ5Br zQP`05@;Z-lOKWSYVcdsLtwek$&hUlqL9=Ogg0fIvrCE8wmQ#B6t}lFbV?Cgzsl9OToOmJ^6(yYdl^A51y|y1mUJr z0fHR`!96EhJ#~B~^8T)hMU<*OCAe|V5Nn7iao>)6zKC)@8Y5Fhl6YYYZl%C zTHf7Py@D@>NH4HM0}-T%raQKUcJO)N1!%rpNAf0%0zF#}hq6h7FYW#P8*_t$1khwAnu>e;;$})f3bA$j}I9lzy@@D_@PIqn}ST~%F~-e z7Y473r$EI914s4}Gxy7#Yrh>}D`w|gCvUAmqntS7U)tIlnKEzj0a^+DdSN=TzK6p# z0$&C7Bu%>IbxdlE?Mtoi_+cLg^#%053pegIH-X+De}p0Pb4;=wR6i=kD0vbOZ&PT z^4@laZBxaw+JY?;$7EqGv7Aq)*)WC3u;vrm3M*(lc!f^Qh}AMh6Qvsv(D^I`?EV>e z(Oxiy>U)t+)+&(fduZdLhl`t%O*8y(u%V-YApCIm*TkHiCk$~k+H5rsy+CbE6x`%Z zxrGdxd_fQZX$H3P$yOKcRfoRST&D13XS=CCRFwGqo{IIjpgmU6&GcVmx+be**0M3J}C^ zjLhxkEIf0BUkq(q!echeLgT1QBHL)TsrXtAZ;(M{vyW!?!8$FHKNNRxNwnPO`b{M0 zAI=*9TEmI$sDoXKpp!?$!jOdB4HcdVsspS zv})GGTDH`1g#gD7o`DkI%)AJ20#Tpu!U+JqsdaTssu(*=1w+hG$1JbIBh5=c|B=eL zQT5&>0e&ts@gJoPbr#2XI&dbgOQSYr4vH6(d6H}0D!dnh-L&dB!0*b^s-r`@8~%x< z*`56d-NZ1Vufd`gEj6aI=P^f!o2`x`*}g2b-5;BFzRU}C0}OhceCn)>A@5px;%*N9 zZ~`{P*kLxTf4J*k`J|irRCJe@C#(ADyjOc~l`c`T5xekD`+q_u$rnL8eGU z2jcH@Jmua#;S25+GcUY--=GEkSJ;5fM$Y)5q85VE;+5f$ZzUyZSbC@@@AAdIk>0d< zoGfuMDs$H)4Bv&w#3I6Bv@ynwH*#XL}bDN8%&`vynlDU zEY?g-^w+;4Ir{UzQTV67Wciotf1!ZZlLPYqa{Ye_f=4K_{4xlv{y@s@Uv+gA9px%T Ho3Q@_*^-3M literal 2210 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4YzZe)eGCf@!Ln`LHy|z%W!GPhw zfpg6VH*;xkEPicJu|rw^o_ku(Z$rk0X&*}&7(VQ)W;mD4#9*J!z@Tt~fx%${BSV7% zGXsMo(7Yxd1_l;Mpn*mV3<4>m3P!_WG(C*wi_!97w45BR4n}abxZpUjsUZt$ABcr< WOLIi26w4KiMf82YWbMO2!okk>X=1lJ6obSBPpE9&iS+O!mc>@2lmdd1( zjn?iVRvMEtfPvt@zc0Z*gH<^K@T8TMQq%yGi)U{%?mah{E(!>Gm%=gW3UEb+&c-Th z&KmUW(%9Wm0~JxM4O=$&M&ZVwhbSNcvXW)z7f6l-{fq+&LRdwOm2)aBk@A5P-3r|u zo^wBO^UhO^pzJ}I2vIDE47P8Q*t1n&{TgWjK2K;od1dhWleX5xp@PzBE4#B@Sof{V z6Tb~6iG+-xY>*8u97z$A1Dc+5KC^=Dn}GLUB(C0#Q49<`6=8D0Sd6xO5<^2FX5Tcs zrE%_9#)1uh)VTFH#*b-3UI6Xu1TGy-<8~p&)NhR@-;jkad5lrZvJJAqY)NC+W{I6d z!ygnb-wbUcYz&Fz?Kp6@U*kI|XrQ||b{Vf?)RhgzQ4zzwPS2jN4^Re-Mx!sRbuR0$ z*7_dv!+9m@>$VEWNyrf6h8BsRvj$%$71DGLqxxdx8fFEGNXi;s(jKYgptUA)uGyF< z^N*qia?1qfC`2}Pi&$j^pUTBxzN|6!O(CBXXjuW27N|UgsVH;%67jS&19z@uaeTjz zx%ov_B!Fz8q8J(ydiXN152p?_Ac{0*i^^Kd#CyIwNpHN~jYl^2Oo_{~+T<~p0Wdfk zpfDPuYfBn^XR_GPDyu@g=R1BYjD&bJ99S8atxF0wI$OxS>q&#&8*^lK7JIk(=<3K} zYQ_ny%sKsqkpR6niE~1X+9vm1dq3@m#6&5L$fz4#GZ0w}rTI5xxR^wXC zHadHR^xk9U5|S`=ich->h7BEj?RL`6kS#K z+;hHrUiX|kuBHpWA1N7wVA|ka| zyl_Z6LPsH-bmmIM06R5~jFFI6#2A5m4Fs|no}0@qdZ%Nk*{P1~(R(MtNJh}CgWSPl$ep1_R`3p{k2 zhruF@Fwf;AZUs#o)tEkJ>4*9SR+GtPY*J1DXRHy}yIafO^4`=oBvB>Np>NMmxF zv#3_qC5^&=`Dm?aVfP<4{t{iJqn24?M zoA^oL$yXHqbe3Z@``~{Aauf1(wvfu&Uo;vra*WB%*vFIANn&`cq;dHLE;gMa;fHl_ zf7$NSyT%CwI#Q{EBEAabV+$3Hf4v&u%L#?DaeiSI+opzSzZdH!6SLqP0or=8W8T?! zSc8u~?W5{z5(on_D~{RkcrmYk7{ZD3AtzzcW|y|I+ZGEtF){mInF3?uI*IkGq={Dc zofqB<@a^FU%gHA>P(=1dkt~=&)}k71XzM1{?W>tIK%caA<~o})O+?bz+nLbg1BhB=F5;v5I2Fj?*bS?`^<}n#Zg2sOtoF!l zb{LiNL5{lF`TC^g1CkfPd}CC0!L>eN0Pyvm5MGlLy;<%uO|ZB{_qSw z`iji$)k}V!zHXI+^EY_r=4bw9#<&V1TAssmnFsSqOdFe*WwLC>kjCVuJGIp-9Q5}D zm@89XqWp$FI1Ks7Lf|JlZMf@iGq~c9dE9hqfzn0xaf9hoz>5m;%$o~%?Srb(DDwzI z-IVFcjbzTqW(;XeF+iF`#b2@4!5=nycEIq z9!5`gv8>-3WagN{fkPoa84vK;fq+^DjsAi(0TgBwtlW?ZuSl{>1_NVp9hu#@PfYr_ z`Tl8oc8v$NZQKp+LYwPPG{OkZQB4YELfXu8+SND(Sgm=4-8>sIM>j)H=NDX zkneoIgvXzw=Dj*gLL*9~=>v_7qYu$o`=#F(JNn>{ycb8xftw%1y*CE9>f&O9_FAHt z2Usgi2wlP>|Dkf-8zL$5AHM_!u3)W<+?go`%iaO;(Q7+IN1WOj8vm^Qa*=*toLW@xspr8vRt zE6`cauxkRMSytBolOtfdU042J4#-1l{Wuuf9RZ7OPWkIS?@}F9cYla3e((Q=_ys%Z V57D$)m>d8A002ovPDHLkV1iUc_RIhP literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5d34410c52bd32e12469795f56d7d43b05e8ec0d GIT binary patch literal 2091 zcmV+`2-Nq9P)k9U5|S`=ich->h7BEj?RL`6kS#K z+;hHrUiX|kuBHpWA1N7wVA|ka| zyl_Z6LPsH-bmmIM06R5~jFFI6#2A5m4Fs|no}0@qdZ%Nk*{P1~(R(MtNJh}CgWSPl$ep1_R`3p{k2 zhruF@Fwf;AZUs#o)tEkJ>4*9SR+GtPY*J1DXRHy}yIafO^4`=oBvB>Np>NMmxF zv#3_qC5^&=`Dm?aVfP<4{t{iJqn24?M zoA^oL$yXHqbe3Z@``~{Aauf1(wvfu&Uo;vra*WB%*vFIANn&`cq;dHLE;gMa;fHl_ zf7$NSyT%CwI#Q{EBEAabV+$3Hf4v&u%L#?DaeiSI+opzSzZdH!6SLqP0or=8W8T?! zSc8u~?W5{z5(on_D~{RkcrmYk7{ZD3AtzzcW|y|I+ZGEtF){mInF3?uI*IkGq={Dc zofqB<@a^FU%gHA>P(=1dkt~=&)}k71XzM1{?W>tIK%caA<~o})O+?bz+nLbg1BhB=F5;v5I2Fj?*bS?`^<}n#Zg2sOtoF!l zb{LiNL5{lF`TC^g1CkfPd}CC0!L>eN0Pyvm5MGlLy;<%uO|ZB{_qSw z`iji$)k}V!zHXI+^EY_r=4bw9#<&V1TAssmnFsSqOdFe*WwLC>kjCVuJGIp-9Q5}D zm@89XqWp$FI1Ks7Lf|JlZMf@iGq~c9dE9hqfzn0xaf9hoz>5m;%$o~%?Srb(DDwzI z-IVFcjbzTqW(;XeF+iF`#b2@4!5=nycEIq z9!5`gv8>-3WagN{fkPoa84vK;fq+^DjsAi(0TgBwtlW?ZuSl{>1_NVp9hu#@PfYr_ z`Tl8oc8v$NZQKp+LYwPPG{OkZQB4YELfXu8+SND(Sgm=4-8>sIM>j)H=NDX zkneoIgvXzw=Dj*gLL*9~=>v_7qYu$o`=#F(JNn>{ycb8xftw%1y*CE9>f&O9_FAHt z2Usgi2wlP>|Dkf-8zL$5AHM_!u3)W<+?go`%iaO;(Q7+IN1WOj8vm^Qa*=*toLW@xspr8vRt zE6`cauxkRMSytBolOtfdU042J4#-1l{Wuuf9RZ7OPWkIS?@}F9cYla3e((Q=_ys%Z V57D$)m>d8A002ovPDHLkV1iUc_RIhP literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f15d0057985dd3574746d8499b0cc5dc09a2a0e3 GIT binary patch literal 3211 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 xP@xwc$rJI+?5L&<`;RrS literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..121df933fba3a412dd940db2648a835794145bb4 GIT binary patch literal 1490 zcmV;@1ugoCP)}-iM-UN^0UGG(=}&dld)|9rf2yXN!GxF8)YSXjbMLvIciXo# z&}Hzng@Q2_TD$kvMspv(6`xkB>pnFbls_W__@2VGjxe(DG&;Z&p!{!hL$zjb;v7&8 zSnJmIIUsE7Z!T>yJOpGjxVa4^p~x>JOd030FRAm+_79?;XH*jDIbumvzIImPFj3_$l%m@ zpeIAVOgYQz#;ULck-K57p*1bm5~vbWPE1*pf;6HqXn4+XRV11pS*x+>VGn&ho&Fpe zP*^>zFnX85F9d$@Pm8@r4blx@kh~JoBtOE+rA)DcZ>%AXHHfBG2s1@&;dI#&n2%>4 zXh+lWe=UCd+lVs#?6YBwr9%n}`V~f20w3)Poysl0i>FmnKI8dX>y|*)Op;qDwHUqI zYXKHXf$d*}VnrpeLWDS*o;NqAar;dgFTNAte9@vetKec#K*`bZ0;0)M^19{_v`#x# zuU2B2HVh*?Kkgr6 zT!ze4EFQl%g}$6S`tE0S{C+aR0`gxyw5=!vS<{mSjvS9fCR8IeYPS8Z}Dw3q>O(vY94V^r=f1E7qO4V6c!V zUUjoa6~>alq+hzx$}GLMu?M%@;NkN<0p8wOAy+k&j-A!X=ye=c1A_xc>oT`&v4{KC z__$b%Bw(D?$%p3)w0=MBV?Z7Sva}XB=4LklI(|Q~0KRxR4Zhl6ll?$&EYX-M zM6$726x_vRWuDi0pTq$bmhoLxk;C%DEy)7WxM<-t;wR-Xy%DyJm1S)pudsDXKQ@e{ zNqZ|+7cNKg=5L7Eg`*@iOMI>^XGHnAjQIV`|Q^zsH#eMw83Kk72Y#Pmpz}GkD zG5HT&f5$>xDjCenYpl7|$MPlZczl1NEa$i~ar(&}EIw83ilmjZlpP*L0Z&p-MUiOT z+_te9oIV%frKj>Dhymgt%~2#uw|-p0r@O1j_b8#2jf)grlO(q{n`s0bS*nyv4Gx%m zx{6{YMOp=-8S9oO;7@+2VDI4?M%QI=&*}_rq_oJSl~8;7yutooYna$qm5Ars?C3R*w`miUoVd z*DBT{*>-W#DfrnzjI6kbH(nS_^hjO1!4JNi#?EhMi0TXp?(|>0jqO%1tO@D*tKpf4 zdhy(&ecijkZhddL8e#Heh*HH~z2%+Ni*g_>U+N>5_2%w-lO4Vl?ytSW=lZMW?(jQ) s%UC6O#R+Zx$Gz6v$8U+!EBu!E59hje>b&4og8%>k07*qoM6N<$f}D`er2qf` literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..63efc4c560f0bcb3d177be53357963bedd32bf74 GIT binary patch literal 3051 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 tk21kQZEHoLa5#kG`d>+0C-jqy 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

!+Z_Tct7Z-w58$GWeo+g!;akIF0zMg-fTAjB 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 V57D$)m>d8A002ovPDHLkV1iUc_RIhP literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..385c7786d3265ce27edea0b1b2dfa1a1d8d887c1 GIT binary patch literal 4345 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..385c7786d3265ce27edea0b1b2dfa1a1d8d887c1 GIT binary patch literal 4345 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a296b5bf613ec48fe8973b53de269bf100fdd4d2 GIT binary patch literal 6341 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 Hu0mjfh1hzb literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/desktop/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..29eb8a069504b19fdeb9a747fefa8011f1847f78 GIT binary patch literal 15863 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a296b5bf613ec48fe8973b53de269bf100fdd4d2 GIT binary patch literal 6341 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 Hu0mjfh1hzb literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2698b24ce0392fdce5489ca5019f123499b9f4eb GIT binary patch literal 9570 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..234a90007ec3024cde190b22c6aefb22f6b7393f GIT binary patch literal 4057 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index a48bd63..e2aca55 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -29,7 +29,9 @@ "icon": [ "icons/32x32.png", "icons/128x128.png", - "icons/icon.png" + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" ] } } From 01a64b3b289ed54ad0b9b09fd8a8f253eb4bd8eb Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 18:11:56 +0000 Subject: [PATCH 03/19] fix(desktop): address PR review feedback Engine server: - hub.subscribe no longer creates a channel for an unknown session (prevented a stray monitor connection from reserving an id and causing spurious 409s); the SSE route now 404s unknown sessions (+ test). - Normalize LOOPWRIGHT_STATIC_DIR to an absolute path (in index.ts and the serveStatic guard) so a relative value no longer 403s every asset. Desktop shell (Rust): - engine.rs: kill the spawned sidecar if readiness detection fails, instead of leaking the process. - secrets.rs: surface non-NotFound index read errors instead of treating them as an empty key list; validate secret key names and reject the reserved LOOPWRIGHT_ prefix before persisting/injecting. Desktop frontend: - api.ts: drop the hardcoded loopback fallback; fail fast on engine_url errors. - main.ts: register the SSE teardown synchronously (no leaked EventSource on fast navigation); validate secret keys as env-var names; keep the restart button resilient on failure; surface an explicit "restart engine to apply" gate after secret changes. - styles.css: quote mixed-case font family names. Docs/tests: - tasks.md: correct the test count (132). - server.test.ts: fail fast if trace polling times out; compute SSE replay count by id comparison rather than assuming a gapless zero-based scheme. Full suite green (132 tests), tsc clean, desktop frontend builds, Rust formatted. --- .kiro/specs/loop-engine/tasks.md | 2 +- desktop/src-tauri/src/engine.rs | 15 +++++++- desktop/src-tauri/src/secrets.rs | 31 +++++++++++++++- desktop/src/api.ts | 8 ++-- desktop/src/main.ts | 64 +++++++++++++++++++++++++++----- desktop/src/styles.css | 3 +- src/server/hub.ts | 8 +++- src/server/index.ts | 6 ++- src/server/server.ts | 17 +++++++-- test/server.test.ts | 9 ++++- 10 files changed, 138 insertions(+), 25 deletions(-) diff --git a/.kiro/specs/loop-engine/tasks.md b/.kiro/specs/loop-engine/tasks.md index 3941009..3b761da 100644 --- a/.kiro/specs/loop-engine/tasks.md +++ b/.kiro/specs/loop-engine/tasks.md @@ -82,7 +82,7 @@ Each task references the requirement(s) it satisfies. Milestones 1–5 are complete and merged on `main` (engine core, runners + role bindings, persistence/resume, the parallel scheduler with git-worktree isolation and the integrator, and the observability/usage/trace layer); the -suite is green (125 tests, `tsc --noEmit` clean). +suite is green (132 tests, `tsc --noEmit` clean). Active work is **Milestone 6 — Desktop delivery**. Rather than reimplement loop logic (forbidden by Req 13), the desktop app reuses the headless engine through diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index ec3c981..28638b5 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -73,7 +73,15 @@ impl EngineManager { .spawn() .map_err(|e| e.to_string())?; - let url = tauri::async_runtime::block_on(read_listening_url(&mut rx))?; + let url = match tauri::async_runtime::block_on(read_listening_url(&mut rx)) { + Ok(url) => url, + Err(e) => { + // Readiness failed (bad output or timeout): don't leak the + // spawned process — kill it before surfacing the error. + let _ = child.kill(); + return Err(e); + } + }; // Keep draining events so a full stdout/stderr pipe can't stall the // engine during a long run. @@ -136,7 +144,10 @@ fn parse_listening(buf: &str) -> Option { } if let Ok(v) = serde_json::from_str::(line) { if v.get("loopwright").and_then(|x| x.as_str()) == Some("listening") { - let host = v.get("host").and_then(|x| x.as_str()).unwrap_or("127.0.0.1"); + let host = v + .get("host") + .and_then(|x| x.as_str()) + .unwrap_or("127.0.0.1"); let port = v.get("port").and_then(|x| x.as_u64())?; return Some(format!("http://{host}:{port}")); } diff --git a/desktop/src-tauri/src/secrets.rs b/desktop/src-tauri/src/secrets.rs index 8318715..6220e89 100644 --- a/desktop/src-tauri/src/secrets.rs +++ b/desktop/src-tauri/src/secrets.rs @@ -26,10 +26,38 @@ fn index_path(app: &AppHandle) -> Result { fn read_index(app: &AppHandle) -> Result, String> { match fs::read_to_string(index_path(app)?) { Ok(s) => serde_json::from_str(&s).map_err(|e| e.to_string()), - Err(_) => Ok(Vec::new()), + // A missing index is the normal "no secrets yet" case; any other error + // (permissions, I/O) must surface rather than masquerade as an empty + // list, which would silently drop tracked keys on the next write. + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()), + Err(e) => Err(e.to_string()), } } +/// Rejects empty/reserved/invalid secret names. Keys become environment +/// variables in the engine sidecar, so they must be valid env-var identifiers +/// and must not shadow Loopwright's own `LOOPWRIGHT_*` wiring (port, db path). +fn validate_key(key: &str) -> Result<(), String> { + if key.is_empty() { + return Err("secret key cannot be empty".to_string()); + } + if key.starts_with("LOOPWRIGHT_") { + return Err("secret key cannot use the reserved LOOPWRIGHT_ prefix".to_string()); + } + let mut chars = key.chars(); + let first_ok = chars + .next() + .map(|c| c == '_' || c.is_ascii_alphabetic()) + .unwrap_or(false); + let rest_ok = key.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()); + if !first_ok || !rest_ok { + return Err(format!( + "secret key \"{key}\" is not a valid environment variable name" + )); + } + Ok(()) +} + fn write_index(app: &AppHandle, keys: &[String]) -> Result<(), String> { let json = serde_json::to_string(keys).map_err(|e| e.to_string())?; fs::write(index_path(app)?, json).map_err(|e| e.to_string()) @@ -42,6 +70,7 @@ pub fn list_keys(app: &AppHandle) -> Result, String> { /// Stores (or replaces) a secret in the keychain and records its name. pub fn set(app: &AppHandle, key: &str, value: &str) -> Result<(), String> { + validate_key(key)?; let entry = Entry::new(SERVICE, key).map_err(|e| e.to_string())?; entry.set_password(value).map_err(|e| e.to_string())?; let mut keys = read_index(app)?; diff --git a/desktop/src/api.ts b/desktop/src/api.ts index a969870..b4f9c4c 100644 --- a/desktop/src/api.ts +++ b/desktop/src/api.ts @@ -26,11 +26,9 @@ async function tauriInvoke(cmd: string, args?: Record): Prom export async function apiBase(): Promise { if (cachedBase !== undefined) return cachedBase; if (isTauri()) { - try { - cachedBase = await tauriInvoke("engine_url"); - } catch { - cachedBase = "http://127.0.0.1:4317"; // fallback to a conventional port - } + // Fail fast: surfacing the real startup/invoke error is far safer than + // silently routing API + secret traffic to an arbitrary local port. + cachedBase = await tauriInvoke("engine_url"); } else { cachedBase = ""; } diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 338220e..9c64f43 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -37,6 +37,9 @@ function badge(state: string): HTMLElement { return h("span", { class: `badge state-${state}` }, [state]); } +/** Valid POSIX-ish environment variable name (used for secret keys). */ +const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; + // --- navigation ------------------------------------------------------------- type Nav = "start" | "sessions" | "secrets"; @@ -236,11 +239,15 @@ function renderMonitor(sessionId: string, goal: string): void { } } - void openStream(sessionId, onMessage, () => { + const closePromise = openStream(sessionId, onMessage, () => { /* EventSource auto-reconnects with Last-Event-ID; nothing to do here */ - }).then((close) => { - teardown = close; }); + // Register teardown synchronously: if the user navigates away before + // openStream resolves, this still closes the EventSource once it exists, + // preventing a leaked connection that dispatches into a stale view. + teardown = () => { + void closePromise.then((close) => close()); + }; } // --- Results view ----------------------------------------------------------- @@ -370,6 +377,18 @@ async function renderSecrets(): Promise { const listEl = h("ul", { class: "secret-list" }); card.append(listEl); + // Stored secrets are injected into the engine only at (re)start, so changing + // them requires a restart to take effect. Make that gate explicit rather than + // silently leaving the running engine on stale values. + const pending = h("div", { class: "hint pending", hidden: "true" }, [ + "Stored secrets changed — restart the engine to apply.", + ]); + const markPending = (): void => { + pending.hidden = false; + }; + + const formError = h("div", { class: "error", hidden: "true" }); + async function refresh(): Promise { listEl.innerHTML = ""; const keys = await listSecretKeys(); @@ -379,6 +398,7 @@ async function renderSecrets(): Promise { del.addEventListener("click", async () => { await deleteSecret(k); await refresh(); + markPending(); }); listEl.append(h("li", {}, [h("code", {}, [k]), del])); } @@ -392,21 +412,47 @@ async function renderSecrets(): Promise { `; form.addEventListener("submit", async (e) => { e.preventDefault(); + formError.hidden = true; const data = new FormData(form); - await setSecret(String(data.get("key")), String(data.get("value"))); + const key = String(data.get("key") ?? "").trim(); + // The key becomes an env var injected into the engine; reject invalid names + // (and the reserved LOOPWRIGHT_ prefix) before persisting. + if (!ENV_KEY_RE.test(key) || key.startsWith("LOOPWRIGHT_")) { + formError.textContent = + "Key must be a valid env var name and must not start with LOOPWRIGHT_ (e.g. OPENAI_API_KEY)."; + formError.hidden = false; + return; + } + try { + await setSecret(key, String(data.get("value") ?? "")); + } catch (err) { + formError.textContent = `Failed to save: ${(err as Error).message}`; + formError.hidden = false; + return; + } form.reset(); await refresh(); + markPending(); }); - card.append(form); + card.append(form, formError); const restart = h("button", {}, ["Restart engine to apply secret changes"]); restart.addEventListener("click", async () => { restart.textContent = "Restarting…"; - await restartEngine(); - restart.textContent = "Restart engine to apply secret changes"; - await checkEngine(); + restart.setAttribute("disabled", "true"); + try { + await restartEngine(); + pending.hidden = true; + await checkEngine(); + } catch (err) { + formError.textContent = `Restart failed: ${(err as Error).message}`; + formError.hidden = false; + } finally { + restart.textContent = "Restart engine to apply secret changes"; + restart.removeAttribute("disabled"); + } }); - card.append(h("div", { class: "actions" }, [restart])); + card.append(h("div", { class: "actions" }, [restart, pending])); await refresh(); } diff --git a/desktop/src/styles.css b/desktop/src/styles.css index 9cc2915..e28ffca 100644 --- a/desktop/src/styles.css +++ b/desktop/src/styles.css @@ -9,7 +9,7 @@ --green: #3fb950; --yellow: #d29922; --red: #f85149; - --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --mono: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace; } * { box-sizing: border-box; } @@ -94,6 +94,7 @@ button.small { font-size: 12px; padding: 3px 8px; } .actions { display: flex; align-items: center; gap: 12px; margin-top: 8px; } .hint { color: var(--muted); font-size: 12px; } .hint.error, .error { color: var(--red); } +.hint.pending { color: var(--yellow); } .goal { font-size: 15px; margin-bottom: 4px; } .session-id { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-bottom: 10px; } diff --git a/src/server/hub.ts b/src/server/hub.ts index 5020cbb..0a77a71 100644 --- a/src/server/hub.ts +++ b/src/server/hub.ts @@ -93,9 +93,15 @@ export class RunHub { * returned unsubscribe function is called. `afterId` of -1 (the default) * replays the whole history, matching a fresh client; a reconnecting client * passes its last seen id so it does not re-process what it already has. + * + * Subscribing to an unknown session does NOT create a channel — that would + * let a stray monitor connection reserve a session id and make a later + * `POST /api/runs` for it spuriously report "already running". Callers should + * gate on {@link has} first; for safety this returns a no-op unsubscribe. */ subscribe(sessionId: string, listener: Listener, afterId = -1): () => void { - const ch = this.channel(sessionId); + const ch = this.channels.get(sessionId); + if (!ch) return () => {}; for (const msg of ch.buffer) { if (msg.id > afterId) listener(msg); } diff --git a/src/server/index.ts b/src/server/index.ts index 71f5f48..cf1991f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -17,6 +17,7 @@ * Tauri sidecar; also runnable directly (`npm run serve`) to use the UI from a * browser without any desktop toolchain. */ +import path from "node:path"; import { loadConfig } from "../config.js"; import { openStore } from "../storage/store.js"; import { createServer } from "./server.js"; @@ -25,7 +26,10 @@ async function main(): Promise { const config = loadConfig(); const store = await openStore(config.dbPath); - const staticDir = process.env.LOOPWRIGHT_STATIC_DIR; + // Resolve to an absolute path so the server's path-traversal guard works even + // when a relative LOOPWRIGHT_STATIC_DIR is supplied. + const staticEnv = process.env.LOOPWRIGHT_STATIC_DIR; + const staticDir = staticEnv ? path.resolve(staticEnv) : undefined; const server = createServer({ store, config, diff --git a/src/server/server.ts b/src/server/server.ts index db3bbdc..c31f6d8 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -237,6 +237,13 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { } function streamRun(req: IncomingMessage, res: ServerResponse, sessionId: string): void { + // Unknown session: don't let an SSE connection implicitly create a channel + // (which would reserve the id). Report 404 so the client can fall back to + // the trace endpoint for a past run. + if (!hub.has(sessionId)) { + return send(res, 404, { error: `no active run for session "${sessionId}"` }); + } + res.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", @@ -275,13 +282,17 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { } async function serveStatic(res: ServerResponse, pathname: string, dir: string): Promise { + // Normalize the asset root to an absolute path so the traversal guard below + // compares like with like even when `dir` came in relative (e.g. a relative + // LOOPWRIGHT_STATIC_DIR), which would otherwise reject every request. + const root = path.resolve(dir); const rel = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, ""); - const resolved = path.resolve(dir, rel); + const resolved = path.resolve(root, rel); // Path-traversal guard: never serve outside the asset directory. - if (resolved !== dir && !resolved.startsWith(dir + path.sep)) { + if (resolved !== root && !resolved.startsWith(root + path.sep)) { return send(res, 403, { error: "forbidden" }); } - const file = await resolveFile(resolved, dir); + const file = await resolveFile(resolved, root); if (!file) return send(res, 404, { error: "not found" }); const ext = path.extname(file).toLowerCase(); res.writeHead(200, { diff --git a/test/server.test.ts b/test/server.test.ts index e5364b6..cd80edc 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -143,6 +143,8 @@ describe("server: health + validation", () => { it("404s unknown api routes and missing traces", async () => { expect((await fetch(`${base}/api/nope`)).status).toBe(404); expect((await fetch(`${base}/api/sessions/ghost/trace`)).status).toBe(404); + // Streaming a session that was never started must not implicitly create it. + expect((await fetch(`${base}/api/runs/ghost/stream`)).status).toBe(404); }); }); @@ -169,6 +171,7 @@ describe("server: run lifecycle + trace", () => { } await new Promise((r) => setTimeout(r, 10)); } + expect(trace, "trace did not reach completed state within polling window").toBeDefined(); expect(trace.trace.session.goal).toBe("ship it"); expect(trace.trace.session.status).toBe("completed"); expect(trace.trace.tasks.map((t: any) => t.taskId)).toContain("task-1"); @@ -249,6 +252,7 @@ describe("server: live SSE stream", () => { // Let the run finish so the whole buffer exists. const first = await fetch(`${base}/api/runs/${sessionId}/stream`); const all = await readSse(first, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); + expect(all.length).toBeGreaterThan(1); const cutoff = all[1]!.id; const resumed = await fetch(`${base}/api/runs/${sessionId}/stream`, { @@ -256,6 +260,9 @@ describe("server: live SSE stream", () => { }); const rest = await readSse(resumed, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); expect(rest.every((m) => m.id > cutoff)).toBe(true); - expect(rest.length).toBe(all.length - (cutoff + 1)); + // Derive the expected replay count by comparison rather than assuming a + // zero-based, gapless id scheme. + const expected = all.filter((m) => m.id > cutoff).length; + expect(rest.length).toBe(expected); }); }); From 698d771bea8f952dd2e96d5f6d6cfece720bb62b Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 18:47:11 +0000 Subject: [PATCH 04/19] fix(desktop): close local-RCE vector, fix Rust build, wire sidecar, patch deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses post-merge review findings P1–P2. P1 (security) — the engine API could start runs (and thus spawn local cli processes / forward stored secrets to arbitrary endpoints) from any browser origin via wildcard CORS and token-less per-request runner config: - Require an unguessable per-process bearer token on every /api route except health (constant-time check; accepted via Authorization header or, for EventSource, a token query param). - Replace `Access-Control-Allow-Origin: *` with an origin allowlist (loopback + the Tauri webview); the specific origin is echoed, never `*`. - Token is delivered out-of-band: a Tauri `engine_token` command, or injected into the served index.html (so only a page loaded from loopback can read it), and reported on the engine readiness line. Frontend attaches it to all requests; restart re-resolves it. P1 (build) — the Tauri shell did not compile / package: - Declare `tokio` (time feature) directly; it was used via tokio::time::timeout but only present transitively. - Wire the sidecar build into Tauri: desktop `build:sidecar` script + run it from beforeDevCommand/beforeBuildCommand so binaries/loopwright-engine exists. P2 (deps) — upgrade vulnerable dev tooling: vitest 2 -> 4 (root) and vite 5 -> 8 (desktop). Both `npm audit` now report 0 vulnerabilities. Validated: 134 engine tests pass (incl. new auth-boundary tests), tsc clean, desktop frontend builds on vite 8, sidecar compiles and enforces the token end-to-end (401 for unauthenticated/cross-origin requests; cli runner never executes), Rust formatted and dependency graph (incl. tokio) resolves. --- desktop/package-lock.json | 1215 ++++++++++----------- desktop/package.json | 3 +- desktop/src-tauri/Cargo.lock | 1 + desktop/src-tauri/Cargo.toml | 3 + desktop/src-tauri/src/engine.rs | 39 +- desktop/src-tauri/src/lib.rs | 9 + desktop/src-tauri/tauri.conf.json | 4 +- desktop/src/api.ts | 41 +- package-lock.json | 1653 +++++++++++++---------------- package.json | 2 +- src/server/index.ts | 10 +- src/server/server.ts | 156 ++- test/server.test.ts | 99 +- 13 files changed, 1567 insertions(+), 1668 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 305e8e0..a47d829 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -13,115 +13,76 @@ "devDependencies": { "@tauri-apps/cli": "^2.0.0", "typescript": "^5.6.3", - "vite": "^5.4.0" + "vite": "^8.0.16" } }, - "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" - ], + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "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" - ], + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "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" + "dependencies": { + "tslib": "^2.4.0" } }, - "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" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "tslib": "^2.4.0" } }, - "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" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "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" - ], + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "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==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -129,50 +90,16 @@ "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" + "android" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -180,135 +107,16 @@ "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" + "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -316,16 +124,16 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -333,33 +141,33 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "freebsd" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "sunos" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -367,176 +175,16 @@ "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" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", - "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", - "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", - "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", - "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", - "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", - "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", - "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", - "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", - "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", - "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], @@ -545,124 +193,49 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", - "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", - "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", - "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", - "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", - "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", - "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", - "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ - "s390x" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", - "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ - "x64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", - "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -671,12 +244,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", - "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -684,13 +260,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", - "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -699,40 +278,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", - "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", - "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", - "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -741,21 +331,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", - "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@tauri-apps/api": { "version": "2.11.0", @@ -984,50 +570,43 @@ "node": ">= 10" } }, - "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==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, - "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==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=12" + "node": ">=12.0.0" }, - "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" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/fsevents": { @@ -1045,6 +624,267 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -1071,6 +911,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -1100,49 +953,38 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/rollup": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", - "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.0", - "@rollup/rollup-android-arm64": "4.62.0", - "@rollup/rollup-darwin-arm64": "4.62.0", - "@rollup/rollup-darwin-x64": "4.62.0", - "@rollup/rollup-freebsd-arm64": "4.62.0", - "@rollup/rollup-freebsd-x64": "4.62.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", - "@rollup/rollup-linux-arm-musleabihf": "4.62.0", - "@rollup/rollup-linux-arm64-gnu": "4.62.0", - "@rollup/rollup-linux-arm64-musl": "4.62.0", - "@rollup/rollup-linux-loong64-gnu": "4.62.0", - "@rollup/rollup-linux-loong64-musl": "4.62.0", - "@rollup/rollup-linux-ppc64-gnu": "4.62.0", - "@rollup/rollup-linux-ppc64-musl": "4.62.0", - "@rollup/rollup-linux-riscv64-gnu": "4.62.0", - "@rollup/rollup-linux-riscv64-musl": "4.62.0", - "@rollup/rollup-linux-s390x-gnu": "4.62.0", - "@rollup/rollup-linux-x64-gnu": "4.62.0", - "@rollup/rollup-linux-x64-musl": "4.62.0", - "@rollup/rollup-openbsd-x64": "4.62.0", - "@rollup/rollup-openharmony-arm64": "4.62.0", - "@rollup/rollup-win32-arm64-msvc": "4.62.0", - "@rollup/rollup-win32-ia32-msvc": "4.62.0", - "@rollup/rollup-win32-x64-gnu": "4.62.0", - "@rollup/rollup-win32-x64-msvc": "4.62.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/source-map-js": { @@ -1155,6 +997,31 @@ "node": ">=0.10.0" } }, + "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/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", + "optional": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1170,21 +1037,23 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -1193,23 +1062,33 @@ "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" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -1226,6 +1105,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } } diff --git a/desktop/package.json b/desktop/package.json index 646ea0b..cd135eb 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build", + "build:sidecar": "node ../scripts/build-sidecar.mjs", "preview": "vite preview", "tauri": "tauri" }, @@ -16,6 +17,6 @@ "devDependencies": { "@tauri-apps/cli": "^2.0.0", "typescript": "^5.6.3", - "vite": "^5.4.0" + "vite": "^8.0.16" } } diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 813ff29..1d7de91 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1781,6 +1781,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-shell", + "tokio", ] [[package]] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 7943a03..c1dd602 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -18,6 +18,9 @@ tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } +# Used directly for the sidecar-startup read timeout (tauri's async runtime is +# tokio, but a transitive dep is not usable directly). +tokio = { version = "1", features = ["time"] } [features] # default to a production build without devtools diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 28638b5..81af507 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -29,6 +29,7 @@ const STARTUP_TIMEOUT: Duration = Duration::from_secs(20); pub struct EngineManager { app: AppHandle, url: Mutex>, + token: Mutex>, child: Mutex>, } @@ -37,6 +38,7 @@ impl EngineManager { Self { app, url: Mutex::new(None), + token: Mutex::new(None), child: Mutex::new(None), } } @@ -46,6 +48,11 @@ impl EngineManager { self.url.lock().unwrap().clone() } + /// The per-process bearer token the engine requires on its API. + pub fn token(&self) -> Option { + self.token.lock().unwrap().clone() + } + /// Spawns the sidecar and blocks until it reports its listening port. pub fn start(&self) -> Result { let data_dir = self.app.path().app_data_dir().map_err(|e| e.to_string())?; @@ -73,8 +80,8 @@ impl EngineManager { .spawn() .map_err(|e| e.to_string())?; - let url = match tauri::async_runtime::block_on(read_listening_url(&mut rx)) { - Ok(url) => url, + let ready = match tauri::async_runtime::block_on(read_ready(&mut rx)) { + Ok(ready) => ready, Err(e) => { // Readiness failed (bad output or timeout): don't leak the // spawned process — kill it before surfacing the error. @@ -87,9 +94,10 @@ impl EngineManager { // engine during a long run. tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); - *self.url.lock().unwrap() = Some(url.clone()); + *self.url.lock().unwrap() = Some(ready.url.clone()); + *self.token.lock().unwrap() = ready.token; *self.child.lock().unwrap() = Some(child); - Ok(url) + Ok(ready.url) } /// Kills the running sidecar (if any) and starts a fresh one. Used after @@ -99,12 +107,19 @@ impl EngineManager { let _ = child.kill(); } *self.url.lock().unwrap() = None; + *self.token.lock().unwrap() = None; self.start() } } -/// Reads sidecar output until the readiness line is seen, then returns the URL. -async fn read_listening_url(rx: &mut Receiver) -> Result { +/// The engine's startup handshake: where to reach it and the token to use. +struct Ready { + url: String, + token: Option, +} + +/// Reads sidecar output until the readiness line is seen, then returns it. +async fn read_ready(rx: &mut Receiver) -> Result { let deadline = Instant::now() + STARTUP_TIMEOUT; let mut buf = String::new(); @@ -121,8 +136,8 @@ async fn read_listening_url(rx: &mut Receiver) -> Result { buf.push_str(&String::from_utf8_lossy(&bytes)); - if let Some(url) = parse_listening(&buf) { - return Ok(url); + if let Some(ready) = parse_ready(&buf) { + return Ok(ready); } } Some(CommandEvent::Error(e)) => return Err(format!("engine error: {e}")), @@ -136,7 +151,7 @@ async fn read_listening_url(rx: &mut Receiver) -> Result Option { +fn parse_ready(buf: &str) -> Option { for line in buf.lines() { let line = line.trim(); if !line.starts_with('{') { @@ -149,7 +164,11 @@ fn parse_listening(buf: &str) -> Option { .and_then(|x| x.as_str()) .unwrap_or("127.0.0.1"); let port = v.get("port").and_then(|x| x.as_u64())?; - return Some(format!("http://{host}:{port}")); + let token = v.get("token").and_then(|x| x.as_str()).map(str::to_string); + return Some(Ready { + url: format!("http://{host}:{port}"), + token, + }); } } } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index bce68c7..ce04030 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -17,6 +17,14 @@ fn engine_url(state: tauri::State) -> Result { state.url().ok_or_else(|| "engine not started".to_string()) } +/// Returns the per-process bearer token the engine API requires. +#[tauri::command] +fn engine_token(state: tauri::State) -> Result { + state + .token() + .ok_or_else(|| "engine not started".to_string()) +} + /// Restarts the engine sidecar (e.g. after secrets change) and returns its URL. #[tauri::command] fn restart_engine(state: tauri::State) -> Result { @@ -55,6 +63,7 @@ pub fn run() { }) .invoke_handler(tauri::generate_handler![ engine_url, + engine_token, restart_engine, set_secret, delete_secret, diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index e2aca55..da2326d 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -5,8 +5,8 @@ "build": { "frontendDist": "../dist", "devUrl": "http://localhost:1420", - "beforeDevCommand": "npm run dev", - "beforeBuildCommand": "npm run build" + "beforeDevCommand": "npm run build:sidecar && npm run dev", + "beforeBuildCommand": "npm run build:sidecar && npm run build" }, "app": { "windows": [ diff --git a/desktop/src/api.ts b/desktop/src/api.ts index b4f9c4c..8b8a590 100644 --- a/desktop/src/api.ts +++ b/desktop/src/api.ts @@ -16,6 +16,7 @@ export function isTauri(): boolean { } let cachedBase: string | undefined; +let cachedToken: string | undefined; async function tauriInvoke(cmd: string, args?: Record): Promise { const { invoke } = await import("@tauri-apps/api/core"); @@ -35,14 +36,38 @@ export async function apiBase(): Promise { return cachedBase; } +/** + * The per-process bearer token guarding the engine API. In Tauri it comes from + * a command (never crosses an origin boundary); in the browser build it is + * injected into the served index.html as `window.__LOOPWRIGHT_TOKEN__`, so only + * a page actually loaded from the loopback server can read it. + */ +async function authToken(): Promise { + if (cachedToken !== undefined) return cachedToken; + if (isTauri()) { + cachedToken = await tauriInvoke("engine_token"); + } else { + cachedToken = (window as unknown as { __LOOPWRIGHT_TOKEN__?: string }).__LOOPWRIGHT_TOKEN__ ?? ""; + } + return cachedToken; +} + +async function authHeaders(extra: Record = {}): Promise> { + const t = await authToken(); + return t ? { ...extra, authorization: `Bearer ${t}` } : extra; +} + /** Re-spawns the engine sidecar (Tauri only) so newly stored secrets apply. */ export async function restartEngine(): Promise { if (!isTauri()) return; + // A restart yields a fresh process with a new token + port; drop both caches + // so they are re-resolved on the next request. cachedBase = await tauriInvoke("restart_engine"); + cachedToken = undefined; } async function getJson(path: string): Promise { - const res = await fetch((await apiBase()) + path); + const res = await fetch((await apiBase()) + path, { headers: await authHeaders() }); if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); return res.json() as Promise; } @@ -57,7 +82,7 @@ export interface StartRunBody { export async function startRun(body: StartRunBody): Promise { const res = await fetch((await apiBase()) + "/api/runs", { method: "POST", - headers: { "content-type": "application/json" }, + headers: await authHeaders({ "content-type": "application/json" }), body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); @@ -93,8 +118,16 @@ export async function openStream( onMessage: (msg: RunMessage) => void, onError?: (err: Event) => void, ): Promise<() => void> { - const url = (await apiBase()) + `/api/runs/${encodeURIComponent(sessionId)}/stream`; - const es = new EventSource(url); + // EventSource cannot set headers, so the token rides as a query param (the + // server accepts it either way). Resolve against the document base so a + // same-origin ("") base still produces an absolute URL. + const t = await authToken(); + const url = new URL( + (await apiBase()) + `/api/runs/${encodeURIComponent(sessionId)}/stream`, + typeof window !== "undefined" ? window.location.href : "http://127.0.0.1", + ); + if (t) url.searchParams.set("token", t); + const es = new EventSource(url.toString()); const types: RunMessage["type"][] = ["status", "transition", "attempt", "outcome", "event", "log"]; for (const type of types) { es.addEventListener(type, (ev) => { diff --git a/package-lock.json b/package-lock.json index 0da5a2c..cd06d8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,46 @@ "@types/node": "^22.19.21", "tsx": "^4.19.2", "typescript": "^5.6.3", - "vitest": "^2.1.8" + "vitest": "^4.1.8" }, "engines": { "node": ">=22" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -469,24 +503,39 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", - "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", - "cpu": [ - "arm" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", - "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -495,12 +544,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", - "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -509,12 +561,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", - "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -523,26 +578,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", - "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", - "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -551,26 +595,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", - "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", - "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -579,26 +612,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", - "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", - "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -607,54 +629,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", - "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", - "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", - "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", - "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -663,40 +663,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", - "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", - "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", - "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -705,12 +680,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", - "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -719,12 +697,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", - "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -733,26 +714,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", - "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", - "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -761,40 +731,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", - "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", - "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", - "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -803,21 +784,53 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", - "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", @@ -837,38 +850,40 @@ } }, "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==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.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==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -880,70 +895,68 @@ } }, "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==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.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==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "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==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "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==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "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==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -959,75 +972,37 @@ "node": ">=12" } }, - "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/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "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/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } + "license": "MIT" }, - "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==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": ">=8" } }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -1093,6 +1068,24 @@ "node": ">=12.0.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/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1108,66 +1101,317 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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/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==", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "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.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "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==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">= 14.16" + "node": ">=12.20.0" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1175,6 +1419,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -1204,49 +1461,38 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/rollup": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", - "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.0", - "@rollup/rollup-android-arm64": "4.62.0", - "@rollup/rollup-darwin-arm64": "4.62.0", - "@rollup/rollup-darwin-x64": "4.62.0", - "@rollup/rollup-freebsd-arm64": "4.62.0", - "@rollup/rollup-freebsd-x64": "4.62.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", - "@rollup/rollup-linux-arm-musleabihf": "4.62.0", - "@rollup/rollup-linux-arm64-gnu": "4.62.0", - "@rollup/rollup-linux-arm64-musl": "4.62.0", - "@rollup/rollup-linux-loong64-gnu": "4.62.0", - "@rollup/rollup-linux-loong64-musl": "4.62.0", - "@rollup/rollup-linux-ppc64-gnu": "4.62.0", - "@rollup/rollup-linux-ppc64-musl": "4.62.0", - "@rollup/rollup-linux-riscv64-gnu": "4.62.0", - "@rollup/rollup-linux-riscv64-musl": "4.62.0", - "@rollup/rollup-linux-s390x-gnu": "4.62.0", - "@rollup/rollup-linux-x64-gnu": "4.62.0", - "@rollup/rollup-linux-x64-musl": "4.62.0", - "@rollup/rollup-openbsd-x64": "4.62.0", - "@rollup/rollup-openharmony-arm64": "4.62.0", - "@rollup/rollup-win32-arm64-msvc": "4.62.0", - "@rollup/rollup-win32-ia32-msvc": "4.62.0", - "@rollup/rollup-win32-x64-gnu": "4.62.0", - "@rollup/rollup-win32-x64-msvc": "4.62.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/siginfo": { @@ -1274,9 +1520,9 @@ "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==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -1288,42 +1534,50 @@ "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/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==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=18" } }, - "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==", + "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": ">=14.0.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "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", + "optional": true + }, "node_modules/tsx": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", @@ -1365,21 +1619,23 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -1388,23 +1644,33 @@ "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" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -1421,515 +1687,89 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "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==", + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "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" + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" }, "bin": { - "vite-node": "vite-node.mjs" + "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/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" + "node": "^20.0.0 || ^22.0.0 || >=24.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", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -1940,6 +1780,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/package.json b/package.json index fdeb539..828885e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,6 @@ "@types/node": "^22.19.21", "tsx": "^4.19.2", "typescript": "^5.6.3", - "vitest": "^2.1.8" + "vitest": "^4.1.8" } } diff --git a/src/server/index.ts b/src/server/index.ts index cf1991f..e98c410 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -5,19 +5,21 @@ * machine-readable line once listening, so a parent process (the Tauri shell) * can discover the bound port: * - * {"loopwright":"listening","host":"127.0.0.1","port":53187} + * {"loopwright":"listening","host":"127.0.0.1","port":53187,"token":"…"} * * Configuration comes entirely from the environment (see config.ts). Bind * settings: * LOOPWRIGHT_PORT port to bind (default 0 = ephemeral) * LOOPWRIGHT_HOST host to bind (default 127.0.0.1, loopback only) * LOOPWRIGHT_STATIC_DIR optional dir of built frontend assets to serve + * LOOPWRIGHT_TOKEN bearer token required on /api (default: random) * * Compiled to a single binary via `bun build --compile` and shipped as a * Tauri sidecar; also runnable directly (`npm run serve`) to use the UI from a * browser without any desktop toolchain. */ import path from "node:path"; +import { randomUUID } from "node:crypto"; import { loadConfig } from "../config.js"; import { openStore } from "../storage/store.js"; import { createServer } from "./server.js"; @@ -30,9 +32,13 @@ async function main(): Promise { // when a relative LOOPWRIGHT_STATIC_DIR is supplied. const staticEnv = process.env.LOOPWRIGHT_STATIC_DIR; const staticDir = staticEnv ? path.resolve(staticEnv) : undefined; + // The supervising process (Tauri) may pin the token via env; otherwise a + // fresh random one is generated and reported on the readiness line. + const token = process.env.LOOPWRIGHT_TOKEN || randomUUID(); const server = createServer({ store, config, + token, ...(staticDir ? { staticDir } : {}), }); @@ -41,7 +47,7 @@ async function main(): Promise { const bound = await server.start(port, host); // Single-line, parseable readiness signal for the supervising process. - console.log(JSON.stringify({ loopwright: "listening", host, port: bound })); + console.log(JSON.stringify({ loopwright: "listening", host, port: bound, token })); const shutdown = (): void => { void server.stop().then(() => process.exit(0)); diff --git a/src/server/server.ts b/src/server/server.ts index c31f6d8..d8ca9de 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,7 +1,7 @@ -import { randomUUID } from "node:crypto"; +import { randomUUID, timingSafeEqual } from "node:crypto"; import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { createReadStream } from "node:fs"; -import { stat } from "node:fs/promises"; +import { readFile, stat } from "node:fs/promises"; import path from "node:path"; import { loadConfig, type LoopwrightConfig } from "../config.js"; import { runGoal as defaultRunGoal, type RunGoalOptions, type SessionResult } from "../session.js"; @@ -18,9 +18,17 @@ import { tapStoreEvents } from "./store-tap.js"; * This is a thin transport over the headless engine: it exposes `runGoal` and * `buildTrace` over HTTP and streams a run's live progress over Server-Sent * Events. It adds NO orchestration policy — start, observe, and review map - * directly onto existing engine entry points (Req 13.3). The Tauri shell runs - * this as a sidecar, but it is equally usable from a browser, which is what - * makes the desktop experience reproducible without a GUI toolchain. + * directly onto existing engine entry points (Req 13.3). + * + * Security model: a run accepts per-request runner profiles, which can spawn + * local processes (`cli` runners) and forward stored secrets to arbitrary + * endpoints. The server therefore binds loopback only and guards every `/api` + * route (except health) with TWO layers: + * 1. an unguessable per-process bearer token, delivered out-of-band to the + * trusted UI (a Tauri command, or injected into the served index.html), + * so a page on another origin can never obtain it; + * 2. a CORS origin allowlist (loopback + the Tauri webview), so cross-site + * pages get no CORS grant even before the token check. */ /** Body accepted by POST /api/runs. */ @@ -57,12 +65,20 @@ export interface CreateServerOptions { staticDir?: string; /** base env for per-run config resolution (defaults to process.env) */ baseEnv?: Record; + /** + * Bearer token required on every `/api` route except health. Defaults to a + * fresh random token; pass a fixed value (e.g. in tests, or from the Tauri + * shell) to control it. + */ + token?: string; } export interface LoopwrightServer { /** the underlying http.Server (call .listen yourself, or use start()) */ http: Server; hub: RunHub; + /** the per-process auth token clients must present */ + token: string; /** listen on a port (0 = ephemeral) and resolve with the bound port */ start(port?: number, host?: string): Promise; stop(): Promise; @@ -70,20 +86,41 @@ export interface LoopwrightServer { const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" }; -function send(res: ServerResponse, status: number, body: unknown): void { +function send(res: ServerResponse, status: number, body: unknown, cors: Record = {}): void { const payload = JSON.stringify(body); - res.writeHead(status, { ...JSON_HEADERS, ...corsHeaders() }); + res.writeHead(status, { ...JSON_HEADERS, ...cors }); res.end(payload); } -function corsHeaders(): Record { - // The Tauri webview origin (tauri://localhost or http://localhost) differs - // from the sidecar origin, and browser dev may serve the UI elsewhere. This - // server only ever binds to loopback, so a permissive CORS policy is safe. +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]); + +/** Loopback and the Tauri webview are the only origins ever granted CORS. */ +function originAllowed(origin: string): boolean { + if (origin === "tauri://localhost") return true; + try { + const u = new URL(origin); + if (u.hostname === "tauri.localhost") return true; + return LOOPBACK_HOSTS.has(u.hostname); + } catch { + return false; + } +} + +/** + * CORS headers for a request. Same-origin / non-browser requests (no Origin) + * need none; cross-origin requests get a grant only for allowlisted origins, + * and the specific origin is echoed rather than `*` so the policy is explicit. + */ +function corsHeadersFor(req: IncomingMessage): Record { + const origin = req.headers.origin; + if (typeof origin !== "string") return {}; + if (!originAllowed(origin)) return {}; return { - "access-control-allow-origin": "*", + "access-control-allow-origin": origin, + vary: "Origin", "access-control-allow-methods": "GET,POST,OPTIONS", - "access-control-allow-headers": "content-type,last-event-id", + "access-control-allow-headers": "authorization,content-type,last-event-id", + "access-control-max-age": "600", }; } @@ -116,8 +153,21 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const runGoalImpl = opts.runGoalImpl ?? defaultRunGoal; const baseEnv = opts.baseEnv ?? process.env; const rates = opts.rates ?? {}; + const token = opts.token ?? randomUUID(); const hub = new RunHub(); + /** Constant-time bearer-token check (header or `token` query param). */ + function authorized(req: IncomingMessage, url: URL): boolean { + const auth = req.headers.authorization; + let provided: string | undefined; + if (typeof auth === "string" && auth.startsWith("Bearer ")) provided = auth.slice(7); + else provided = url.searchParams.get("token") ?? undefined; + if (!provided) return false; + const a = Buffer.from(provided); + const b = Buffer.from(token); + return a.length === b.length && timingSafeEqual(a, b); + } + const http = createHttpServer((req, res) => { handle(req, res).catch((err) => { if (!res.headersSent) send(res, 500, { error: String(err?.message ?? err) }); @@ -129,61 +179,71 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const method = req.method ?? "GET"; const url = new URL(req.url ?? "/", "http://localhost"); const pathname = url.pathname; + const cors = corsHeadersFor(req); if (method === "OPTIONS") { - res.writeHead(204, corsHeaders()); + res.writeHead(204, cors); res.end(); return; } + // Health is intentionally unauthenticated (no sensitive data) so the UI can + // show connectivity before it has resolved the token. if (pathname === "/api/health") { - return send(res, 200, { ok: true }); + return send(res, 200, { ok: true }, cors); + } + + // Everything else under /api requires the token. This is the boundary that + // stops a page on another origin from starting runs (which can execute + // local commands and exfiltrate secrets via runner profiles). + if (pathname.startsWith("/api/")) { + if (!authorized(req, url)) return send(res, 401, { error: "unauthorized" }, cors); } if (pathname === "/api/runs" && method === "POST") { - return startRun(req, res); + return startRun(req, res, cors); } const streamMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stream$/); if (streamMatch && method === "GET") { - return streamRun(req, res, decodeURIComponent(streamMatch[1] as string)); + return streamRun(req, res, decodeURIComponent(streamMatch[1] as string), cors); } if (pathname === "/api/sessions" && method === "GET") { const sessions = await store.listSessions(); sessions.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); - return send(res, 200, { sessions }); + return send(res, 200, { sessions }, cors); } const traceMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/trace$/); if (traceMatch && method === "GET") { const id = decodeURIComponent(traceMatch[1] as string); const trace = await buildTrace(store, id, rates); - if (!trace.session) return send(res, 404, { error: `no session "${id}"` }); + if (!trace.session) return send(res, 404, { error: `no session "${id}"` }, cors); return send(res, 200, { trace, text: formatTrace(trace), phase: hub.phase(id) ?? null, - }); + }, cors); } if (pathname.startsWith("/api/")) { - return send(res, 404, { error: "not found" }); + return send(res, 404, { error: "not found" }, cors); } - if (staticDir) return serveStatic(res, pathname, staticDir); - return send(res, 404, { error: "not found" }); + if (staticDir) return serveStatic(res, pathname, staticDir, cors); + return send(res, 404, { error: "not found" }, cors); } - async function startRun(req: IncomingMessage, res: ServerResponse): Promise { + async function startRun(req: IncomingMessage, res: ServerResponse, cors: Record): Promise { let body: StartRunBody; try { body = ((await readJsonBody(req)) ?? {}) as StartRunBody; } catch (err) { - return send(res, 400, { error: `invalid JSON body: ${String((err as Error).message)}` }); + return send(res, 400, { error: `invalid JSON body: ${String((err as Error).message)}` }, cors); } const goal = (body.goal ?? "").trim(); - if (!goal) return send(res, 400, { error: "goal is required" }); + if (!goal) return send(res, 400, { error: "goal is required" }, cors); // Resolve per-run config from the merged env, but never let a caller // redirect persistence away from the server's store. @@ -193,13 +253,13 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { try { runConfig = loadConfig(mergedEnv); } catch (err) { - return send(res, 400, { error: `invalid config: ${String((err as Error).message)}` }); + return send(res, 400, { error: `invalid config: ${String((err as Error).message)}` }, cors); } runConfig.dbPath = config.dbPath; const sessionId = body.sessionId ?? randomUUID(); if (hub.has(sessionId) && hub.phase(sessionId) === "running") { - return send(res, 409, { error: `session ${sessionId} is already running` }); + return send(res, 409, { error: `session ${sessionId} is already running` }, cors); } // Live wiring: the observer streams transitions/attempts/outcomes; the @@ -233,15 +293,20 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { } satisfies RunStatusData); }); - send(res, 202, { sessionId }); + send(res, 202, { sessionId }, cors); } - function streamRun(req: IncomingMessage, res: ServerResponse, sessionId: string): void { + function streamRun( + req: IncomingMessage, + res: ServerResponse, + sessionId: string, + cors: Record, + ): void { // Unknown session: don't let an SSE connection implicitly create a channel // (which would reserve the id). Report 404 so the client can fall back to // the trace endpoint for a past run. if (!hub.has(sessionId)) { - return send(res, 404, { error: `no active run for session "${sessionId}"` }); + return send(res, 404, { error: `no active run for session "${sessionId}"` }, cors); } res.writeHead(200, { @@ -249,7 +314,7 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no", - ...corsHeaders(), + ...cors, }); // Resume support: a reconnecting client passes the id of the last message @@ -281,7 +346,12 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { res.on("close", close); } - async function serveStatic(res: ServerResponse, pathname: string, dir: string): Promise { + async function serveStatic( + res: ServerResponse, + pathname: string, + dir: string, + cors: Record, + ): Promise { // Normalize the asset root to an absolute path so the traversal guard below // compares like with like even when `dir` came in relative (e.g. a relative // LOOPWRIGHT_STATIC_DIR), which would otherwise reject every request. @@ -290,14 +360,27 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const resolved = path.resolve(root, rel); // Path-traversal guard: never serve outside the asset directory. if (resolved !== root && !resolved.startsWith(root + path.sep)) { - return send(res, 403, { error: "forbidden" }); + return send(res, 403, { error: "forbidden" }, cors); } const file = await resolveFile(resolved, root); - if (!file) return send(res, 404, { error: "not found" }); + if (!file) return send(res, 404, { error: "not found" }, cors); + + // index.html is served same-origin to the trusted UI; inject the auth token + // so the browser build can authenticate without an unauthenticated token + // endpoint that another origin could read. + if (path.basename(file) === "index.html") { + const html = await readFile(file, "utf8"); + const tag = ``; + const injected = html.includes("") ? html.replace("", `${tag}`) : tag + html; + res.writeHead(200, { "content-type": "text/html; charset=utf-8", ...cors }); + res.end(injected); + return; + } + const ext = path.extname(file).toLowerCase(); res.writeHead(200, { "content-type": STATIC_CONTENT_TYPES[ext] ?? "application/octet-stream", - ...corsHeaders(), + ...cors, }); createReadStream(file).pipe(res); } @@ -323,6 +406,7 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { return { http, hub, + token, start(port = 0, host = "127.0.0.1"): Promise { return new Promise((resolve, reject) => { http.once("error", reject); diff --git a/test/server.test.ts b/test/server.test.ts index cd80edc..49cc856 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -11,10 +11,15 @@ import type { TaskOutcome } from "../src/engine/loop.js"; * here we verify the HTTP/SSE surface that the desktop shell consumes — start a * run, observe it live, and read its trace — using a simulated `runGoal` that * drives the exact hooks the real engine uses (store events, observer - * transitions/outcomes, log lines). + * transitions/outcomes, log lines). A fixed token exercises the auth boundary. */ const baseConfig: LoopwrightConfig = loadConfig({}); +const TOKEN = "test-token"; +const auth = (extra: Record = {}): Record => ({ + ...extra, + authorization: `Bearer ${TOKEN}`, +}); function greenOutcome(taskId: string): TaskOutcome { return { @@ -79,11 +84,22 @@ let server: LoopwrightServer; let base: string; async function startServer(runGoalImpl: RunGoalImpl = simulatedRun): Promise { - server = createServer({ store: new MemoryStore(), config: baseConfig, runGoalImpl, baseEnv: {} }); + server = createServer({ store: new MemoryStore(), config: baseConfig, runGoalImpl, baseEnv: {}, token: TOKEN }); const port = await server.start(0); base = `http://127.0.0.1:${port}`; } +/** Starts a run and returns its session id (with auth). */ +async function startRun(goal: string): Promise { + const res = await fetch(`${base}/api/runs`, { + method: "POST", + headers: auth({ "content-type": "application/json" }), + body: JSON.stringify({ goal }), + }); + expect(res.status).toBe(202); + return ((await res.json()) as { sessionId: string }).sessionId; +} + afterEach(async () => { await server?.stop(); }); @@ -125,7 +141,7 @@ async function readSse( describe("server: health + validation", () => { beforeEach(() => startServer()); - it("reports health", async () => { + it("reports health without a token", async () => { const res = await fetch(`${base}/api/health`); expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true }); @@ -134,37 +150,54 @@ describe("server: health + validation", () => { it("rejects a run with no goal", async () => { const res = await fetch(`${base}/api/runs`, { method: "POST", - headers: { "content-type": "application/json" }, + headers: auth({ "content-type": "application/json" }), body: JSON.stringify({ goal: " " }), }); expect(res.status).toBe(400); }); it("404s unknown api routes and missing traces", async () => { - expect((await fetch(`${base}/api/nope`)).status).toBe(404); - expect((await fetch(`${base}/api/sessions/ghost/trace`)).status).toBe(404); + expect((await fetch(`${base}/api/nope`, { headers: auth() })).status).toBe(404); + expect((await fetch(`${base}/api/sessions/ghost/trace`, { headers: auth() })).status).toBe(404); // Streaming a session that was never started must not implicitly create it. - expect((await fetch(`${base}/api/runs/ghost/stream`)).status).toBe(404); + expect((await fetch(`${base}/api/runs/ghost/stream`, { headers: auth() })).status).toBe(404); }); }); -describe("server: run lifecycle + trace", () => { +describe("server: auth boundary", () => { beforeEach(() => startServer()); - it("starts a run, persists it, and serves its trace + session list", async () => { - const res = await fetch(`${base}/api/runs`, { + it("rejects /api requests without the token", async () => { + expect((await fetch(`${base}/api/sessions`)).status).toBe(401); + const run = await fetch(`${base}/api/runs`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ goal: "ship it" }), + body: JSON.stringify({ goal: "do it" }), }); - expect(res.status).toBe(202); - const { sessionId } = (await res.json()) as { sessionId: string }; + expect(run.status).toBe(401); + // A wrong token is rejected too. + expect((await fetch(`${base}/api/sessions`, { headers: { authorization: "Bearer nope" } })).status).toBe(401); + }); + + it("accepts the token via query param (for EventSource)", async () => { + const sessionId = await startRun("observe"); + const res = await fetch(`${base}/api/runs/${sessionId}/stream?token=${TOKEN}`); + expect(res.status).toBe(200); + await res.body?.cancel(); + }); +}); + +describe("server: run lifecycle + trace", () => { + beforeEach(() => startServer()); + + it("starts a run, persists it, and serves its trace + session list", async () => { + const sessionId = await startRun("ship it"); expect(sessionId).toBeTruthy(); // The simulated run is synchronous in effect; poll the trace until done. let trace: any; for (let i = 0; i < 50; i++) { - const t = await fetch(`${base}/api/sessions/${sessionId}/trace`); + const t = await fetch(`${base}/api/sessions/${sessionId}/trace`, { headers: auth() }); if (t.status === 200) { trace = await t.json(); if (trace.trace.session?.status === "completed") break; @@ -178,7 +211,7 @@ describe("server: run lifecycle + trace", () => { expect(trace.trace.usage.total.calls).toBe(1); expect(typeof trace.text).toBe("string"); - const list = (await (await fetch(`${base}/api/sessions`)).json()) as { sessions: any[] }; + const list = (await (await fetch(`${base}/api/sessions`, { headers: auth() })).json()) as { sessions: any[] }; expect(list.sessions.some((s) => s.id === sessionId)).toBe(true); }); @@ -188,21 +221,15 @@ describe("server: run lifecycle + trace", () => { store: new MemoryStore(), config: baseConfig, baseEnv: {}, + token: TOKEN, runGoalImpl: async () => { throw new Error("boom"); }, }); base = `http://127.0.0.1:${await server.start(0)}`; - const { sessionId } = (await ( - await fetch(`${base}/api/runs`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ goal: "explode" }), - }) - ).json()) as { sessionId: string }; - - const stream = await fetch(`${base}/api/runs/${sessionId}/stream`); + const sessionId = await startRun("explode"); + const stream = await fetch(`${base}/api/runs/${sessionId}/stream`, { headers: auth() }); const msgs = await readSse(stream, (m) => m.some((x) => x.event === "status" && x.data.phase === "error")); const err = msgs.find((m) => m.event === "status" && m.data.phase === "error"); expect(err?.data.error).toContain("boom"); @@ -213,15 +240,9 @@ describe("server: live SSE stream", () => { beforeEach(() => startServer()); it("replays buffered messages then signals done, in order with monotonic ids", async () => { - const { sessionId } = (await ( - await fetch(`${base}/api/runs`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ goal: "observe me" }), - }) - ).json()) as { sessionId: string }; - - const stream = await fetch(`${base}/api/runs/${sessionId}/stream`); + const sessionId = await startRun("observe me"); + + const stream = await fetch(`${base}/api/runs/${sessionId}/stream`, { headers: auth() }); const msgs = await readSse(stream, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); const types = msgs.map((m) => m.event); @@ -241,22 +262,16 @@ describe("server: live SSE stream", () => { }); it("resumes from Last-Event-ID, skipping already-seen messages", async () => { - const { sessionId } = (await ( - await fetch(`${base}/api/runs`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ goal: "resume me" }), - }) - ).json()) as { sessionId: string }; + const sessionId = await startRun("resume me"); // Let the run finish so the whole buffer exists. - const first = await fetch(`${base}/api/runs/${sessionId}/stream`); + const first = await fetch(`${base}/api/runs/${sessionId}/stream`, { headers: auth() }); const all = await readSse(first, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); expect(all.length).toBeGreaterThan(1); const cutoff = all[1]!.id; const resumed = await fetch(`${base}/api/runs/${sessionId}/stream`, { - headers: { "last-event-id": String(cutoff) }, + headers: auth({ "last-event-id": String(cutoff) }), }); const rest = await readSse(resumed, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); expect(rest.every((m) => m.id > cutoff)).toBe(true); From 96e01191d7618a168f328582694b830b6bf74abe Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 19:03:35 +0000 Subject: [PATCH 05/19] fix(server): harden token exposure, host binding, memory + admission control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second round of review findings. P1 — token leak in browser/static mode: static asset responses no longer carry CORS headers, so a page on another loopback origin can no longer fetch and read the token injected into index.html (the standalone UI loads same-origin and needs no CORS; the Tauri webview gets the token via the engine_token command). P1 — loopback-only enforcement: the entrypoint now refuses to bind a non-loopback LOOPWRIGHT_HOST unless LOOPWRIGHT_ALLOW_NON_LOOPBACK is set, so the token-authenticated API + injected token can't be exposed over the network by accident. Adds an exported isLoopbackHost helper. P2 — bounded, released event buffers: RunHub now caps retained messages per run (monotonic ids via a seq counter, so Last-Event-ID resume is unaffected), and the server releases a finished run's buffer after a grace period (hub.forget on a TTL) instead of holding it forever. P2 — admission control: a configurable cap on concurrent active runs returns 429 once exceeded, so repeated clicks / a buggy UI / a leaked token can't kick off unbounded background work. P3 — query-token scope: the ?token= query param is now accepted only on the SSE /stream route (EventSource can't set headers); every other route requires the Authorization header, keeping tokens out of URLs/logs. Validated: 140 tests pass (new hub buffer/forget tests + server tests for query-token restriction, the 429 cap, and static-without-CORS), tsc clean, and verified against the compiled sidecar (non-loopback refused; no ACAO on static; query token rejected off /stream). --- src/server/hub.ts | 23 ++++++++++++-- src/server/index.ts | 20 +++++++++++- src/server/server.ts | 76 ++++++++++++++++++++++++++++++++++---------- test/hub.test.ts | 31 ++++++++++++++++++ test/server.test.ts | 62 ++++++++++++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 test/hub.test.ts diff --git a/src/server/hub.ts b/src/server/hub.ts index 0a77a71..73f9176 100644 --- a/src/server/hub.ts +++ b/src/server/hub.ts @@ -23,7 +23,7 @@ export type RunMessageType = | "log"; // a human-readable log line from the engine export interface RunMessage { - /** 0-based index within the session, used as the SSE event id for resume. */ + /** Monotonic per-session ordinal, used as the SSE event id for resume. */ id: number; type: RunMessageType; data: unknown; @@ -45,13 +45,26 @@ interface Channel { buffer: RunMessage[]; emitter: EventEmitter; phase: RunPhase; + /** next id to assign; never reused even after the buffer is trimmed */ + seq: number; } const MESSAGE_EVENT = "message"; +/** Default cap on retained messages per session (bounds memory). */ +const DEFAULT_MAX_BUFFER = 2000; + export class RunHub { private readonly channels = new Map(); + /** + * @param maxBuffer most recent messages retained per session for replay. + * Older messages are dropped once exceeded; ids stay monotonic so + * `Last-Event-ID` resume is unaffected (a client that connects after a + * trim simply won't replay the dropped early messages). + */ + constructor(private readonly maxBuffer: number = DEFAULT_MAX_BUFFER) {} + private channel(sessionId: string): Channel { let ch = this.channels.get(sessionId); if (!ch) { @@ -59,7 +72,7 @@ export class RunHub { // Each SSE client adds a listener; a long run with several open monitor // tabs would otherwise trip Node's default 10-listener leak warning. emitter.setMaxListeners(0); - ch = { buffer: [], emitter, phase: "running" }; + ch = { buffer: [], emitter, phase: "running", seq: 0 }; this.channels.set(sessionId, ch); } return ch; @@ -77,8 +90,12 @@ export class RunHub { /** Publishes a message to a session, assigning it the next ordinal id. */ publish(sessionId: string, type: RunMessageType, data: unknown): RunMessage { const ch = this.channel(sessionId); - const msg: RunMessage = { id: ch.buffer.length, type, data }; + const msg: RunMessage = { id: ch.seq++, type, data }; ch.buffer.push(msg); + // Bound memory: retain only the most recent `maxBuffer` messages. + if (ch.buffer.length > this.maxBuffer) { + ch.buffer.splice(0, ch.buffer.length - this.maxBuffer); + } if (type === "status") { const phase = (data as RunStatusData).phase; if (phase) ch.phase = phase; diff --git a/src/server/index.ts b/src/server/index.ts index e98c410..c459171 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -13,6 +13,7 @@ * LOOPWRIGHT_HOST host to bind (default 127.0.0.1, loopback only) * LOOPWRIGHT_STATIC_DIR optional dir of built frontend assets to serve * LOOPWRIGHT_TOKEN bearer token required on /api (default: random) + * LOOPWRIGHT_ALLOW_NON_LOOPBACK opt-in to bind a non-loopback host (unsafe) * * Compiled to a single binary via `bun build --compile` and shipped as a * Tauri sidecar; also runnable directly (`npm run serve`) to use the UI from a @@ -22,7 +23,11 @@ import path from "node:path"; import { randomUUID } from "node:crypto"; import { loadConfig } from "../config.js"; import { openStore } from "../storage/store.js"; -import { createServer } from "./server.js"; +import { createServer, isLoopbackHost } from "./server.js"; + +function envFlag(v: string | undefined): boolean { + return v !== undefined && ["1", "true", "yes", "on"].includes(v.trim().toLowerCase()); +} async function main(): Promise { const config = loadConfig(); @@ -44,6 +49,19 @@ async function main(): Promise { const port = Number.parseInt(process.env.LOOPWRIGHT_PORT ?? "0", 10) || 0; const host = process.env.LOOPWRIGHT_HOST ?? "127.0.0.1"; + + // The engine serves a token-authenticated local API (and, with a static dir, + // injects that token into index.html). Binding a non-loopback host would + // expose both over the network, contradicting the security model — refuse + // unless explicitly opted in. + if (!isLoopbackHost(host) && !envFlag(process.env.LOOPWRIGHT_ALLOW_NON_LOOPBACK)) { + console.error( + `Refusing to bind non-loopback host "${host}". The engine exposes a local, ` + + `token-authenticated API. Set LOOPWRIGHT_ALLOW_NON_LOOPBACK=1 to override (unsafe).`, + ); + process.exit(1); + } + const bound = await server.start(port, host); // Single-line, parseable readiness signal for the supervising process. diff --git a/src/server/server.ts b/src/server/server.ts index d8ca9de..9f8e9df 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -71,6 +71,12 @@ export interface CreateServerOptions { * shell) to control it. */ token?: string; + /** max runs that may be active concurrently before new ones get 429 */ + maxActiveRuns?: number; + /** how long (ms) to retain a finished run's event buffer before releasing it */ + retainMs?: number; + /** most recent messages retained per run for SSE replay */ + maxBufferPerRun?: number; } export interface LoopwrightServer { @@ -94,6 +100,11 @@ function send(res: ServerResponse, status: number, body: unknown, cors: Record= maxActiveRuns) { + return send( + res, + 429, + { error: `too many active runs (max ${maxActiveRuns}); wait for one to finish` }, + cors, + ); + } // Live wiring: the observer streams transitions/attempts/outcomes; the // tapping store streams lifecycle + runner-call events; `log` streams the @@ -272,8 +304,18 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { }; const tappedStore = tapStoreEvents(store, (rec) => hub.publish(sessionId, "event", rec)); + activeRuns += 1; hub.publish(sessionId, "status", { phase: "running" } satisfies RunStatusData); + // Frees the active-run slot and, after a grace period, releases the run's + // in-memory event buffer (late viewers can still catch up until then; the + // durable trace remains available from the store afterwards). + const settle = (): void => { + activeRuns = Math.max(0, activeRuns - 1); + const timer = setTimeout(() => hub.forget(sessionId), retainMs); + if (typeof timer.unref === "function") timer.unref(); + }; + // Fire-and-forget: the run proceeds in the background and the client // follows it over SSE. Errors are surfaced as a terminal status message. void runGoalImpl(goal, runConfig, { @@ -285,12 +327,14 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { }) .then((result) => { hub.publish(sessionId, "status", { phase: "done", result } satisfies RunStatusData); + settle(); }) .catch((err: unknown) => { hub.publish(sessionId, "status", { phase: "error", error: String((err as Error)?.message ?? err), } satisfies RunStatusData); + settle(); }); send(res, 202, { sessionId }, cors); @@ -346,12 +390,11 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { res.on("close", close); } - async function serveStatic( - res: ServerResponse, - pathname: string, - dir: string, - cors: Record, - ): Promise { + async function serveStatic(res: ServerResponse, pathname: string): Promise { + // Static assets are served WITHOUT CORS headers. The standalone browser UI + // loads them same-origin (no CORS needed), and withholding CORS stops a + // page on another origin from reading the injected token out of index.html. + const dir = staticDir as string; // Normalize the asset root to an absolute path so the traversal guard below // compares like with like even when `dir` came in relative (e.g. a relative // LOOPWRIGHT_STATIC_DIR), which would otherwise reject every request. @@ -360,10 +403,10 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const resolved = path.resolve(root, rel); // Path-traversal guard: never serve outside the asset directory. if (resolved !== root && !resolved.startsWith(root + path.sep)) { - return send(res, 403, { error: "forbidden" }, cors); + return send(res, 403, { error: "forbidden" }); } const file = await resolveFile(resolved, root); - if (!file) return send(res, 404, { error: "not found" }, cors); + if (!file) return send(res, 404, { error: "not found" }); // index.html is served same-origin to the trusted UI; inject the auth token // so the browser build can authenticate without an unauthenticated token @@ -372,7 +415,7 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const html = await readFile(file, "utf8"); const tag = ``; const injected = html.includes("") ? html.replace("", `${tag}`) : tag + html; - res.writeHead(200, { "content-type": "text/html; charset=utf-8", ...cors }); + res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(injected); return; } @@ -380,7 +423,6 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const ext = path.extname(file).toLowerCase(); res.writeHead(200, { "content-type": STATIC_CONTENT_TYPES[ext] ?? "application/octet-stream", - ...cors, }); createReadStream(file).pipe(res); } diff --git a/test/hub.test.ts b/test/hub.test.ts new file mode 100644 index 0000000..3523c5d --- /dev/null +++ b/test/hub.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { RunHub } from "../src/server/hub.js"; + +describe("RunHub", () => { + it("assigns monotonic ids and bounds the replay buffer", () => { + const hub = new RunHub(3); // retain only the 3 most recent + for (let i = 0; i < 10; i++) hub.publish("s", "log", { i }); + + const seen: number[] = []; + hub.subscribe("s", (m) => seen.push(m.id)); // replays the retained buffer + // Older messages were dropped, but ids stayed monotonic. + expect(seen).toEqual([7, 8, 9]); + }); + + it("does not create a channel when subscribing to an unknown session", () => { + const hub = new RunHub(); + const unsub = hub.subscribe("ghost", () => { + throw new Error("listener should not fire"); + }); + unsub(); + expect(hub.has("ghost")).toBe(false); + }); + + it("forget releases the channel", () => { + const hub = new RunHub(); + hub.publish("s", "log", {}); + expect(hub.has("s")).toBe(true); + hub.forget("s"); + expect(hub.has("s")).toBe(false); + }); +}); diff --git a/test/server.test.ts b/test/server.test.ts index 49cc856..70f8a38 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -1,4 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { createServer, type LoopwrightServer, type RunGoalImpl } from "../src/server/server.js"; import { MemoryStore } from "../src/storage/store.js"; import { loadConfig, type LoopwrightConfig } from "../src/config.js"; @@ -185,6 +188,65 @@ describe("server: auth boundary", () => { expect(res.status).toBe(200); await res.body?.cancel(); }); + + it("does NOT accept a query token on non-stream routes", async () => { + // Query-token auth is restricted to /stream so tokens don't leak via URLs. + expect((await fetch(`${base}/api/sessions?token=${TOKEN}`)).status).toBe(401); + }); +}); + +describe("server: admission control", () => { + it("rejects new runs past the active cap with 429", async () => { + let release: () => void = () => {}; + const hold = new Promise((r) => { + release = r; + }); + server = createServer({ + store: new MemoryStore(), + config: baseConfig, + baseEnv: {}, + token: TOKEN, + maxActiveRuns: 1, + // First run stays active until released, holding the only slot. + runGoalImpl: async (g, c, opts = {}) => { + await hold; + return simulatedRun(g, c, opts); + }, + }); + base = `http://127.0.0.1:${await server.start(0)}`; + + const first = await fetch(`${base}/api/runs`, { + method: "POST", + headers: auth({ "content-type": "application/json" }), + body: JSON.stringify({ goal: "one" }), + }); + expect(first.status).toBe(202); + + const second = await fetch(`${base}/api/runs`, { + method: "POST", + headers: auth({ "content-type": "application/json" }), + body: JSON.stringify({ goal: "two" }), + }); + expect(second.status).toBe(429); + + release(); + }); +}); + +describe("server: static assets", () => { + it("serves index.html without CORS and injects the token", async () => { + const dir = mkdtempSync(path.join(os.tmpdir(), "lw-static-")); + writeFileSync(path.join(dir, "index.html"), "hi"); + server = createServer({ store: new MemoryStore(), config: baseConfig, baseEnv: {}, token: TOKEN, staticDir: dir }); + base = `http://127.0.0.1:${await server.start(0)}`; + + // A page on another loopback origin must not be able to read the token. + const res = await fetch(`${base}/`, { headers: { origin: "http://localhost:9999" } }); + expect(res.status).toBe(200); + expect(res.headers.get("access-control-allow-origin")).toBeNull(); + const html = await res.text(); + expect(html).toContain(`window.__LOOPWRIGHT_TOKEN__="${TOKEN}"`); + }); }); describe("server: run lifecycle + trace", () => { From 593537808134f668f3736094bcd1f2b9e88e96de Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 19:16:33 +0000 Subject: [PATCH 06/19] fix: stale-event replay on session reuse, integration status, store + cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third round of review findings. P1 — reused session id replayed stale live events: the hub now starts a fresh channel per run (RunHub.start) that discards the previous run's buffer and listeners, so reusing a finished session id can't replay the old run's events. A generation token guards the deferred cleanup timer so an old run's TTL forget can't drop a newer run on the same id. P1 — integration failure left the session "completed": final durable status is now decided AFTER integration via finalSessionStatus(), which downgrades to needs_human when integration.ok is false (conflicts / failed verification). The integration result is also recorded as an event for the trace/UI. P2 — corrupt/unreadable store silently discarded: JsonFileStore.open now treats only ENOENT as "start fresh"; permission/I/O errors propagate, and invalid JSON fails loud with the file left untouched for recovery (instead of being clobbered by the next write). P2 — token-bearing index.html was cacheable: it is now served with Cache-Control: no-store so a stale token can't linger in browser disk cache (hashed assets still cache normally). Validated: 145 tests pass (new: hub reuse stream isolation, finalSessionStatus matrix, corrupt-store refusal, static no-store + no-CORS), tsc clean, and the recompiled sidecar serves index.html with Cache-Control: no-store. --- src/observability/events.ts | 1 + src/server/hub.ts | 32 ++++++++++++++++++++++++---- src/server/server.ts | 13 +++++++++--- src/session.ts | 42 +++++++++++++++++++++++++++++++++---- src/storage/store.ts | 31 ++++++++++++++++++++------- test/server.test.ts | 38 +++++++++++++++++++++++++++++++++ test/session.test.ts | 19 ++++++++++++++++- test/store.test.ts | 13 +++++++++++- 8 files changed, 169 insertions(+), 20 deletions(-) diff --git a/src/observability/events.ts b/src/observability/events.ts index 2a6992c..3fce6f8 100644 --- a/src/observability/events.ts +++ b/src/observability/events.ts @@ -37,6 +37,7 @@ export const EVENT_TYPES = { planReviewed: "plan_reviewed", runnerCall: "runner_call", sessionFinished: "session_finished", + integration: "integration", } as const; function num(v: unknown): number { diff --git a/src/server/hub.ts b/src/server/hub.ts index 73f9176..76495d4 100644 --- a/src/server/hub.ts +++ b/src/server/hub.ts @@ -47,6 +47,8 @@ interface Channel { phase: RunPhase; /** next id to assign; never reused even after the buffer is trimmed */ seq: number; + /** bumped each time a fresh run (re)starts on this session id */ + generation: number; } const MESSAGE_EVENT = "message"; @@ -72,12 +74,28 @@ export class RunHub { // Each SSE client adds a listener; a long run with several open monitor // tabs would otherwise trip Node's default 10-listener leak warning. emitter.setMaxListeners(0); - ch = { buffer: [], emitter, phase: "running", seq: 0 }; + ch = { buffer: [], emitter, phase: "running", seq: 0, generation: 1 }; this.channels.set(sessionId, ch); } return ch; } + /** + * Begins a fresh run for a session, discarding any retained buffer and + * listeners from a previous (finished) run so a reused session id can never + * replay the old run's events. Returns a generation token that {@link forget} + * uses so a late cleanup timer from the old run can't drop the new channel. + */ + start(sessionId: string): number { + const existing = this.channels.get(sessionId); + const generation = (existing?.generation ?? 0) + 1; + if (existing) existing.emitter.removeAllListeners(); + const emitter = new EventEmitter(); + emitter.setMaxListeners(0); + this.channels.set(sessionId, { buffer: [], emitter, phase: "running", seq: 0, generation }); + return generation; + } + /** True once a run for this session has been registered (started). */ has(sessionId: string): boolean { return this.channels.has(sessionId); @@ -126,10 +144,16 @@ export class RunHub { return () => ch.emitter.off(MESSAGE_EVENT, listener); } - /** Drops a finished session's buffer to bound memory (optional cleanup). */ - forget(sessionId: string): void { + /** + * Drops a finished session's buffer to bound memory. When `generation` is + * given, only forgets if it still matches — so a cleanup timer scheduled by + * an old run won't delete a newer run that reused the same session id. + */ + forget(sessionId: string, generation?: number): void { const ch = this.channels.get(sessionId); - if (ch) ch.emitter.removeAllListeners(); + if (!ch) return; + if (generation !== undefined && ch.generation !== generation) return; + ch.emitter.removeAllListeners(); this.channels.delete(sessionId); } } diff --git a/src/server/server.ts b/src/server/server.ts index 9f8e9df..66d5611 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -305,14 +305,19 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const tappedStore = tapStoreEvents(store, (rec) => hub.publish(sessionId, "event", rec)); activeRuns += 1; + // Begin a fresh hub channel for this run: discards any retained buffer from + // a previous run that reused this session id (so the stream can't replay + // stale events) and yields a generation token for the cleanup guard below. + const generation = hub.start(sessionId); hub.publish(sessionId, "status", { phase: "running" } satisfies RunStatusData); // Frees the active-run slot and, after a grace period, releases the run's // in-memory event buffer (late viewers can still catch up until then; the - // durable trace remains available from the store afterwards). + // durable trace remains available from the store afterwards). The + // generation guard ensures this never drops a newer run on the same id. const settle = (): void => { activeRuns = Math.max(0, activeRuns - 1); - const timer = setTimeout(() => hub.forget(sessionId), retainMs); + const timer = setTimeout(() => hub.forget(sessionId, generation), retainMs); if (typeof timer.unref === "function") timer.unref(); }; @@ -415,7 +420,9 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const html = await readFile(file, "utf8"); const tag = ``; const injected = html.includes("") ? html.replace("", `${tag}`) : tag + html; - res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); + // index.html carries the live API token, so it must never be cached to + // disk (hashed asset files below can still cache normally). + res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }); res.end(injected); return; } diff --git a/src/session.ts b/src/session.ts index 6262f3d..f17f62f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -13,7 +13,7 @@ import { integrate, type IntegrationResult } from "./engine/integrator.js"; import { GitWorktreeManager } from "./workspace/worktrees.js"; import type { IntegrationBranch } from "./engine/integrator.js"; import type { CommandExecutor } from "./engine/mechanicalGate.js"; -import type { Store } from "./storage/store.js"; +import type { Store, SessionStatus } from "./storage/store.js"; import { storeObserver, combineObservers } from "./storage/checkpoint.js"; import type { RunnerCallSink } from "./observability/instrument.js"; import { EVENT_TYPES } from "./observability/events.js"; @@ -220,9 +220,6 @@ export async function runGoal( } if (store && sessionId) { - await store.updateSession(sessionId, { - status: needsHuman.length > 0 ? "needs_human" : "completed", - }); await store.recordEvent({ sessionId, at: new Date().toISOString(), @@ -261,6 +258,29 @@ export async function runGoal( for (const wt of wtManager.list()) await wtManager.release(wt.taskId); } + // Durable final status is decided LAST, so an integration that surfaced + // conflicts or failed verification (integration.ok === false) marks the + // session needs_human rather than leaving the earlier "completed" optimism. + if (store && sessionId) { + if (integration) { + await store.recordEvent({ + sessionId, + at: new Date().toISOString(), + type: EVENT_TYPES.integration, + data: { + ok: integration.ok, + merged: integration.merged, + conflicts: integration.conflicts, + integrationBranch: integration.integrationBranch, + verification: integration.verification ?? null, + }, + }); + } + await store.updateSession(sessionId, { + status: finalSessionStatus(needsHuman.length, integration), + }); + } + return { goal, ...(sessionId ? { sessionId } : {}), @@ -275,6 +295,20 @@ export async function runGoal( }; } +/** + * Decides the durable final session status. Integration that surfaced conflicts + * or failed verification (integration.ok === false) is blocking and downgrades + * an otherwise-complete session to needs_human. + */ +export function finalSessionStatus( + needsHumanCount: number, + integration?: { ok: boolean }, +): SessionStatus { + if (needsHumanCount > 0) return "needs_human"; + if (integration && !integration.ok) return "needs_human"; + return "completed"; +} + /** Flattens a session's still-open blocking findings for reporting. */ export function openBlockers(result: SessionResult): Finding[] { const out: Finding[] = []; diff --git a/src/storage/store.ts b/src/storage/store.ts index 81c0fc6..a322c9b 100644 --- a/src/storage/store.ts +++ b/src/storage/store.ts @@ -259,15 +259,32 @@ export class JsonFileStore extends BaseStore { /** Opens (or initializes) the store at `filePath`, loading any existing data. */ static async open(filePath: string): Promise { - let db = emptyDb(); + let raw: string; try { - const raw = await readFile(filePath, "utf8"); - const parsed = JSON.parse(raw) as Partial; - db = { ...emptyDb(), ...parsed } as Db; - } catch { - // missing or unreadable file -> start fresh (parent dir created on write) + raw = await readFile(filePath, "utf8"); + } catch (err) { + // No file yet is the normal first-run case; start fresh (the parent dir + // is created on the first write). + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return new JsonFileStore(filePath, emptyDb()); + } + // Permission / I/O errors must NOT be mistaken for "empty" — doing so + // would let the next write clobber recoverable history. Fail loud. + throw err; } - return new JsonFileStore(filePath, db); + + let parsed: Partial; + try { + parsed = JSON.parse(raw) as Partial; + } catch (err) { + // The file exists but isn't valid JSON. Leave it untouched for recovery + // and refuse to start rather than overwrite it with an empty DB. + throw new Error( + `Loopwright store at ${filePath} is not valid JSON; left untouched for ` + + `recovery (move or delete it to start fresh): ${(err as Error).message}`, + ); + } + return new JsonFileStore(filePath, { ...emptyDb(), ...parsed } as Db); } protected persist(): Promise { diff --git a/test/server.test.ts b/test/server.test.ts index 70f8a38..224446d 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -244,11 +244,49 @@ describe("server: static assets", () => { const res = await fetch(`${base}/`, { headers: { origin: "http://localhost:9999" } }); expect(res.status).toBe(200); expect(res.headers.get("access-control-allow-origin")).toBeNull(); + // Token-bearing HTML must not be cached. + expect(res.headers.get("cache-control")).toBe("no-store"); const html = await res.text(); expect(html).toContain(`window.__LOOPWRIGHT_TOKEN__="${TOKEN}"`); }); }); +describe("server: session id reuse", () => { + beforeEach(() => startServer()); + + it("does not replay a previous run's events when a finished id is reused", async () => { + const sid = "reused-session"; + + // Run 1 to completion on the fixed id. + const r1 = await fetch(`${base}/api/runs`, { + method: "POST", + headers: auth({ "content-type": "application/json" }), + body: JSON.stringify({ goal: "first", sessionId: sid }), + }); + expect(r1.status).toBe(202); + const s1 = await fetch(`${base}/api/runs/${sid}/stream`, { headers: auth() }); + await readSse(s1, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); + + // Run 2 reuses the id (allowed now that run 1 is finished). + const r2 = await fetch(`${base}/api/runs`, { + method: "POST", + headers: auth({ "content-type": "application/json" }), + body: JSON.stringify({ goal: "second", sessionId: sid }), + }); + expect(r2.status).toBe(202); + + const s2 = await fetch(`${base}/api/runs/${sid}/stream`, { headers: auth() }); + const msgs = await readSse(s2, (m) => m.some((x) => x.event === "status" && x.data.phase === "done")); + + // Fresh channel: ids restart at 0, the first event is run 2 going running, + // and there is exactly one terminal "done" (no stale run-1 done replayed). + expect(msgs[0]!.id).toBe(0); + expect(msgs[0]!.event).toBe("status"); + expect(msgs[0]!.data.phase).toBe("running"); + expect(msgs.filter((m) => m.event === "status" && m.data.phase === "done").length).toBe(1); + }); +}); + describe("server: run lifecycle + trace", () => { beforeEach(() => startServer()); diff --git a/test/session.test.ts b/test/session.test.ts index cb4e1af..ba19993 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { runGoal, openBlockers } from "../src/session.js"; +import { runGoal, openBlockers, finalSessionStatus } from "../src/session.js"; import { loadConfig } from "../src/config.js"; import { MockRunner } from "../src/runners/mockRunner.js"; import type { AgentRunner, RunnerProfile, RunRequest } from "../src/runners/agentRunner.js"; @@ -117,3 +117,20 @@ describe("runGoal end-to-end", () => { await expect(runGoal("g", config, { executor: okExecutor })).rejects.toThrow(); }); }); + +describe("finalSessionStatus", () => { + it("is completed only when nothing needs a human and integration (if any) is ok", () => { + expect(finalSessionStatus(0)).toBe("completed"); + expect(finalSessionStatus(0, { ok: true })).toBe("completed"); + }); + + it("is needs_human when a task needs attention", () => { + expect(finalSessionStatus(2)).toBe("needs_human"); + }); + + it("is needs_human when integration failed, even with no task blockers", () => { + // A clean per-task run that fails to integrate (conflicts / failed verify) + // must not be reported as completed. + expect(finalSessionStatus(0, { ok: false })).toBe("needs_human"); + }); +}); diff --git a/test/store.test.ts b/test/store.test.ts index 9e107e6..172249f 100644 --- a/test/store.test.ts +++ b/test/store.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { mkdtemp, readFile } from "node:fs/promises"; +import { mkdtemp, readFile, writeFile } from "node:fs/promises"; import { MemoryStore, JsonFileStore, openStore } from "../src/storage/store.js"; import type { TaskOutcome } from "../src/engine/loop.js"; @@ -106,6 +106,17 @@ describe("JsonFileStore", () => { expect((await reopened.getTask("s1", "t1"))?.state).toBe("GREEN"); }); + it("refuses to open a corrupt store and leaves the file untouched", async () => { + const dir = await mkdtemp(join(tmpdir(), "loopwright-store-")); + const file = join(dir, "sessions.db"); + await writeFile(file, "{ this is not json"); + + // Fails loud instead of silently starting empty (which would clobber it). + await expect(JsonFileStore.open(file)).rejects.toThrow(/not valid JSON/); + // The unreadable file is preserved as-is for recovery. + expect(await readFile(file, "utf8")).toBe("{ this is not json"); + }); + it("serializes concurrent writes without corrupting the file", async () => { const dir = await mkdtemp(join(tmpdir(), "loopwright-store-")); const file = join(dir, "sessions.db"); From 110b4eee675fff58e4a58bf2b6aece06c941cb22 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 19:35:47 +0000 Subject: [PATCH 07/19] fix: persist failed runs, always release worktrees, surface integration in UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth round of review findings. P1 — engine failures left sessions stuck "running": runGoal's body is now wrapped in try/catch/finally. On any throw (planning, runner execution, worktree setup, integration, cleanup) it records a structured session_failed event and persists status "failed", then rethrows so the server's SSE error status still fires. P1 — worktrees leaked on thrown errors: per-task worktree release now happens in a finally that runs regardless of whether integration succeeded or threw, so a failed run can't leave .loopwright worktrees/branches behind. (Uses a const handle inside try for closure narrowing and mirrors it for the finally.) P2 — integration failures were easy to miss in the desktop UI: - the live "done" banner no longer always says "completed"; it reflects integration failure / tasks needing a human; - the results view now has a first-class Integration & verification card (merged/conflicts/verification, highlighted when blocking) plus a Run failed card sourced from the durable event log. Validated: 146 tests pass (new: failed-run persistence + session_failed event), tsc clean, desktop frontend builds. --- desktop/src/main.ts | 67 +++++++- desktop/src/styles.css | 2 + src/observability/events.ts | 1 + src/session.ts | 325 ++++++++++++++++++++---------------- test/session.test.ts | 24 +++ 5 files changed, 272 insertions(+), 147 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 9c64f43..ec2a4f3 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -227,8 +227,22 @@ function renderMonitor(sessionId: string, goal: string): void { } else if (msg.type === "status") { const phase = document.getElementById("phase") as HTMLElement; if (msg.data.phase === "done") { - phase.className = "phase done"; - phase.textContent = "completed"; + // "done" is not necessarily success: a clean per-task run can still + // fail to integrate (merge conflicts / failed verification), and some + // tasks may need a human. Reflect that instead of a blanket "completed". + const r = msg.data.result ?? {}; + const integrationFailed = r.integration && r.integration.ok === false; + const needsHuman = Array.isArray(r.needsHuman) && r.needsHuman.length > 0; + if (integrationFailed) { + phase.className = "phase error"; + phase.textContent = "integration failed — needs attention"; + } else if (needsHuman) { + phase.className = "phase error"; + phase.textContent = "completed — some tasks need human attention"; + } else { + phase.className = "phase done"; + phase.textContent = "completed"; + } const btn = h("button", { class: "primary" }, ["View results"]); btn.addEventListener("click", () => renderResults(sessionId)); phase.append(" ", btn); @@ -310,10 +324,57 @@ async function renderResults(sessionId: string): Promise { tasksCard.append(block); } + // Blocking summary cards (shown high up so a merge/verify failure can't be + // missed behind all-green task counts). Sourced from the durable event log. + const blocking: HTMLElement[] = []; + + const failedEvent = trace.events.find((e) => e.type === "session_failed"); + if (failedEvent) { + const card = h("div", { class: "card error" }); + card.append(h("h3", {}, ["Run failed"])); + card.append(h("div", {}, [String((failedEvent.data as Record).error ?? "unknown error")])); + blocking.push(card); + } + + const integrationEvent = trace.events.find((e) => e.type === "integration"); + if (integrationEvent) { + const d = integrationEvent.data as { + ok?: boolean; + merged?: unknown[]; + conflicts?: unknown[]; + integrationBranch?: string; + verification?: { passed?: boolean } | null; + }; + const ok = d.ok === true; + const conflicts = Array.isArray(d.conflicts) ? d.conflicts : []; + const merged = Array.isArray(d.merged) ? d.merged.length : 0; + const card = h("div", { class: `card${ok ? "" : " integration-bad"}` }); + card.append(h("h3", {}, ["Integration & verification"])); + card.append( + h("div", { class: `phase ${ok ? "done" : "error"}` }, [ + ok ? "branches merged and full verification passed" : "FAILED — merge conflicts or verification did not pass", + ]), + ); + card.append( + h("div", { class: "hint" }, [ + `branch ${String(d.integrationBranch ?? "?")} · merged ${merged} · conflicts ${conflicts.length}`, + ]), + ); + if (conflicts.length) { + const ul = h("ul", { class: "tx" }); + for (const c of conflicts) ul.append(h("li", {}, [typeof c === "string" ? c : JSON.stringify(c)])); + card.append(h("div", { class: "hint" }, ["Conflicting branches:"]), ul); + } + if (d.verification && d.verification.passed === false) { + card.append(h("div", { class: "error" }, ["Full-tree verification failed after merge."])); + } + blocking.push(card); + } + const raw = h("details", { class: "card" }); raw.append(h("summary", {}, ["Raw trace (text)"]), h("pre", { class: "log" }, [resp.text])); - view.append(summary, usage, tasksCard, raw); + view.append(summary, ...blocking, usage, tasksCard, raw); } function countStates(trace: TraceResponse["trace"]): Record { diff --git a/desktop/src/styles.css b/desktop/src/styles.css index e28ffca..fbde23b 100644 --- a/desktop/src/styles.css +++ b/desktop/src/styles.css @@ -123,6 +123,8 @@ table.kv tr.total td { font-weight: 700; } .count.unverified { color: var(--yellow); } .count.needs-human { color: var(--red); } +.card.integration-bad { border-color: var(--red); } + .task-block { padding: 8px 0; border-bottom: 1px solid var(--border); } .task-head { font-family: var(--mono); display: flex; align-items: center; gap: 8px; } .degraded { color: var(--yellow); font-size: 12px; } diff --git a/src/observability/events.ts b/src/observability/events.ts index 3fce6f8..803ee01 100644 --- a/src/observability/events.ts +++ b/src/observability/events.ts @@ -38,6 +38,7 @@ export const EVENT_TYPES = { runnerCall: "runner_call", sessionFinished: "session_finished", integration: "integration", + sessionFailed: "session_failed", } as const; function num(v: unknown): number { diff --git a/src/session.ts b/src/session.ts index f17f62f..c902f4f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -118,126 +118,133 @@ export async function runGoal( }); } - // Roles, instrumented so every runner invocation emits a structured event - // (Task 22). When persisting, calls are recorded to the store's event stream. - const onRunnerCall: RunnerCallSink | undefined = - store && sessionId - ? (e) => - store.recordEvent({ - sessionId, - at: e.at, - type: EVENT_TYPES.runnerCall, - data: e as unknown as Record, - }) - : roleOpts.onRunnerCall; - const { actor, critic } = createRoles(config, { - ...roleOpts, - ...(onRunnerCall ? { onRunnerCall } : {}), - }); + // Worktree manager is held here so the `finally` below can guarantee cleanup + // even when the run throws partway through. + let wtManager: GitWorktreeManager | undefined; - // Checkpoint to the store and (optionally) fan out to a caller-supplied - // observer such as an event log. Absent both, the loop runs unobserved. - const observer = - (store && sessionId) || extraObserver - ? combineObservers( - store && sessionId ? storeObserver(store, sessionId) : undefined, - extraObserver, - ) - : undefined; - - const baseDeps = { - actor, - critic, - config, - cwd, - ...(executor ? { executor } : {}), - ...(opts.log ? { log: opts.log } : {}), - ...(observer ? { observer } : {}), - }; - - const plan = await runPlanReview(goal, baseDeps); - if (store && sessionId) { - await store.updateSession(sessionId, { - planApproved: plan.approved, - planRevisions: plan.revisions, - }); - await store.recordEvent({ - sessionId, - at: new Date().toISOString(), - type: EVENT_TYPES.planReviewed, - data: { approved: plan.approved, revisions: plan.revisions, openItems: plan.openItems.length }, + try { + // Roles, instrumented so every runner invocation emits a structured event + // (Task 22). When persisting, calls are recorded to the store's event stream. + const onRunnerCall: RunnerCallSink | undefined = + store && sessionId + ? (e) => + store.recordEvent({ + sessionId, + at: e.at, + type: EVENT_TYPES.runnerCall, + data: e as unknown as Record, + }) + : roleOpts.onRunnerCall; + const { actor, critic } = createRoles(config, { + ...roleOpts, + ...(onRunnerCall ? { onRunnerCall } : {}), }); - } - // Isolated worktrees + integration (Tasks 20, 21), opt-in via repoDir. Each - // task builds in its own git worktree; on success its changes are committed - // on a per-task branch for the integrator to merge after the run. - const useWorktrees = Boolean(repoDir) && config.useWorktrees; - const wtManager = useWorktrees - ? new GitWorktreeManager({ repoDir: repoDir as string, sessionId: sessionId ?? randomUUID() }) - : undefined; - const greenBranches: IntegrationBranch[] = []; + // Checkpoint to the store and (optionally) fan out to a caller-supplied + // observer such as an event log. Absent both, the loop runs unobserved. + const observer = + (store && sessionId) || extraObserver + ? combineObservers( + store && sessionId ? storeObserver(store, sessionId) : undefined, + extraObserver, + ) + : undefined; - const schedulerExtra: Partial = {}; - if (wtManager) { - schedulerExtra.workspaceFor = async (t) => (await wtManager.acquire(t.id)).path; - schedulerExtra.onTaskSettled = async (t, r) => { - const unblocking = - r.outcome?.finalState === "GREEN" || r.outcome?.finalState === "UNVERIFIED_BY_CRITIC"; - if (r.status === "completed" && unblocking) { - const { committed } = await wtManager.commit(t.id, `loopwright(${t.id}): ${t.title}`); - const branch = wtManager.branchFor(t.id); - if (committed && branch) greenBranches.push({ taskId: t.id, branch }); - } else if (r.status !== "resumed") { - await wtManager.release(t.id, { deleteBranch: true }); - } + const baseDeps = { + actor, + critic, + config, + cwd, + ...(executor ? { executor } : {}), + ...(opts.log ? { log: opts.log } : {}), + ...(observer ? { observer } : {}), }; - } else { - if (workspaceFor) schedulerExtra.workspaceFor = workspaceFor; - if (onTaskSettled) schedulerExtra.onTaskSettled = onTaskSettled; - } - // The scheduler owns ordering, the parallelism cap, dependency-failure - // propagation, and (via resumeOutcome) reuse of completed tasks. - const results = await runScheduledTasks(plan.plan.tasks, { - ...baseDeps, - ...schedulerExtra, - ...(resume && store && sessionId - ? { resumeOutcome: async (t) => (await store.getOutcome(sessionId, t.id))?.outcome } - : {}), - }); + const plan = await runPlanReview(goal, baseDeps); + if (store && sessionId) { + await store.updateSession(sessionId, { + planApproved: plan.approved, + planRevisions: plan.revisions, + }); + await store.recordEvent({ + sessionId, + at: new Date().toISOString(), + type: EVENT_TYPES.planReviewed, + data: { approved: plan.approved, revisions: plan.revisions, openItems: plan.openItems.length }, + }); + } - const green: string[] = []; - const unverified: string[] = []; - const needsHuman: string[] = []; - const skipped: string[] = []; + // Isolated worktrees + integration (Tasks 20, 21), opt-in via repoDir. Each + // task builds in its own git worktree; on success its changes are committed + // on a per-task branch for the integrator to merge after the run. `wt` is a + // const so its non-undefined narrowing holds inside the closures below; + // `wtManager` mirrors it for the `finally` cleanup. + const useWorktrees = Boolean(repoDir) && config.useWorktrees; + const wt = useWorktrees + ? new GitWorktreeManager({ repoDir: repoDir as string, sessionId: sessionId ?? randomUUID() }) + : undefined; + wtManager = wt; + const greenBranches: IntegrationBranch[] = []; - for (const r of results) { - if (r.status === "skipped") skipped.push(r.taskId); - else if (r.outcome?.finalState === "GREEN") green.push(r.taskId); - else if (r.outcome?.finalState === "UNVERIFIED_BY_CRITIC") unverified.push(r.taskId); - else needsHuman.push(r.taskId); - } + const schedulerExtra: Partial = {}; + if (wt) { + schedulerExtra.workspaceFor = async (t) => (await wt.acquire(t.id)).path; + schedulerExtra.onTaskSettled = async (t, r) => { + const unblocking = + r.outcome?.finalState === "GREEN" || r.outcome?.finalState === "UNVERIFIED_BY_CRITIC"; + if (r.status === "completed" && unblocking) { + const { committed } = await wt.commit(t.id, `loopwright(${t.id}): ${t.title}`); + const branch = wt.branchFor(t.id); + if (committed && branch) greenBranches.push({ taskId: t.id, branch }); + } else if (r.status !== "resumed") { + await wt.release(t.id, { deleteBranch: true }); + } + }; + } else { + if (workspaceFor) schedulerExtra.workspaceFor = workspaceFor; + if (onTaskSettled) schedulerExtra.onTaskSettled = onTaskSettled; + } - if (store && sessionId) { - await store.recordEvent({ - sessionId, - at: new Date().toISOString(), - type: EVENT_TYPES.sessionFinished, - data: { - green, - unverified, - needsHuman, - skipped, - allVerified: green.length === plan.plan.tasks.length, - }, + // The scheduler owns ordering, the parallelism cap, dependency-failure + // propagation, and (via resumeOutcome) reuse of completed tasks. + const results = await runScheduledTasks(plan.plan.tasks, { + ...baseDeps, + ...schedulerExtra, + ...(resume && store && sessionId + ? { resumeOutcome: async (t) => (await store.getOutcome(sessionId, t.id))?.outcome } + : {}), }); - } - // Integrate the per-task branches and run full verification on the result. - let integration: IntegrationResult | undefined; - if (wtManager) { - if (greenBranches.length > 0) { + const green: string[] = []; + const unverified: string[] = []; + const needsHuman: string[] = []; + const skipped: string[] = []; + + for (const r of results) { + if (r.status === "skipped") skipped.push(r.taskId); + else if (r.outcome?.finalState === "GREEN") green.push(r.taskId); + else if (r.outcome?.finalState === "UNVERIFIED_BY_CRITIC") unverified.push(r.taskId); + else needsHuman.push(r.taskId); + } + + if (store && sessionId) { + await store.recordEvent({ + sessionId, + at: new Date().toISOString(), + type: EVENT_TYPES.sessionFinished, + data: { + green, + unverified, + needsHuman, + skipped, + allVerified: green.length === plan.plan.tasks.length, + }, + }); + } + + // Integrate the per-task branches and run full verification on the result. + let integration: IntegrationResult | undefined; + if (wt && greenBranches.length > 0) { const branchTaskIds = new Set(greenBranches.map((b) => b.taskId)); const verifyCommands = [ ...new Set( @@ -254,45 +261,75 @@ export async function runGoal( ...(opts.log ? { log: opts.log } : {}), }); } - // Tear down any worktrees kept for integration. - for (const wt of wtManager.list()) await wtManager.release(wt.taskId); - } - // Durable final status is decided LAST, so an integration that surfaced - // conflicts or failed verification (integration.ok === false) marks the - // session needs_human rather than leaving the earlier "completed" optimism. - if (store && sessionId) { - if (integration) { - await store.recordEvent({ - sessionId, - at: new Date().toISOString(), - type: EVENT_TYPES.integration, - data: { - ok: integration.ok, - merged: integration.merged, - conflicts: integration.conflicts, - integrationBranch: integration.integrationBranch, - verification: integration.verification ?? null, - }, + // Durable final status is decided LAST, so an integration that surfaced + // conflicts or failed verification (integration.ok === false) marks the + // session needs_human rather than leaving the earlier "completed" optimism. + if (store && sessionId) { + if (integration) { + await store.recordEvent({ + sessionId, + at: new Date().toISOString(), + type: EVENT_TYPES.integration, + data: { + ok: integration.ok, + merged: integration.merged, + conflicts: integration.conflicts, + integrationBranch: integration.integrationBranch, + verification: integration.verification ?? null, + }, + }); + } + await store.updateSession(sessionId, { + status: finalSessionStatus(needsHuman.length, integration), }); } - await store.updateSession(sessionId, { - status: finalSessionStatus(needsHuman.length, integration), - }); - } - return { - goal, - ...(sessionId ? { sessionId } : {}), - plan, - results, - green, - unverified, - needsHuman, - skipped, - allVerified: green.length === plan.plan.tasks.length, - ...(integration ? { integration } : {}), - }; + return { + goal, + ...(sessionId ? { sessionId } : {}), + plan, + results, + green, + unverified, + needsHuman, + skipped, + allVerified: green.length === plan.plan.tasks.length, + ...(integration ? { integration } : {}), + }; + } catch (err) { + // Any throw (planning, runner execution, worktree setup, integration, + // cleanup) must leave the durable session in a terminal state, not stuck + // "running". Record a structured failure, then rethrow so callers (e.g. the + // server's SSE error status) still see it. + if (store && sessionId) { + try { + await store.recordEvent({ + sessionId, + at: new Date().toISOString(), + type: EVENT_TYPES.sessionFailed, + data: { error: String((err as Error)?.message ?? err) }, + }); + await store.updateSession(sessionId, { status: "failed" }); + } catch { + /* best effort: never mask the original error with a persistence error */ + } + } + throw err; + } finally { + // Release any worktrees still held — covers both the normal end of a + // worktree run and a throw during the run/integration/cleanup, so a failed + // run can't leave .loopwright worktrees/branches behind. + if (wtManager) { + for (const held of wtManager.list()) { + try { + await wtManager.release(held.taskId); + } catch { + /* best effort cleanup */ + } + } + } + } } /** diff --git a/test/session.test.ts b/test/session.test.ts index ba19993..f1a6012 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -4,6 +4,7 @@ import { loadConfig } from "../src/config.js"; import { MockRunner } from "../src/runners/mockRunner.js"; import type { AgentRunner, RunnerProfile, RunRequest } from "../src/runners/agentRunner.js"; import { criticGreen, criticBlock, scriptedExecutor } from "../src/adapters/mocks.js"; +import { MemoryStore } from "../src/storage/store.js"; /** * End-to-end session tests. The whole config -> createRoles -> plan review -> @@ -116,6 +117,29 @@ describe("runGoal end-to-end", () => { }); await expect(runGoal("g", config, { executor: okExecutor })).rejects.toThrow(); }); + + it("persists 'failed' + a failure event when the run throws", async () => { + const store = new MemoryStore(); + const config = configWithMockRoles(); + // A runner that throws makes plan review (and thus runGoal) reject. + const throwingFactory = (profile: RunnerProfile): AgentRunner => ({ + profile, + run: async () => { + throw new Error("kaboom"); + }, + }); + + await expect( + runGoal("g", config, { store, factory: throwingFactory, executor: okExecutor }), + ).rejects.toThrow(/kaboom/); + + const sessions = await store.listSessions(); + expect(sessions).toHaveLength(1); + // The durable session is terminal (failed), not stuck "running". + expect(sessions[0]!.status).toBe("failed"); + const events = await store.listEvents(sessions[0]!.id); + expect(events.some((e) => e.type === "session_failed")).toBe(true); + }); }); describe("finalSessionStatus", () => { From 00759ddd0a906165e8f0a2704fbae7cb72adfed8 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 19:54:55 +0000 Subject: [PATCH 08/19] fix: orphan recovery, task-fault isolation, and run cancellation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifth round of review findings. P1 — sidecar kill/restart left sessions stuck "running": startup now calls reconcileInterruptedSessions(), marking any still-"running" session "failed" with a session_interrupted event, so a crash/restart can't leave durable sessions running forever. P1 — one rejected parallel task could fail the whole session and abandon siblings: the scheduler wraps each task run so it never rejects (an unexpected throw becomes a NEEDS_HUMAN result for that task only). Promise.race no longer rejects, all in-flight siblings drain, and worktree cleanup runs after they settle rather than racing live tasks. P2 — actor build/model failures were session-fatal: runTask now catches a thrown actor build and degrades that task to NEEDS_HUMAN (like a malformed critic) instead of throwing out of the run. P2 — no cancellation path: added cooperative cancellation via AbortSignal. runGoal/scheduler/loop honor it (stop launching, bail between steps, drain, then throw AbortError so the session is persisted failed), the mechanical gate kills in-flight subprocesses on abort, the server exposes POST /api/runs/:id/cancel backed by a per-run AbortController, and the desktop monitor gains a "Stop run" button. Validated: 151 tests pass (new: scheduler fault-isolation + cancel, gate subprocess kill, session reconcile, server cancel), tsc clean, desktop builds, and the compiled sidecar serves the cancel route (404 unknown, 401 unauth). --- desktop/src/api.ts | 9 +++++ desktop/src/main.ts | 19 ++++++++++ src/engine/loop.ts | 25 ++++++++++++- src/engine/mechanicalGate.ts | 35 +++++++++++++++--- src/engine/scheduler.ts | 71 +++++++++++++++++++++++++++++------- src/observability/events.ts | 1 + src/server/index.ts | 9 +++++ src/server/server.ts | 19 +++++++++- src/session.ts | 28 ++++++++++++++ test/mechanicalGate.test.ts | 12 ++++++ test/scheduler.test.ts | 46 +++++++++++++++++++++++ test/server.test.ts | 33 +++++++++++++++++ test/session.test.ts | 18 ++++++++- 13 files changed, 302 insertions(+), 23 deletions(-) diff --git a/desktop/src/api.ts b/desktop/src/api.ts index 8b8a590..771fada 100644 --- a/desktop/src/api.ts +++ b/desktop/src/api.ts @@ -90,6 +90,15 @@ export async function startRun(body: StartRunBody): Promise { return sessionId; } +/** Requests cancellation of an in-flight run. 404 (already finished) is ignored. */ +export async function cancelRun(sessionId: string): Promise { + const res = await fetch((await apiBase()) + `/api/runs/${encodeURIComponent(sessionId)}/cancel`, { + method: "POST", + headers: await authHeaders(), + }); + if (!res.ok && res.status !== 404) throw new Error(`${res.status} ${await res.text()}`); +} + export async function listSessions(): Promise { const { sessions } = await getJson<{ sessions: SessionRecord[] }>("/api/sessions"); return sessions; diff --git a/desktop/src/main.ts b/desktop/src/main.ts index ec2a4f3..29d04b1 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -1,6 +1,7 @@ import "./styles.css"; import { apiBase, + cancelRun, deleteSecret, getTrace, health, @@ -174,6 +175,21 @@ function renderMonitor(sessionId: string, goal: string): void { view.append(header, usage, tasksCard, logCard); + // Stop control: requests cooperative cancellation of the in-flight run. + const stopBtn = h("button", { class: "danger" }, ["Stop run"]); + stopBtn.addEventListener("click", async () => { + stopBtn.setAttribute("disabled", "true"); + stopBtn.textContent = "Stopping…"; + try { + await cancelRun(sessionId); + } catch (err) { + stopBtn.removeAttribute("disabled"); + stopBtn.textContent = "Stop run"; + (document.getElementById("plan") as HTMLElement).textContent = `Cancel failed: ${(err as Error).message}`; + } + }); + (header.querySelector("#phase") as HTMLElement).append(" ", stopBtn); + const taskRows = new Map(); let actorCalls = 0; let criticCalls = 0; @@ -226,6 +242,9 @@ function renderMonitor(sessionId: string, goal: string): void { } } else if (msg.type === "status") { const phase = document.getElementById("phase") as HTMLElement; + if (msg.data.phase === "done" || msg.data.phase === "error") { + stopBtn.remove(); // run is over; no longer cancellable + } if (msg.data.phase === "done") { // "done" is not necessarily success: a clean per-task run can still // fail to integrate (merge conflicts / failed verification), and some diff --git a/src/engine/loop.ts b/src/engine/loop.ts index 0f0ed03..4c9fbee 100644 --- a/src/engine/loop.ts +++ b/src/engine/loop.ts @@ -17,11 +17,12 @@ import { type TaskState, } from "../domain/stateMachine.js"; import { runMechanicalGate, type CommandExecutor } from "./mechanicalGate.js"; -import { guardProgress } from "./watchdog.js"; +import { guardProgress, type Guarded } from "./watchdog.js"; import { redact, redactAndTruncate } from "./redaction.js"; import { parseCriticResponse, REPAIR_HINT } from "./criticParser.js"; import type { Actor, + ActorBuildResult, BuildFeedback, Critic, CriticRequest, @@ -44,6 +45,8 @@ export interface LoopDeps { * config.stuckThresholdMs when set. <= 0 disables it. (Milestone 3, Task 18) */ stuckThresholdMs?: number; + /** cancels the run cooperatively: checked between steps; kills gate commands */ + signal?: AbortSignal; } /** A single state transition, surfaced to observers as it happens. */ @@ -240,10 +243,27 @@ export async function runTask( state = await fire("BUILD_STARTED", "initial build"); while (!isTerminal(state)) { + // Cooperative cancellation: bail to a terminal state between steps so an + // in-flight task stops promptly when the run is cancelled. + if (deps.signal?.aborted) { + degradedReason = "Run cancelled before completion."; + await fire("STUCK_ABORTED", degradedReason); // routes to NEEDS_HUMAN + break; + } switch (state) { case "BUILDING": { buildAttempts++; - const guardedBuild = await guardProgress(deps.actor.build(task, feedback), stuckThresholdMs); + let guardedBuild: Guarded; + try { + guardedBuild = await guardProgress(deps.actor.build(task, feedback), stuckThresholdMs); + } catch (err) { + // An actor/model failure (quota, unusable output, transport) is + // task-fatal, not session-fatal: degrade this task to NEEDS_HUMAN + // (like a malformed critic) instead of throwing out of the run. + degradedReason = `Actor build failed: ${String((err as Error)?.message ?? err)}`; + await fire("STUCK_ABORTED", degradedReason); // routes BUILDING -> NEEDS_HUMAN + break; + } if (guardedBuild.stuck) { degradedReason = `No progress within ${guardedBuild.elapsedMs}ms while building (stuck).`; await fire("STUCK_ABORTED", degradedReason); @@ -264,6 +284,7 @@ export async function runTask( lastGate = await runMechanicalGate(task.verifyCommands, { cwd: deps.cwd, ...(deps.executor ? { executor: deps.executor } : {}), + ...(deps.signal ? { signal: deps.signal } : {}), }); if (lastGate.passed) { await fire("MECHANICAL_PASSED", `gate passed (${lastGate.steps.length} step(s))`); diff --git a/src/engine/mechanicalGate.ts b/src/engine/mechanicalGate.ts index 40a6099..d8186e6 100644 --- a/src/engine/mechanicalGate.ts +++ b/src/engine/mechanicalGate.ts @@ -23,6 +23,7 @@ export interface CommandOutcome { export type CommandExecutor = ( command: string, cwd: string, + signal?: AbortSignal, ) => Promise; const DEFAULT_TIMEOUT_MS = 5 * 60_000; @@ -49,12 +50,18 @@ export function createShellExecutor( const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; const maxChars = opts.maxCapturedChars ?? DEFAULT_MAX_CAPTURED_CHARS; - return (command, cwd) => + return (command, cwd, signal) => new Promise((resolve) => { const started = Date.now(); + // If already cancelled, don't even spawn. + if (signal?.aborted) { + resolve({ exitCode: TIMEOUT_EXIT_CODE, output: "[cancelled]", durationMs: 0 }); + return; + } const child = spawn(command, { cwd, shell: true }); let output = ""; let timedOut = false; + let cancelled = false; const append = (buf: Buffer) => { output += buf.toString(); @@ -66,10 +73,22 @@ export function createShellExecutor( child.kill("SIGKILL"); }, timeoutMs); + // Cancellation: kill the child so a long verify/build command stops + // promptly when the run is cancelled. + const onAbort = (): void => { + cancelled = true; + child.kill("SIGKILL"); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + const cleanup = (): void => { + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + }; + child.stdout.on("data", append); child.stderr.on("data", append); child.on("error", (err) => { - clearTimeout(timer); + cleanup(); resolve({ exitCode: 127, output: `${output}\n${err.message}`, @@ -77,12 +96,14 @@ export function createShellExecutor( }); }); child.on("close", (code) => { - clearTimeout(timer); + cleanup(); resolve({ - exitCode: timedOut ? TIMEOUT_EXIT_CODE : (code ?? 1), + exitCode: timedOut || cancelled ? TIMEOUT_EXIT_CODE : (code ?? 1), output: timedOut ? `${output}\n[killed: exceeded ${timeoutMs}ms timeout]` - : output, + : cancelled + ? `${output}\n[killed: run cancelled]` + : output, durationMs: Date.now() - started, }); }); @@ -95,6 +116,8 @@ export const defaultExecutor: CommandExecutor = createShellExecutor(); export interface MechanicalGateOptions { cwd: string; executor?: CommandExecutor; + /** cancels in-flight commands when aborted */ + signal?: AbortSignal; } /** @@ -117,7 +140,7 @@ export async function runMechanicalGate( } for (const command of commands) { - const outcome = await executor(command, opts.cwd); + const outcome = await executor(command, opts.cwd, opts.signal); const passed = outcome.exitCode === 0; steps.push({ command, diff --git a/src/engine/scheduler.ts b/src/engine/scheduler.ts index ddd3b37..0fcc420 100644 --- a/src/engine/scheduler.ts +++ b/src/engine/scheduler.ts @@ -110,6 +110,29 @@ function isBlocking(result: ScheduledResult | undefined): boolean { return !isUnblocking(result); } +/** Synthetic terminal outcome for a task whose run threw unexpectedly. */ +function failedTaskOutcome(taskId: string, reason: string): TaskOutcome { + return { + taskId, + finalState: "NEEDS_HUMAN", + verified: false, + history: [], + buildAttempts: 0, + reviewCycles: 0, + nits: [], + unresolvedBlockers: [], + degradedReason: reason, + lastDiff: "", + }; +} + +/** An AbortError-shaped error so callers can distinguish cancellation. */ +function abortError(): Error { + const e = new Error("run cancelled"); + e.name = "AbortError"; + return e; +} + /** * Executes all tasks honoring dependencies + the parallelism cap. Returns one * result per task in the input's declared order. @@ -167,24 +190,40 @@ export async function runScheduledTasks( } } - // 2) Launch ready tasks up to the parallelism cap. - for (const id of [...remaining]) { - if (running.size >= maxParallel) break; - const task = byId.get(id) as TaskSpec; - if (!depsSatisfied(task)) continue; - remaining.delete(id); - const cwd = await workspaceFor(task); - deps.log?.(`[${id}] START`); - running.set( - id, - runTask(task, { ...deps, cwd }).then((outcome) => ({ id, outcome })), - ); + // 2) Launch ready tasks up to the parallelism cap. Once cancelled, stop + // launching new work; already-running tasks are still drained below. + if (!deps.signal?.aborted) { + for (const id of [...remaining]) { + if (running.size >= maxParallel) break; + const task = byId.get(id) as TaskSpec; + if (!depsSatisfied(task)) continue; + remaining.delete(id); + const cwd = await workspaceFor(task); + deps.log?.(`[${id}] START`); + // The run is wrapped so it never rejects: an unexpected throw becomes a + // NEEDS_HUMAN result for THAT task, so one task can't reject the + // Promise.race and abandon its still-running siblings (which would let + // worktree cleanup race live tasks). + running.set( + id, + runTask(task, { ...deps, cwd }) + .then((outcome) => ({ id, outcome })) + .catch((err) => ({ + id, + outcome: failedTaskOutcome( + id, + `Task failed unexpectedly: ${String((err as Error)?.message ?? err)}`, + ), + })), + ); + } } // 3) Nothing running and nothing launchable: all work is resolved. if (running.size === 0) break; - // 4) Wait for the next task to finish, record it, and re-evaluate. + // 4) Wait for the next task to finish, record it, and re-evaluate. The + // settle wrapper guarantees this never rejects, so siblings always drain. const { id, outcome } = await Promise.race(running.values()); running.delete(id); const r: ScheduledResult = { taskId: id, status: "completed", outcome }; @@ -193,6 +232,12 @@ export async function runScheduledTasks( await deps.onTaskSettled?.(byId.get(id) as TaskSpec, r); } + // Cancellation surfaces only AFTER all in-flight tasks have drained, so the + // caller's worktree/cleanup runs without racing live tasks. + if (deps.signal?.aborted) { + throw abortError(); + } + return tasks.map( (t) => results.get(t.id) ?? { diff --git a/src/observability/events.ts b/src/observability/events.ts index 803ee01..862e561 100644 --- a/src/observability/events.ts +++ b/src/observability/events.ts @@ -39,6 +39,7 @@ export const EVENT_TYPES = { sessionFinished: "session_finished", integration: "integration", sessionFailed: "session_failed", + sessionInterrupted: "session_interrupted", } as const; function num(v: unknown): number { diff --git a/src/server/index.ts b/src/server/index.ts index c459171..ad4fe10 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -23,6 +23,7 @@ import path from "node:path"; import { randomUUID } from "node:crypto"; import { loadConfig } from "../config.js"; import { openStore } from "../storage/store.js"; +import { reconcileInterruptedSessions } from "../session.js"; import { createServer, isLoopbackHost } from "./server.js"; function envFlag(v: string | undefined): boolean { @@ -33,6 +34,14 @@ async function main(): Promise { const config = loadConfig(); const store = await openStore(config.dbPath); + // Reconcile sessions left "running" by a previous process that was killed or + // crashed mid-run (e.g. the desktop "restart engine" button), so they don't + // stay stuck running forever. + const reconciled = await reconcileInterruptedSessions(store); + if (reconciled > 0) { + console.error(`Marked ${reconciled} interrupted session(s) as failed on startup.`); + } + // Resolve to an absolute path so the server's path-traversal guard works even // when a relative LOOPWRIGHT_STATIC_DIR is supplied. const staticEnv = process.env.LOOPWRIGHT_STATIC_DIR; diff --git a/src/server/server.ts b/src/server/server.ts index 66d5611..5529868 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -169,6 +169,8 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const retainMs = opts.retainMs ?? 5 * 60_000; const hub = new RunHub(opts.maxBufferPerRun); let activeRuns = 0; + /** abort controllers for in-flight runs, keyed by session id (for cancel) */ + const controllers = new Map(); /** * Constant-time bearer-token check. The token is read from the Authorization @@ -231,6 +233,15 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { return streamRun(req, res, decodeURIComponent(streamMatch[1] as string), cors); } + const cancelMatch = pathname.match(/^\/api\/runs\/([^/]+)\/cancel$/); + if (cancelMatch && method === "POST") { + const id = decodeURIComponent(cancelMatch[1] as string); + const controller = controllers.get(id); + if (!controller) return send(res, 404, { error: `no active run for session "${id}"` }, cors); + controller.abort(); + return send(res, 202, { cancelling: true }, cors); + } + if (pathname === "/api/sessions" && method === "GET") { const sessions = await store.listSessions(); sessions.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); @@ -305,6 +316,9 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const tappedStore = tapStoreEvents(store, (rec) => hub.publish(sessionId, "event", rec)); activeRuns += 1; + // Per-run abort controller so POST /api/runs/:id/cancel can stop it. + const controller = new AbortController(); + controllers.set(sessionId, controller); // Begin a fresh hub channel for this run: discards any retained buffer from // a previous run that reused this session id (so the stream can't replay // stale events) and yields a generation token for the cleanup guard below. @@ -317,18 +331,21 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { // generation guard ensures this never drops a newer run on the same id. const settle = (): void => { activeRuns = Math.max(0, activeRuns - 1); + controllers.delete(sessionId); const timer = setTimeout(() => hub.forget(sessionId, generation), retainMs); if (typeof timer.unref === "function") timer.unref(); }; // Fire-and-forget: the run proceeds in the background and the client - // follows it over SSE. Errors are surfaced as a terminal status message. + // follows it over SSE. Errors (including cancellation) surface as a terminal + // status message. void runGoalImpl(goal, runConfig, { store: tappedStore, sessionId, resume: body.resume ?? false, observer, log: (line) => void hub.publish(sessionId, "log", { line }), + signal: controller.signal, }) .then((result) => { hub.publish(sessionId, "status", { phase: "done", result } satisfies RunStatusData); diff --git a/src/session.ts b/src/session.ts index c902f4f..5680222 100644 --- a/src/session.ts +++ b/src/session.ts @@ -79,6 +79,8 @@ export interface RunGoalOptions extends CreateRolesOptions { * after the run (Tasks 20, 21). */ repoDir?: string; + /** cooperative cancellation: aborts scheduling, gate commands, and the run */ + signal?: AbortSignal; } export async function runGoal( @@ -95,6 +97,7 @@ export async function runGoal( workspaceFor, onTaskSettled, repoDir, + signal, ...roleOpts } = opts; const cwd = opts.cwd ?? "."; @@ -158,6 +161,7 @@ export async function runGoal( ...(executor ? { executor } : {}), ...(opts.log ? { log: opts.log } : {}), ...(observer ? { observer } : {}), + ...(signal ? { signal } : {}), }; const plan = await runPlanReview(goal, baseDeps); @@ -346,6 +350,30 @@ export function finalSessionStatus( return "completed"; } +/** + * Marks any session left as `running` (e.g. because the engine process was + * killed/restarted mid-run, bypassing the normal failure path) as `failed`, + * recording a `session_interrupted` event. Call this at startup so a crashed or + * restarted engine doesn't leave durable sessions stuck "running" forever. + * Returns the number of sessions reconciled. + */ +export async function reconcileInterruptedSessions(store: Store): Promise { + const sessions = await store.listSessions(); + let reconciled = 0; + for (const s of sessions) { + if (s.status !== "running") continue; + await store.recordEvent({ + sessionId: s.id, + at: new Date().toISOString(), + type: EVENT_TYPES.sessionInterrupted, + data: { reason: "engine restarted or crashed while the run was in progress" }, + }); + await store.updateSession(s.id, { status: "failed" }); + reconciled += 1; + } + return reconciled; +} + /** Flattens a session's still-open blocking findings for reporting. */ export function openBlockers(result: SessionResult): Finding[] { const out: Finding[] = []; diff --git a/test/mechanicalGate.test.ts b/test/mechanicalGate.test.ts index f546389..13038e5 100644 --- a/test/mechanicalGate.test.ts +++ b/test/mechanicalGate.test.ts @@ -3,6 +3,18 @@ import { runMechanicalGate, createShellExecutor, TIMEOUT_EXIT_CODE } from "../sr import { scriptedExecutor } from "../src/adapters/mocks.js"; describe("mechanical gate", () => { + it("kills a running command when the abort signal fires", async () => { + const exec = createShellExecutor(); + const ac = new AbortController(); + const started = Date.now(); + const p = exec("sleep 5", ".", ac.signal); + setTimeout(() => ac.abort(), 20); + const out = await p; + // Cancelled, not run to completion: non-zero and well under the 5s sleep. + expect(out.exitCode).not.toBe(0); + expect(Date.now() - started).toBeLessThan(4000); + }); + it("passes when all commands succeed", async () => { const exec = scriptedExecutor(() => ({ exitCode: 0, output: "ok" })); const r = await runMechanicalGate(["npm run build", "npm test"], { cwd: ".", executor: exec }); diff --git a/test/scheduler.test.ts b/test/scheduler.test.ts index 838676f..8ad8133 100644 --- a/test/scheduler.test.ts +++ b/test/scheduler.test.ts @@ -4,6 +4,7 @@ import { loadConfig, type LoopwrightConfig } from "../src/config.js"; import { MockActor, MockCritic, criticBlock, criticGreen } from "../src/adapters/mocks.js"; import type { CommandExecutor } from "../src/engine/mechanicalGate.js"; import type { TaskSpec } from "../src/schemas/plan.js"; +import type { Actor } from "../src/adapters/agents.js"; const task = (id: string, dependencies: string[] = [], verify = ["check"]): TaskSpec => ({ id, @@ -143,3 +144,48 @@ describe("runScheduledTasks failure propagation + resume", () => { expect(order).toEqual([]); }); }); + +describe("runScheduledTasks fault isolation + cancellation", () => { + function actorThrowingOn(badId: string): Actor { + return { + draftPlan: async () => ({ plan: { goal: "g", tasks: [] } }), + build: async (t) => { + if (t.id === badId) throw new Error("model exploded"); + return { diff: "d", touchedFiles: [], summary: "s" }; + }, + selfReview: async () => criticGreen(), + }; + } + + it("isolates a throwing task as NEEDS_HUMAN while independent siblings finish", async () => { + const { exec } = recordingExecutor(); + const deps = { + actor: actorThrowingOn("boom"), + critic: new MockCritic({ fallback: criticGreen() }), + config: cfg(2), + cwd: ".", + executor: exec, + }; + // independent tasks: one task's actor build throws, the other is fine + const results = await runScheduledTasks([task("boom"), task("ok")], deps); + const byId = Object.fromEntries(results.map((r) => [r.taskId, r])); + // the failure is task-fatal (NEEDS_HUMAN), not session-fatal + expect(byId.boom?.outcome?.finalState).toBe("NEEDS_HUMAN"); + expect(byId.boom?.outcome?.degradedReason).toMatch(/actor build failed/i); + // the independent sibling still ran to GREEN + expect(byId.ok?.outcome?.finalState).toBe("GREEN"); + }); + + it("cancels via AbortSignal, draining in-flight tasks before throwing", async () => { + const ac = new AbortController(); + // a slow gate keeps tasks in-flight so cancellation lands mid-run + const slow: CommandExecutor = async () => { + await new Promise((r) => setTimeout(r, 30)); + return { exitCode: 0, output: "", durationMs: 30 }; + }; + const deps = { ...greenDeps(cfg(2), slow), signal: ac.signal }; + const p = runScheduledTasks([task("a"), task("b")], deps); + setTimeout(() => ac.abort(), 5); + await expect(p).rejects.toThrow(/cancelled/i); + }); +}); diff --git a/test/server.test.ts b/test/server.test.ts index 224446d..c7d9c82 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -195,6 +195,39 @@ describe("server: auth boundary", () => { }); }); +describe("server: run cancellation", () => { + it("cancels an in-flight run and surfaces a terminal error", async () => { + await server?.stop(); + server = createServer({ + store: new MemoryStore(), + config: baseConfig, + baseEnv: {}, + token: TOKEN, + // A run that only settles when its AbortSignal fires. + runGoalImpl: (_g, _c, o = {}) => + new Promise((_resolve, reject) => { + o.signal?.addEventListener("abort", () => { + const e = new Error("run cancelled"); + e.name = "AbortError"; + reject(e); + }); + }), + }); + base = `http://127.0.0.1:${await server.start(0)}`; + + const sessionId = await startRun("long running"); + expect((await fetch(`${base}/api/runs/ghost/cancel`, { method: "POST", headers: auth() })).status).toBe(404); + + const cancelled = await fetch(`${base}/api/runs/${sessionId}/cancel`, { method: "POST", headers: auth() }); + expect(cancelled.status).toBe(202); + + const stream = await fetch(`${base}/api/runs/${sessionId}/stream`, { headers: auth() }); + const msgs = await readSse(stream, (m) => m.some((x) => x.event === "status" && x.data.phase === "error")); + const err = msgs.find((m) => m.event === "status" && m.data.phase === "error"); + expect(err?.data.error).toContain("cancelled"); + }); +}); + describe("server: admission control", () => { it("rejects new runs past the active cap with 429", async () => { let release: () => void = () => {}; diff --git a/test/session.test.ts b/test/session.test.ts index f1a6012..9bd8209 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { runGoal, openBlockers, finalSessionStatus } from "../src/session.js"; +import { runGoal, openBlockers, finalSessionStatus, reconcileInterruptedSessions } from "../src/session.js"; import { loadConfig } from "../src/config.js"; import { MockRunner } from "../src/runners/mockRunner.js"; import type { AgentRunner, RunnerProfile, RunRequest } from "../src/runners/agentRunner.js"; @@ -158,3 +158,19 @@ describe("finalSessionStatus", () => { expect(finalSessionStatus(0, { ok: false })).toBe("needs_human"); }); }); + +describe("reconcileInterruptedSessions", () => { + it("marks orphaned running sessions as failed with an event", async () => { + const store = new MemoryStore(); + const now = new Date().toISOString(); + await store.createSession({ id: "stuck", goal: "g", createdAt: now, updatedAt: now, status: "running" }); + await store.createSession({ id: "ok", goal: "g", createdAt: now, updatedAt: now, status: "completed" }); + + const n = await reconcileInterruptedSessions(store); + expect(n).toBe(1); + expect((await store.getSession("stuck"))?.status).toBe("failed"); + expect((await store.getSession("ok"))?.status).toBe("completed"); // untouched + const events = await store.listEvents("stuck"); + expect(events.some((e) => e.type === "session_interrupted")).toBe(true); + }); +}); From be3e0672f0227348c1dc2eeeaa31f27919e41ee1 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 20:10:34 +0000 Subject: [PATCH 09/19] fix: thread cancellation into model runners and integration Cancel now reaches the model runners and the integration step, not just the loop and the mechanical gate. - RunRequest carries an optional AbortSignal; HttpRunner aborts its in-flight fetch and CliRunner kills its subprocess tree (reusing killProcessTree so descendants die too) when the signal fires. - Thread the run's signal through createRoles into RunnerActor and RunnerCritic so every actor/critic runner.run call is cancellable. - Make integrate() cancel-aware: stop merging further branches once aborted and pass the signal into the verification gate. So clicking Stop during a long HTTP/CLI model call or during integration returns promptly instead of waiting out the runner/request timeout. Co-authored-by: Inzimam Ul Haq <27832433+inhaq@users.noreply.github.com> --- src/adapters/roleBindings.ts | 4 ++++ src/adapters/runnerRoles.ts | 24 ++++++++++++++++++++++-- src/engine/integrator.ts | 9 +++++++++ src/runners/agentRunner.ts | 6 ++++++ src/runners/cliRunner.ts | 31 ++++++++++++++++++++++++++++--- src/runners/httpRunner.ts | 22 ++++++++++++++++++++-- src/session.ts | 2 ++ 7 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/adapters/roleBindings.ts b/src/adapters/roleBindings.ts index 9061a25..7f932d4 100644 --- a/src/adapters/roleBindings.ts +++ b/src/adapters/roleBindings.ts @@ -38,6 +38,8 @@ export interface CreateRolesOptions { log?: (line: string) => void; /** when set, every runner call emits an event attributed to its role (M5) */ onRunnerCall?: RunnerCallSink; + /** cooperative cancellation, threaded into the actor/critic runner calls */ + signal?: AbortSignal; } function resolveProfile( @@ -96,12 +98,14 @@ export function createRoles( ...(opts.actorPrompts ? { prompts: opts.actorPrompts } : {}), ...(opts.cwd ? { cwd: opts.cwd } : {}), ...(opts.log ? { log: opts.log } : {}), + ...(opts.signal ? { signal: opts.signal } : {}), }); const critic = new RunnerCritic(instrumentedCritic, { ...(opts.criticPrompts ? { prompts: opts.criticPrompts } : {}), ...(opts.cwd ? { cwd: opts.cwd } : {}), ...(opts.log ? { log: opts.log } : {}), + ...(opts.signal ? { signal: opts.signal } : {}), }); return { actor, critic }; diff --git a/src/adapters/runnerRoles.ts b/src/adapters/runnerRoles.ts index db430ae..1940b3a 100644 --- a/src/adapters/runnerRoles.ts +++ b/src/adapters/runnerRoles.ts @@ -64,12 +64,16 @@ export interface RunnerActorOptions { /** retry once with a corrective nudge when output won't parse (default true) */ repairOnce?: boolean; log?: (line: string) => void; + /** cooperative cancellation, threaded into every runner call */ + signal?: AbortSignal; } export interface RunnerCriticOptions { prompts?: CriticPromptTemplates; cwd?: string; log?: (line: string) => void; + /** cooperative cancellation, threaded into every runner call */ + signal?: AbortSignal; } /** Actor role backed by an AgentRunner + prompt templates. */ @@ -79,6 +83,7 @@ export class RunnerActor implements Actor { private readonly cwd: string; private readonly repairOnce: boolean; private readonly log: ((line: string) => void) | undefined; + private readonly signal: AbortSignal | undefined; constructor(runner: AgentRunner, opts: RunnerActorOptions = {}) { this.runner = runner; @@ -86,6 +91,7 @@ export class RunnerActor implements Actor { this.cwd = opts.cwd ?? "."; this.repairOnce = opts.repairOnce ?? true; this.log = opts.log; + this.signal = opts.signal; } async draftPlan(goal: string, feedback?: Finding[]): Promise { @@ -120,6 +126,7 @@ export class RunnerActor implements Actor { prompt: this.prompts.selfReview(bundle), cwd: this.cwd, system: this.prompts.system, + ...(this.signal ? { signal: this.signal } : {}), }); return { text: res.text, quotaExhausted: res.quotaExhausted }; } @@ -130,7 +137,12 @@ export class RunnerActor implements Actor { schema: S, what: string, ): Promise> { - let res = await this.runner.run({ prompt, cwd: this.cwd, system: this.prompts.system }); + let res = await this.runner.run({ + prompt, + cwd: this.cwd, + system: this.prompts.system, + ...(this.signal ? { signal: this.signal } : {}), + }); if (res.quotaExhausted) { throw new RunnerRoleError(`Actor ${what} failed: runner quota exhausted.`, { quotaExhausted: true, @@ -144,6 +156,7 @@ export class RunnerActor implements Actor { prompt: `${prompt}\n\n${PARSE_REPAIR_NUDGE}`, cwd: this.cwd, system: this.prompts.system, + ...(this.signal ? { signal: this.signal } : {}), }); if (res.quotaExhausted) { throw new RunnerRoleError(`Actor ${what} failed: runner quota exhausted.`, { @@ -165,11 +178,13 @@ export class RunnerCritic implements Critic { private readonly runner: AgentRunner; private readonly prompts: CriticPromptTemplates; private readonly cwd: string; + private readonly signal: AbortSignal | undefined; constructor(runner: AgentRunner, opts: RunnerCriticOptions = {}) { this.runner = runner; this.prompts = opts.prompts ?? DEFAULT_CRITIC_PROMPTS; this.cwd = opts.cwd ?? "."; + this.signal = opts.signal; } /** @@ -183,7 +198,12 @@ export class RunnerCritic implements Critic { req.kind === "plan" ? this.prompts.planReview(req.goal, req.plan, req.repairHint) : this.prompts.taskReview(req.bundle, req.repairHint); - const res = await this.runner.run({ prompt, cwd: this.cwd, system: this.prompts.system }); + const res = await this.runner.run({ + prompt, + cwd: this.cwd, + system: this.prompts.system, + ...(this.signal ? { signal: this.signal } : {}), + }); return { text: res.text, quotaExhausted: res.quotaExhausted }; } } diff --git a/src/engine/integrator.ts b/src/engine/integrator.ts index f64d686..2d06b5c 100644 --- a/src/engine/integrator.ts +++ b/src/engine/integrator.ts @@ -52,6 +52,8 @@ export interface IntegrateInput { verifyCommands?: string[]; /** executor for the verification gate (defaults to the gate's real one) */ executor?: CommandExecutor; + /** cooperative cancellation: skips remaining merges and aborts verification */ + signal?: AbortSignal; git?: GitExec; log?: (line: string) => void; } @@ -76,6 +78,12 @@ export async function integrate(input: IntegrateInput): Promise { const started = Date.now(); + // Already cancelled before we spawned: don't launch the subprocess at all. + if (req.signal?.aborted) { + resolve({ + stdout: "", + stderr: "[cancelled]", + exitCode: 124, + timedOut: false, + durationMs: 0, + spawnError: "run cancelled", + }); + return; + } const child = spawn(command, args, { cwd: req.cwd, env, @@ -198,6 +210,7 @@ export class CliRunner implements AgentRunner { let stdout = ""; let stderr = ""; let timedOut = false; + let cancelled = false; child.stdout.on("data", (b: Buffer) => { stdout += b.toString(); @@ -213,8 +226,20 @@ export class CliRunner implements AgentRunner { killProcessTree(child); }, timeoutMs); - child.on("error", (err) => { + // External cancellation: kill the whole subprocess tree so a long model + // call stops promptly instead of waiting out the runner timeout. + const onAbort = (): void => { + cancelled = true; + killProcessTree(child); + }; + req.signal?.addEventListener("abort", onAbort, { once: true }); + const cleanup = (): void => { clearTimeout(timer); + req.signal?.removeEventListener("abort", onAbort); + }; + + child.on("error", (err) => { + cleanup(); resolve({ stdout, stderr, @@ -225,11 +250,11 @@ export class CliRunner implements AgentRunner { }); }); child.on("close", (code) => { - clearTimeout(timer); + cleanup(); resolve({ stdout, stderr, - exitCode: timedOut ? 124 : (code ?? 1), + exitCode: timedOut || cancelled ? 124 : (code ?? 1), timedOut, durationMs: Date.now() - started, }); diff --git a/src/runners/httpRunner.ts b/src/runners/httpRunner.ts index 5dba323..9cea8fe 100644 --- a/src/runners/httpRunner.ts +++ b/src/runners/httpRunner.ts @@ -158,6 +158,15 @@ export class HttpRunner implements AgentRunner { const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs); const started = Date.now(); + // External cancellation: if the run is cancelled, abort the in-flight fetch + // immediately rather than waiting out the request timeout. If the signal has + // already fired, abort before we even dispatch. + const onAbort = (): void => controller.abort(); + if (req.signal) { + if (req.signal.aborted) controller.abort(); + else req.signal.addEventListener("abort", onAbort, { once: true }); + } + try { const res = await this.fetchImpl(url, { method: "POST", @@ -203,6 +212,9 @@ export class HttpRunner implements AgentRunner { }, }; } catch (err) { + // Distinguish an external cancellation from a timeout: both abort the same + // controller, so we check the caller's signal first. + const cancelled = req.signal?.aborted ?? false; const aborted = controller.signal.aborted; return { text: "", @@ -211,12 +223,18 @@ export class HttpRunner implements AgentRunner { runnerId: this.profile.id, model: this.profile.model, durationMs: Date.now() - started, - timedOut: aborted, - error: aborted ? "request timed out" : String((err as Error).message ?? err), + timedOut: aborted && !cancelled, + cancelled, + error: cancelled + ? "request cancelled" + : aborted + ? "request timed out" + : String((err as Error).message ?? err), }, }; } finally { clearTimeout(timer); + req.signal?.removeEventListener("abort", onAbort); } } diff --git a/src/session.ts b/src/session.ts index 5680222..960c9df 100644 --- a/src/session.ts +++ b/src/session.ts @@ -141,6 +141,7 @@ export async function runGoal( const { actor, critic } = createRoles(config, { ...roleOpts, ...(onRunnerCall ? { onRunnerCall } : {}), + ...(signal ? { signal } : {}), }); // Checkpoint to the store and (optionally) fan out to a caller-supplied @@ -262,6 +263,7 @@ export async function runGoal( branches: greenBranches, ...(verifyCommands.length ? { verifyCommands } : {}), ...(executor ? { executor } : {}), + ...(signal ? { signal } : {}), ...(opts.log ? { log: opts.log } : {}), }); } From 9d50d8284fc932e8f81163832b6b636a98c80039 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 20:16:27 +0000 Subject: [PATCH 10/19] ci: build and bundle the desktop/Tauri release path The existing matrix only exercised the headless engine (npm ci, typecheck, test). Add a dedicated desktop job that proves the full desktop shell builds on Linux/macOS/Windows: - Bun + Rust toolchains and the Tauri v2 Linux system deps (incl. libsecret for the keyring backend). - desktop npm ci + npm audit (high), build the engine sidecar, build the Vite frontend, cargo test the Rust crate, then a full tauri build. This closes the gap where a break in the Tauri shell or sidecar could ship unnoticed. --- .github/workflows/build.yml | 92 +++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72c84da..631cd39 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,3 +42,95 @@ jobs: - name: Test run: npm test + + # Proves the desktop/Tauri release path actually builds: the Bun-compiled + # engine sidecar, the Vite frontend, the Rust crate (cargo), and the full + # `tauri build` bundle. The JS `build` job above only exercises the headless + # engine, so a break in the desktop shell would otherwise ship unnoticed. + desktop: + name: desktop (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + # The sidecar (src/server/index.ts) is compiled to a standalone binary + # with Bun, which Tauri then embeds as an external binary. + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + # Cache the (large) Cargo build of the Tauri dependency tree, keyed on the + # crate's Cargo.lock, so reruns don't recompile from scratch. + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: desktop/src-tauri -> target + + # Tauri v2 needs the GTK/WebKit stack on Linux; the keyring crate's + # secret-service backend additionally needs libsecret. macOS and Windows + # ship the required system frameworks already. + - name: Install Linux system dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev \ + libssl-dev \ + libsecret-1-dev \ + patchelf \ + file \ + build-essential + + # The sidecar bundles src/, which imports from the root package's deps. + - name: Install root dependencies + run: npm ci + + - name: Install desktop dependencies + run: npm ci + working-directory: desktop + + # Surface known-high/critical advisories in the desktop dependency tree. + - name: Audit desktop dependencies + run: npm audit --audit-level=high + working-directory: desktop + + # Build the engine sidecar binary (binaries/loopwright-engine-) + # before the Rust build, which validates the externalBin in build.rs. + - name: Build engine sidecar + run: npm run build:sidecar + working-directory: desktop + + # tsc --noEmit && vite build -> produces desktop/dist for the Tauri build. + - name: Build desktop frontend + run: npm run build + working-directory: desktop + + # Compiles the Rust crate (and runs build.rs / tauri-build) to prove the + # native side builds; there are no Rust unit tests yet. + - name: Cargo test + run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --locked + + # Full release-path build: bundles the app for the host platform. + - name: Tauri build + run: npm run tauri build + working-directory: desktop From d244a61d55d35c933813b488b731a6dbc449490c Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 20:30:08 +0000 Subject: [PATCH 11/19] ci: fix Linux AppImage bundling on GitHub runners The desktop ubuntu job built the binary and bundled .deb and .rpm, but AppImage bundling failed with 'failed to run linuxdeploy': the bundling tools are AppImages and GitHub runners lack FUSE. Set APPIMAGE_EXTRACT_AND_RUN=1 so they extract-and-run instead of mounting. macOS and Windows builds already pass. --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 631cd39..24356b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -131,6 +131,12 @@ jobs: run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --locked # Full release-path build: bundles the app for the host platform. + # APPIMAGE_EXTRACT_AND_RUN lets the AppImage bundling tools (linuxdeploy / + # appimagetool, themselves AppImages) run on GitHub runners, which lack + # FUSE; without it Linux AppImage bundling fails with "failed to run + # linuxdeploy". No-op on macOS/Windows. - name: Tauri build run: npm run tauri build working-directory: desktop + env: + APPIMAGE_EXTRACT_AND_RUN: 1 From b2b193b952ce4612d36417f27f78c696ab0eb429 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 20:39:58 +0000 Subject: [PATCH 12/19] ci: bundle deb+rpm on Linux, skip flaky AppImage step APPIMAGE_EXTRACT_AND_RUN did not resolve the linuxdeploy failure on the GitHub Ubuntu runner. Restrict the Linux bundle to deb + rpm (which build reliably and exercise the full compile-and-bundle pipeline) and keep full all-target bundling on macOS and Windows. --- .github/workflows/build.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24356b1..838861c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -130,13 +130,16 @@ jobs: - name: Cargo test run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --locked - # Full release-path build: bundles the app for the host platform. - # APPIMAGE_EXTRACT_AND_RUN lets the AppImage bundling tools (linuxdeploy / - # appimagetool, themselves AppImages) run on GitHub runners, which lack - # FUSE; without it Linux AppImage bundling fails with "failed to run - # linuxdeploy". No-op on macOS/Windows. - - name: Tauri build + # Full release-path build. On Linux we bundle deb + rpm but skip AppImage: + # its bundler (linuxdeploy, itself an AppImage) is unreliable on GitHub's + # runners due to the FUSE/sandbox restrictions, and deb + rpm already + # prove the compile-and-bundle pipeline. macOS/Windows bundle all targets. + - name: Tauri build (Linux, deb + rpm) + if: matrix.os == 'ubuntu-latest' + run: npm run tauri build -- --bundles deb,rpm + working-directory: desktop + + - name: Tauri build (macOS/Windows) + if: matrix.os != 'ubuntu-latest' run: npm run tauri build working-directory: desktop - env: - APPIMAGE_EXTRACT_AND_RUN: 1 From 7b650b83af55465cbc3626a7fc45d7dac50d070c Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 21:04:39 +0000 Subject: [PATCH 13/19] fix: graceful shutdown, session-id validation, pinned Bun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review findings: - Graceful server shutdown: stop() (and a new token-protected POST /api/shutdown) now aborts in-flight runs — whose cooperative cancel handlers synchronously kill detached subprocess trees — ends tracked SSE streams, then closes the HTTP server, force-closing any lingering sockets after a grace period. Previously stop() only called http.close(), which could hang on long-lived SSE streams and orphan work. - Tauri restart/quit now calls the engine's graceful shutdown endpoint before child.kill(), so detached subprocess trees aren't orphaned by a hard sidecar kill. lib.rs builds the app and handles ExitRequested/Exit to stop the engine on quit. - Validate caller-supplied sessionId (^[A-Za-z0-9_-]{1,64}$) at the server boundary before it becomes a worktree path or branch ref; slug the session id in GitWorktreeManager as defense in depth. - Pin CI Bun to 1.2.14 so the shipped sidecar binary builds reproducibly. --- .github/workflows/build.yml | 6 +- desktop/src-tauri/src/engine.rs | 62 +++++++++++++++++- desktop/src-tauri/src/lib.rs | 15 ++++- src/server/index.ts | 8 +++ src/server/server.ts | 107 +++++++++++++++++++++++++++++++- src/workspace/worktrees.ts | 8 ++- 6 files changed, 197 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 838861c..3382efb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,11 +65,13 @@ jobs: node-version: 22 # The sidecar (src/server/index.ts) is compiled to a standalone binary - # with Bun, which Tauri then embeds as an external binary. + # with Bun, which Tauri then embeds as an external binary. Pin the version + # so the shipped sidecar is built reproducibly rather than with whatever + # "latest" happens to be at build time. - name: Set up Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.2.14 - name: Set up Rust uses: dtolnay/rust-toolchain@stable diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 81af507..32634b9 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -9,6 +9,8 @@ //! secret crossing into the webview or onto disk in plaintext. use std::collections::HashMap; +use std::io::{Read, Write}; +use std::net::TcpStream; use std::sync::Mutex; use std::time::{Duration, Instant}; @@ -103,15 +105,71 @@ impl EngineManager { /// Kills the running sidecar (if any) and starts a fresh one. Used after /// secrets change so the new values are picked up. pub fn restart(&self) -> Result { - if let Some(child) = self.child.lock().unwrap().take() { + self.shutdown(); + self.start() + } + + /// Stops the running sidecar gracefully: asks the engine to shut itself + /// down over HTTP first — so it can cancel in-flight runs and kill their + /// detached subprocess trees — and only then force-kills as a fallback. + /// A direct `child.kill()` would orphan those detached descendants. + pub fn shutdown(&self) { + let child = self.child.lock().unwrap().take(); + let url = self.url.lock().unwrap().clone(); + let token = self.token.lock().unwrap().clone(); + + if let Some(url) = url.as_deref() { + request_shutdown(url, token.as_deref()); + } + // Give the engine a brief moment to act on the request and exit on its + // own before we force-kill. + if child.is_some() { + std::thread::sleep(Duration::from_millis(400)); + } + if let Some(child) = child { + // Best-effort fallback: a no-op if the engine already exited. let _ = child.kill(); } + *self.url.lock().unwrap() = None; *self.token.lock().unwrap() = None; - self.start() } } +/// Sends a best-effort `POST /api/shutdown` to the engine over loopback so it +/// can tear itself down gracefully. Uses a raw, short-lived TCP request to +/// avoid pulling in an HTTP client dependency for a single local call; all +/// errors are ignored because `shutdown()` force-kills as a fallback. +fn request_shutdown(url: &str, token: Option<&str>) { + // url looks like "http://127.0.0.1:53187"; reduce it to "host:port". + let authority = match url.strip_prefix("http://") { + Some(rest) => rest.split('/').next().unwrap_or(rest), + None => return, + }; + + let mut stream = match TcpStream::connect(authority) { + Ok(s) => s, + Err(_) => return, + }; + let _ = stream.set_write_timeout(Some(Duration::from_secs(2))); + let _ = stream.set_read_timeout(Some(Duration::from_secs(2))); + + let auth_line = match token { + Some(t) => format!("Authorization: Bearer {t}\r\n"), + None => String::new(), + }; + let req = format!( + "POST /api/shutdown HTTP/1.1\r\nHost: {authority}\r\n{auth_line}\ +Content-Length: 0\r\nConnection: close\r\n\r\n" + ); + let _ = stream.write_all(req.as_bytes()); + let _ = stream.flush(); + // Read (and discard) the response so we wait for the server to acknowledge + // before returning; the connection closing also signals it has begun. + let mut buf = [0u8; 256]; + let _ = stream.read(&mut buf); +} + /// The engine's startup handshake: where to reach it and the token to use. struct Ready { url: String, diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ce04030..70133e7 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -48,7 +48,7 @@ fn list_secret_keys(app: tauri::AppHandle) -> Result, String> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() + let app = tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .setup(|app| { // Start the engine sidecar up front so the UI has an endpoint as @@ -69,6 +69,17 @@ pub fn run() { delete_secret, list_secret_keys ]) - .run(tauri::generate_context!()) + .build(tauri::generate_context!()) .expect("error while running tauri application"); + + // Gracefully stop the engine sidecar when the app is exiting so it can + // cancel in-flight runs and kill their detached subprocess trees, instead + // of being orphaned by an abrupt process teardown. + app.run(|app_handle, event| { + if let tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit = event { + if let Some(manager) = app_handle.try_state::() { + manager.shutdown(); + } + } + }); } diff --git a/src/server/index.ts b/src/server/index.ts index ad4fe10..493db86 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -53,6 +53,8 @@ async function main(): Promise { store, config, token, + // When a graceful shutdown finishes (signal or POST /api/shutdown), exit. + onShutdown: () => process.exit(0), ...(staticDir ? { staticDir } : {}), }); @@ -76,7 +78,13 @@ async function main(): Promise { // Single-line, parseable readiness signal for the supervising process. console.log(JSON.stringify({ loopwright: "listening", host, port: bound, token })); + // Graceful shutdown on signals: stop() aborts in-flight runs (killing their + // detached subprocess trees) and closes SSE streams before the process exits, + // so a SIGTERM/SIGINT can't orphan active work or hang on open streams. + let stopping = false; const shutdown = (): void => { + if (stopping) return; + stopping = true; void server.stop().then(() => process.exit(0)); }; process.on("SIGINT", shutdown); diff --git a/src/server/server.ts b/src/server/server.ts index 5529868..f055627 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -77,6 +77,17 @@ export interface CreateServerOptions { retainMs?: number; /** most recent messages retained per run for SSE replay */ maxBufferPerRun?: number; + /** + * Invoked once a graceful shutdown (via stop() or POST /api/shutdown) has + * finished tearing the server down — e.g. the entrypoint passes + * `() => process.exit(0)`. + */ + onShutdown?: () => void; + /** + * How long (ms) stop() waits for in-flight connections to drain before + * force-closing any that remain (idle keep-alive sockets, slow clients). + */ + shutdownGraceMs?: number; } export interface LoopwrightServer { @@ -105,6 +116,20 @@ export function isLoopbackHost(host: string): boolean { return LOOPBACK_HOSTS.has(host.trim().toLowerCase()); } +/** + * Caller-supplied session ids flow into git branch names and worktree + * directory paths (see workspace/worktrees.ts), so they must be a bounded, + * filesystem- and ref-safe token. We accept letters, digits, `_` and `-` only + * (which covers the UUIDs we mint for new runs) and forbid `.`/`/` so a value + * like `../../etc` or `..` can never escape the worktree root or forge a ref. + */ +const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,64}$/; + +/** Whether a caller-supplied session id is safe to use in paths/refs. */ +export function isValidSessionId(id: string): boolean { + return SESSION_ID_RE.test(id); +} + /** Loopback and the Tauri webview are the only origins ever granted CORS. */ function originAllowed(origin: string): boolean { if (origin === "tauri://localhost") return true; @@ -167,10 +192,16 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const token = opts.token ?? randomUUID(); const maxActiveRuns = opts.maxActiveRuns ?? 4; const retainMs = opts.retainMs ?? 5 * 60_000; + const onShutdown = opts.onShutdown; + const shutdownGraceMs = opts.shutdownGraceMs ?? 3_000; const hub = new RunHub(opts.maxBufferPerRun); let activeRuns = 0; /** abort controllers for in-flight runs, keyed by session id (for cancel) */ const controllers = new Map(); + /** open SSE responses, so a graceful shutdown can end them deterministically */ + const sseClients = new Set(); + /** memoized graceful-shutdown promise so stop() is safe to call repeatedly */ + let closing: Promise | undefined; /** * Constant-time bearer-token check. The token is read from the Authorization @@ -242,6 +273,19 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { return send(res, 202, { cancelling: true }, cors); } + // Graceful shutdown: the desktop shell calls this before killing the + // sidecar so in-flight runs are cancelled (which kills their detached + // subprocess trees) and SSE clients are closed cleanly, rather than being + // hard-killed and orphaning work. Token-protected like the rest of /api. + if (pathname === "/api/shutdown" && method === "POST") { + send(res, 202, { shuttingDown: true }, cors); + // Defer so the 202 flushes before the server tears itself down. + setImmediate(() => { + void closeServer().then(() => onShutdown?.()); + }); + return; + } + if (pathname === "/api/sessions" && method === "GET") { const sessions = await store.listSessions(); sessions.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); @@ -290,6 +334,17 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { } runConfig.dbPath = config.dbPath; + // A caller-supplied session id becomes part of git branch names and + // worktree paths, so reject anything outside the bounded safe format before + // it reaches the filesystem/git. New runs without an id get a safe UUID. + if (body.sessionId !== undefined && !isValidSessionId(body.sessionId)) { + return send( + res, + 400, + { error: "sessionId must match /^[A-Za-z0-9_-]{1,64}$/" }, + cors, + ); + } const sessionId = body.sessionId ?? randomUUID(); if (hub.has(sessionId) && hub.phase(sessionId) === "running") { return send(res, 409, { error: `session ${sessionId} is already running` }, cors); @@ -400,6 +455,10 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { Number.isFinite(afterId) ? afterId : -1, ); + // Track this stream so a graceful shutdown can end it; otherwise the + // long-lived keep-alive connection would block http.close() indefinitely. + sseClients.add(res); + // Heartbeat keeps intermediaries from closing an idle stream during long // model calls. const heartbeat = setInterval(() => res.write(": ping\n\n"), 15_000); @@ -407,6 +466,7 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const close = (): void => { clearInterval(heartbeat); unsubscribe(); + sseClients.delete(res); }; req.on("close", close); res.on("close", close); @@ -469,6 +529,51 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { return undefined; } + /** + * Graceful teardown: stop in-flight work and close connections so the + * process can exit promptly without orphaning runs. + * 1. Abort every active run. The cooperative cancel handlers (HttpRunner, + * CliRunner, the mechanical gate) fire synchronously and kill detached + * subprocess trees, so no shell/build descendants are left running. + * 2. End open SSE streams — these are long-lived and would otherwise keep + * http.close() from ever completing. + * 3. Stop accepting new connections and wait for drain, then force-close + * anything still lingering after the grace period. + * Memoized so concurrent stop()/shutdown callers share one teardown. + */ + function closeServer(): Promise { + if (closing) return closing; + closing = (async () => { + for (const controller of controllers.values()) controller.abort(); + for (const res of sseClients) { + try { + res.end(); + } catch { + /* already closed */ + } + } + sseClients.clear(); + await new Promise((resolve) => { + let settled = false; + const finish = (): void => { + if (!settled) { + settled = true; + resolve(); + } + }; + http.close(() => finish()); + const timer = setTimeout(() => { + // Drop any sockets still open (idle keep-alive, slow clients) so the + // close callback can fire and we don't hang on shutdown. + http.closeAllConnections?.(); + finish(); + }, shutdownGraceMs); + if (typeof timer.unref === "function") timer.unref(); + }); + })(); + return closing; + } + return { http, hub, @@ -485,7 +590,7 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { }); }, stop(): Promise { - return new Promise((resolve) => http.close(() => resolve())); + return closeServer(); }, }; } diff --git a/src/workspace/worktrees.ts b/src/workspace/worktrees.ts index 6096582..6041b99 100644 --- a/src/workspace/worktrees.ts +++ b/src/workspace/worktrees.ts @@ -53,9 +53,13 @@ export class GitWorktreeManager { constructor(opts: WorktreeManagerOptions) { this.repoDir = opts.repoDir; - this.sessionId = opts.sessionId; + // Defense in depth: the session id becomes part of branch names and the + // worktree root path, so slug it here too (the server already restricts it + // to a safe format) — a stray `/` or `..` must never escape the root or + // forge a ref even if a caller bypasses the HTTP boundary. + this.sessionId = slug(opts.sessionId); this.baseRef = opts.baseRef ?? "HEAD"; - this.root = opts.root ?? path.join(opts.repoDir, ".loopwright", "worktrees", opts.sessionId); + this.root = opts.root ?? path.join(opts.repoDir, ".loopwright", "worktrees", this.sessionId); this.branchPrefix = opts.branchPrefix ?? "loopwright"; this.exec = opts.git ?? spawnGit; } From dbd51414f64d5c26ab7a20dd972095c37c031d2c Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 21:22:58 +0000 Subject: [PATCH 14/19] fix: drain active runs on shutdown, align Tauri kill, audit root, add tests Address PR review findings: - closeServer() now tracks in-flight run promises and awaits them (bounded by a single shutdownGraceMs deadline) after aborting, so a shutdown no longer races ahead of durable failure writes and worktree cleanup. New runs are refused once shutdown has begun. - Tauri shutdown() polls the engine's loopback port until the listener closes (up to 6s, comfortably above the server's grace window) instead of a fixed 400ms sleep, so it waits for the graceful path to finish before force-killing the sidecar. - CI: add root 'npm audit --audit-level=moderate' (the sidecar engine's dependency tree), alongside the existing desktop audit. - Tests: cover POST /api/shutdown, stop() waiting for an aborted run's cleanup to complete, and rejection of unsafe session ids (../x, a/b, empty, >64 chars). --- .github/workflows/build.yml | 6 +++ desktop/src-tauri/src/engine.rs | 33 +++++++++++-- src/server/server.ts | 50 ++++++++++++++++--- test/server.test.ts | 88 +++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3382efb..dadd6b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,12 @@ jobs: - name: Test run: npm test + # Surface known advisories in the root dependency tree (the engine that + # ships in the sidecar). Run once on Linux since it is OS-independent. + - name: Audit dependencies + if: matrix.os == 'ubuntu-latest' + run: npm audit --audit-level=moderate + # Proves the desktop/Tauri release path actually builds: the Bun-compiled # engine sidecar, the Vite frontend, the Rust crate (cargo), and the full # `tauri build` bundle. The JS `build` job above only exercises the headless diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 32634b9..789d210 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -120,11 +120,11 @@ impl EngineManager { if let Some(url) = url.as_deref() { request_shutdown(url, token.as_deref()); - } - // Give the engine a brief moment to act on the request and exit on its - // own before we force-kill. - if child.is_some() { - std::thread::sleep(Duration::from_millis(400)); + // Wait for the engine to actually stop serving before force-killing, + // so its graceful path (cancel runs, persist failures, clean up + // worktrees) can finish. Bounded well above the server's grace + // window so a wedged engine is still killed rather than hanging quit. + wait_for_listener_close(url, Duration::from_secs(6)); } if let Some(child) = child { // Best-effort fallback: a no-op if the engine already exited. @@ -170,6 +170,29 @@ Content-Length: 0\r\nConnection: close\r\n\r\n" let _ = stream.read(&mut buf); } +/// Polls the engine's loopback port until it stops accepting connections (i.e. +/// the graceful shutdown has closed the listener and the process is exiting), +/// or `timeout` elapses. On loopback a closed port refuses instantly, so this +/// returns as soon as the engine is done — typically well under the timeout. +fn wait_for_listener_close(url: &str, timeout: Duration) { + let authority = match url.strip_prefix("http://") { + Some(rest) => rest.split('/').next().unwrap_or(rest), + None => return, + }; + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + match TcpStream::connect(authority) { + // Still listening: the engine hasn't finished shutting down yet. + Ok(stream) => { + drop(stream); + std::thread::sleep(Duration::from_millis(100)); + } + // Connection refused: the listener is gone and the engine is exiting. + Err(_) => return, + } + } +} + /// The engine's startup handshake: where to reach it and the token to use. struct Ready { url: String, diff --git a/src/server/server.ts b/src/server/server.ts index f055627..e1b7dcc 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -172,6 +172,14 @@ async function readJsonBody(req: IncomingMessage, limitBytes = 1_000_000): Promi return JSON.parse(Buffer.concat(chunks).toString("utf8")); } +/** A promise that resolves after `ms`, using an unref'd timer (won't hold the process open). */ +function delay(ms: number): Promise { + return new Promise((resolve) => { + const timer = setTimeout(resolve, ms); + if (typeof timer.unref === "function") timer.unref(); + }); +} + const STATIC_CONTENT_TYPES: Record = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", @@ -200,6 +208,12 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { const controllers = new Map(); /** open SSE responses, so a graceful shutdown can end them deterministically */ const sseClients = new Set(); + /** + * Settle promises for in-flight runs, so a graceful shutdown can wait for + * each aborted run to finish its durable failure write and worktree cleanup + * before the process exits (rather than cutting them off). + */ + const activeRunPromises = new Set>(); /** memoized graceful-shutdown promise so stop() is safe to call repeatedly */ let closing: Promise | undefined; @@ -313,6 +327,10 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { } async function startRun(req: IncomingMessage, res: ServerResponse, cors: Record): Promise { + // Refuse new work once a graceful shutdown has begun, so we don't start a + // run the shutdown won't wait for. + if (closing) return send(res, 503, { error: "server is shutting down" }, cors); + let body: StartRunBody; try { body = ((await readJsonBody(req)) ?? {}) as StartRunBody; @@ -393,8 +411,10 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { // Fire-and-forget: the run proceeds in the background and the client // follows it over SSE. Errors (including cancellation) surface as a terminal - // status message. - void runGoalImpl(goal, runConfig, { + // status message. The settle promise is tracked so a graceful shutdown can + // wait for an aborted run to finish persisting its failure and cleaning up + // its worktree. + const run = runGoalImpl(goal, runConfig, { store: tappedStore, sessionId, resume: body.resume ?? false, @@ -404,15 +424,18 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { }) .then((result) => { hub.publish(sessionId, "status", { phase: "done", result } satisfies RunStatusData); - settle(); }) .catch((err: unknown) => { hub.publish(sessionId, "status", { phase: "error", error: String((err as Error)?.message ?? err), } satisfies RunStatusData); + }) + .finally(() => { settle(); + activeRunPromises.delete(run); }); + activeRunPromises.add(run); send(res, 202, { sessionId }, cors); } @@ -531,20 +554,32 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { /** * Graceful teardown: stop in-flight work and close connections so the - * process can exit promptly without orphaning runs. + * process can exit promptly without orphaning runs, all bounded by a single + * hard deadline (shutdownGraceMs) so shutdown can never hang. * 1. Abort every active run. The cooperative cancel handlers (HttpRunner, * CliRunner, the mechanical gate) fire synchronously and kill detached - * subprocess trees, so no shell/build descendants are left running. + * subprocess trees. Then wait — up to the deadline — for each run to + * finish settling, so its durable failure write and worktree cleanup are + * not cut off (the gap a bare http.close() left open). * 2. End open SSE streams — these are long-lived and would otherwise keep * http.close() from ever completing. * 3. Stop accepting new connections and wait for drain, then force-close - * anything still lingering after the grace period. + * anything still lingering when the deadline elapses. * Memoized so concurrent stop()/shutdown callers share one teardown. */ function closeServer(): Promise { if (closing) return closing; closing = (async () => { + const deadline = Date.now() + shutdownGraceMs; + const remaining = (): number => Math.max(0, deadline - Date.now()); + for (const controller of controllers.values()) controller.abort(); + if (activeRunPromises.size > 0) { + // Race the run settles against the deadline so a wedged run can't hang + // shutdown. The run promises never reject (errors are caught above). + await Promise.race([Promise.allSettled([...activeRunPromises]), delay(remaining())]); + } + for (const res of sseClients) { try { res.end(); @@ -553,6 +588,7 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { } } sseClients.clear(); + await new Promise((resolve) => { let settled = false; const finish = (): void => { @@ -567,7 +603,7 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { // close callback can fire and we don't hang on shutdown. http.closeAllConnections?.(); finish(); - }, shutdownGraceMs); + }, remaining()); if (typeof timer.unref === "function") timer.unref(); }); })(); diff --git a/test/server.test.ts b/test/server.test.ts index c7d9c82..7d537e5 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -141,6 +141,19 @@ async function readSse( return msgs; } +/** Polls `pred` until it returns truthy, or the timeout elapses. */ +async function waitFor( + pred: () => boolean | Promise, + timeoutMs = 3000, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await pred()) return true; + await new Promise((r) => setTimeout(r, 20)); + } + return false; +} + describe("server: health + validation", () => { beforeEach(() => startServer()); @@ -414,3 +427,78 @@ describe("server: live SSE stream", () => { expect(rest.length).toBe(expected); }); }); + +describe("server: graceful shutdown", () => { + it("POST /api/shutdown returns 202, tears down, and invokes onShutdown", async () => { + let shutdownCalled = false; + server = createServer({ + store: new MemoryStore(), + config: baseConfig, + baseEnv: {}, + token: TOKEN, + shutdownGraceMs: 500, + onShutdown: () => { + shutdownCalled = true; + }, + }); + base = `http://127.0.0.1:${await server.start(0)}`; + + const res = await fetch(`${base}/api/shutdown`, { method: "POST", headers: auth() }); + expect(res.status).toBe(202); + expect(await res.json()).toEqual({ shuttingDown: true }); + + // onShutdown fires only after the graceful teardown completes. + expect(await waitFor(() => shutdownCalled)).toBe(true); + }); + + it("stop() aborts an in-flight run and waits for its cleanup to finish", async () => { + let cleanedUp = false; + server = createServer({ + store: new MemoryStore(), + config: baseConfig, + baseEnv: {}, + token: TOKEN, + shutdownGraceMs: 2000, + // On cancel, perform async cleanup (the real engine persists the failure + // and removes worktrees here) before the run settles. + runGoalImpl: (_g, _c, o = {}) => + new Promise((_resolve, reject) => { + o.signal?.addEventListener("abort", () => { + setTimeout(() => { + cleanedUp = true; + const e = new Error("run cancelled"); + e.name = "AbortError"; + reject(e); + }, 50); + }); + }), + }); + base = `http://127.0.0.1:${await server.start(0)}`; + + await startRun("long running"); + + // stop() must not resolve until the aborted run has finished settling, so + // its durable cleanup is never cut off. + await server.stop(); + expect(cleanedUp).toBe(true); + }); + + it("rejects unsafe session ids before they reach git paths/refs", async () => { + await startServer(); + for (const bad of ["../x", "a/b", "", "x".repeat(65)]) { + const res = await fetch(`${base}/api/runs`, { + method: "POST", + headers: auth({ "content-type": "application/json" }), + body: JSON.stringify({ goal: "g", sessionId: bad }), + }); + expect(res.status, `expected 400 for sessionId ${JSON.stringify(bad)}`).toBe(400); + } + // A bounded, safe id (the shape of the UUIDs we mint) is still accepted. + const ok = await fetch(`${base}/api/runs`, { + method: "POST", + headers: auth({ "content-type": "application/json" }), + body: JSON.stringify({ goal: "g", sessionId: "Safe_id-123" }), + }); + expect(ok.status).toBe(202); + }); +}); From a72b4564546e88d3ad2004e8682448fcf8ffe0c7 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 21:49:54 +0000 Subject: [PATCH 15/19] chore(desktop): release metadata, release pipeline, shutdown tests Address remaining production-readiness findings: - Set a real release version 0.1.0 across the root and desktop package manifests, Cargo.toml/Cargo.lock, and tauri.conf.json (was 0.0.0). - Add production bundle metadata to tauri.conf.json (publisher, homepage, category, copyright, short/long description) so installers carry proper identity. - CI: upload the built installers as artifacts from every desktop run so QA has a downloadable deliverable, not just a green check. - Add a release workflow that builds per-OS and publishes installers to a draft GitHub Release on a version tag, with macOS notarization and updater signing wired through repository secrets (activate when configured). - Add targeted Rust tests for the sidecar shutdown path: request_shutdown posts an authorized POST /api/shutdown, and wait_for_listener_close returns on refusal, waits for the listener to close, and respects its timeout. Remaining team-owned items (documented in release.yml): real signing/ notarization secrets, confirming the bundle identifier's domain, and an auto-update endpoint. --- .github/workflows/build.yml | 12 +++ .github/workflows/release.yml | 122 ++++++++++++++++++++++++++++ desktop/package-lock.json | 4 +- desktop/package.json | 2 +- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/src/engine.rs | 127 ++++++++++++++++++++++++++++++ desktop/src-tauri/tauri.conf.json | 8 +- package-lock.json | 4 +- package.json | 2 +- 10 files changed, 276 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dadd6b1..744ab9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -151,3 +151,15 @@ jobs: if: matrix.os != 'ubuntu-latest' run: npm run tauri build working-directory: desktop + + # Preserve the built installers so QA and users have a downloadable + # deliverable from every CI run. The release workflow publishes tagged + # builds to a GitHub Release; this keeps untagged builds available too. + - name: Upload installers + uses: actions/upload-artifact@v4 + with: + name: loopwright-installers-${{ matrix.os }} + path: | + desktop/src-tauri/target/release/bundle/** + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f70af1f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,122 @@ +name: Release + +# Publishes desktop installers to a GitHub Release. Triggered by pushing a +# version tag (e.g. v0.1.0); `workflow_dispatch` allows a manual draft build. +# +# Code signing / notarization and updater signing are wired through the env +# block below and activate automatically once the corresponding repository +# secrets are configured — until then the workflow still builds and uploads +# unsigned installers as a draft release for QA. See the notes at the bottom. +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + name: release (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + # Build every platform even if one fails, so a single platform issue does + # not block publishing the others. + fail-fast: false + matrix: + include: + # Linux ships deb + rpm (AppImage's linuxdeploy is unreliable on + # GitHub runners; see build.yml). + - os: ubuntu-latest + args: "--bundles deb,rpm" + - os: macos-latest + args: "" + - os: windows-latest + args: "" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.2.14 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: desktop/src-tauri -> target + + - name: Install Linux system dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev \ + libssl-dev \ + libsecret-1-dev \ + patchelf \ + file \ + build-essential + + # The sidecar bundles src/, which imports from the root package's deps; + # tauri-action runs the beforeBuildCommand (build:sidecar && build) which + # needs both dependency trees installed. + - name: Install root dependencies + run: npm ci + + - name: Install desktop dependencies + run: npm ci + working-directory: desktop + + - name: Build, bundle, and publish the desktop app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # --- macOS signing + notarization (used automatically when set) --- + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # --- Updater artifact signing (used automatically when set) --- + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + projectPath: desktop + # On a tag push, reuse the tag; on manual dispatch, derive a unique tag. + tagName: ${{ github.ref_type == 'tag' && github.ref_name || format('v0.0.0-dev.{0}', github.run_number) }} + releaseName: Loopwright ${{ github.ref_type == 'tag' && github.ref_name || format('dev build {0}', github.run_number) }} + # Publish as a draft so a human reviews artifacts before release. + releaseDraft: true + prerelease: false + args: ${{ matrix.args }} + +# Production distribution checklist (requires team-owned credentials/decisions): +# - macOS: add APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / +# APPLE_SIGNING_IDENTITY / APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID secrets +# to sign + notarize. Windows: configure a code-signing cert in tauri.conf +# (bundle.windows.certificateThumbprint or a signCommand) + secret. +# - Confirm the bundle identifier (currently dev.loopwright.desktop) maps to a +# domain the org controls before first public release. +# - Auto-update: add the tauri updater plugin + endpoints and a signing key +# (TAURI_SIGNING_PRIVATE_KEY) to ship update artifacts. diff --git a/desktop/package-lock.json b/desktop/package-lock.json index a47d829..0175e93 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "loopwright-desktop", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loopwright-desktop", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { "@tauri-apps/api": "^2.0.0" }, diff --git a/desktop/package.json b/desktop/package.json index cd135eb..3df47b4 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "loopwright-desktop", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "description": "Tauri desktop shell for the Loopwright actor-critic engine", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 1d7de91..c70e063 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1773,7 +1773,7 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "loopwright-desktop" -version = "0.0.0" +version = "0.1.0" dependencies = [ "keyring", "serde", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index c1dd602..b433f27 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loopwright-desktop" -version = "0.0.0" +version = "0.1.0" description = "Desktop shell for the Loopwright actor-critic engine" edition = "2021" rust-version = "1.77" diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 789d210..f6fa685 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -255,3 +255,130 @@ fn parse_ready(buf: &str) -> Option { } None } + + +#[cfg(test)] +mod tests { + use super::{request_shutdown, wait_for_listener_close}; + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::thread; + use std::time::{Duration, Instant}; + + /// request_shutdown posts to /api/shutdown with the bearer token. + #[test] + fn request_shutdown_sends_authorized_post() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let addr = listener.local_addr().expect("addr"); + let url = format!("http://{addr}"); + + let server = thread::spawn(move || { + let (mut sock, _) = listener.accept().expect("accept"); + let mut buf = [0u8; 1024]; + let n = sock.read(&mut buf).expect("read"); + // Respond so the client's read() returns promptly. + let _ = sock.write_all( + b"HTTP/1.1 202 Accepted\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ); + String::from_utf8_lossy(&buf[..n]).into_owned() + }); + + request_shutdown(&url, Some("secret-token")); + + let req = server.join().expect("join"); + assert!( + req.starts_with("POST /api/shutdown HTTP/1.1"), + "unexpected request line: {req}" + ); + assert!( + req.contains("Authorization: Bearer secret-token"), + "missing/incorrect auth header: {req}" + ); + } + + /// request_shutdown omits the auth header when no token is known, and a bad + /// (non-http) url is a no-op rather than a panic. + #[test] + fn request_shutdown_handles_no_token_and_bad_url() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let addr = listener.local_addr().expect("addr"); + let url = format!("http://{addr}"); + + let server = thread::spawn(move || { + let (mut sock, _) = listener.accept().expect("accept"); + let mut buf = [0u8; 1024]; + let n = sock.read(&mut buf).expect("read"); + let _ = sock.write_all(b"HTTP/1.1 202 Accepted\r\nContent-Length: 0\r\n\r\n"); + String::from_utf8_lossy(&buf[..n]).into_owned() + }); + + request_shutdown(&url, None); + let req = server.join().expect("join"); + assert!(!req.contains("Authorization:"), "should not send auth: {req}"); + + // Non-http url must not panic or hang. + request_shutdown("ftp://example.com", Some("t")); + } + + /// Returns immediately (well under the timeout) when nothing is listening. + #[test] + fn wait_for_listener_close_returns_when_refused() { + // Bind then drop to obtain a port that is no longer accepting. + let addr = { + let l = TcpListener::bind("127.0.0.1:0").expect("bind"); + l.local_addr().expect("addr") + }; + let url = format!("http://{addr}"); + + let start = Instant::now(); + wait_for_listener_close(&url, Duration::from_secs(2)); + assert!( + start.elapsed() < Duration::from_secs(1), + "should return promptly on connection refused" + ); + } + + /// Returns once the listener actually closes, not before. + #[test] + fn wait_for_listener_close_waits_for_close() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let addr = listener.local_addr().expect("addr"); + let url = format!("http://{addr}"); + + let closer = thread::spawn(move || { + thread::sleep(Duration::from_millis(300)); + drop(listener); // stop accepting + }); + + let start = Instant::now(); + wait_for_listener_close(&url, Duration::from_secs(5)); + let elapsed = start.elapsed(); + closer.join().expect("join"); + + assert!( + elapsed >= Duration::from_millis(250), + "should not return before the listener closes (elapsed {elapsed:?})" + ); + assert!( + elapsed < Duration::from_secs(5), + "should return shortly after close, not at the timeout (elapsed {elapsed:?})" + ); + } + + /// Respects the timeout when the listener never closes (a wedged engine). + #[test] + fn wait_for_listener_close_respects_timeout() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let addr = listener.local_addr().expect("addr"); + let url = format!("http://{addr}"); + + let start = Instant::now(); + wait_for_listener_close(&url, Duration::from_millis(300)); + let elapsed = start.elapsed(); + assert!( + elapsed >= Duration::from_millis(250), + "should wait out the timeout while still listening (elapsed {elapsed:?})" + ); + drop(listener); + } +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index da2326d..635117b 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "Loopwright", - "version": "0.0.0", + "version": "0.1.0", "identifier": "dev.loopwright.desktop", "build": { "frontendDist": "../dist", @@ -25,6 +25,12 @@ "bundle": { "active": true, "targets": "all", + "publisher": "Loopwright", + "homepage": "https://github.com/inhaq/loopwright", + "category": "DeveloperTool", + "copyright": "Copyright © 2026 Loopwright", + "shortDescription": "Autonomous actor-critic coding-agent orchestrator.", + "longDescription": "Loopwright runs a model-agnostic actor-critic loop that orchestrates autonomous coding agents, with a desktop shell over the headless engine.", "externalBin": ["binaries/loopwright-engine"], "icon": [ "icons/32x32.png", diff --git a/package-lock.json b/package-lock.json index cd06d8c..865e5ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "loopwright", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loopwright", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { "zod": "^3.23.8" }, diff --git a/package.json b/package.json index 828885e..0bc8303 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopwright", - "version": "0.0.0", + "version": "0.1.0", "private": true, "description": "A model-agnostic actor-critic loop engine for orchestrating autonomous coding agents", "type": "module", From bf1b00717636c8e118a9962b07cfd7bd67c62466 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 21:57:53 +0000 Subject: [PATCH 16/19] test(desktop): relax cross-platform timing in listener-close test Windows loopback connection-refused can take ~1s (vs instant on Linux/macOS), so the <1s bound was brittle. Assert the function returns well before its timeout instead, which still proves it returns on refusal rather than hanging. --- desktop/src-tauri/src/engine.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index f6fa685..9d2e20e 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -320,7 +320,8 @@ mod tests { request_shutdown("ftp://example.com", Some("t")); } - /// Returns immediately (well under the timeout) when nothing is listening. + /// Returns on connection refusal (does not wait out the timeout) when + /// nothing is listening. #[test] fn wait_for_listener_close_returns_when_refused() { // Bind then drop to obtain a port that is no longer accepting. @@ -331,10 +332,14 @@ mod tests { let url = format!("http://{addr}"); let start = Instant::now(); - wait_for_listener_close(&url, Duration::from_secs(2)); + wait_for_listener_close(&url, Duration::from_secs(5)); + // It must return on refusal rather than hang to the timeout. Windows + // loopback refusal can take ~1s, so allow a generous margin while still + // proving it returned well before the 5s timeout. assert!( - start.elapsed() < Duration::from_secs(1), - "should return promptly on connection refused" + start.elapsed() < Duration::from_secs(3), + "should return on connection refused, not wait out the timeout (elapsed {:?})", + start.elapsed() ); } From 88bea4c1704ece1874beb4db9f87cb048cda6ad9 Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 22:20:15 +0000 Subject: [PATCH 17/19] ci: gate releases on quality checks + tag/version match; derive keychain namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address release-readiness findings: - release.yml now runs a 'gate' job (typecheck, root+desktop audits, JS tests, cargo test) that the publish job depends on, so a bad tagged commit can no longer produce a draft release without passing the same checks as main. - gate also verifies, on tag builds, that the vX.Y.Z tag exactly matches the version in package.json, desktop/package.json, tauri.conf.json, and Cargo.toml — failing fast before any build if they diverge. - secrets.rs derives the keychain service namespace from the app bundle identifier (app.config().identifier) instead of a second hardcoded 'dev.loopwright.desktop' copy, so the keychain namespace tracks the app identity automatically when it is finalized for production. Signing/notarization, Windows signing, the updater, and confirming the bundle identifier's domain remain team-owned (documented in release.yml). --- .github/workflows/release.yml | 115 +++++++++++++++++++++++++++++-- desktop/src-tauri/src/secrets.rs | 16 +++-- 2 files changed, 122 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f70af1f..d3c786d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,9 +3,12 @@ name: Release # Publishes desktop installers to a GitHub Release. Triggered by pushing a # version tag (e.g. v0.1.0); `workflow_dispatch` allows a manual draft build. # -# Code signing / notarization and updater signing are wired through the env -# block below and activate automatically once the corresponding repository -# secrets are configured — until then the workflow still builds and uploads +# A `gate` job runs the full quality suite (typecheck, tests, audits, cargo +# test) and, for tag builds, verifies the tag matches every manifest version +# BEFORE anything is published — so a bad tagged commit cannot produce a +# release. Code signing / notarization and updater signing are wired through +# the publish job's env block and activate automatically once the corresponding +# repository secrets are configured; until then the workflow still uploads # unsigned installers as a draft release for QA. See the notes at the bottom. on: push: @@ -20,8 +23,111 @@ concurrency: cancel-in-progress: false jobs: + # Quality gate: must pass before any artifact is built or published. + gate: + name: gate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + # Fail fast (before the expensive toolchain/build steps) if a tag does not + # match the versions committed to every manifest. + - name: Verify tag matches manifest versions + if: github.ref_type == 'tag' + run: | + set -euo pipefail + tag="${GITHUB_REF_NAME#v}" + if ! printf '%s' "$tag" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::tag '${GITHUB_REF_NAME}' is not a vX.Y.Z release tag" + exit 1 + fi + root=$(node -p "require('./package.json').version") + desk=$(node -p "require('./desktop/package.json').version") + conf=$(node -p "require('./desktop/src-tauri/tauri.conf.json').version") + cargo=$(grep -m1 -E '^version *= *"' desktop/src-tauri/Cargo.toml | sed -E 's/^version *= *"([^"]+)".*/\1/') + fail=0 + for pair in "root package.json:${root}" "desktop/package.json:${desk}" "tauri.conf.json:${conf}" "Cargo.toml:${cargo}"; do + name="${pair%%:*}"; ver="${pair##*:}" + if [ "${ver}" != "${tag}" ]; then + echo "::error::${name} version '${ver}' does not match tag '${tag}'" + fail=1 + fi + done + if [ "${fail}" -ne 0 ]; then + echo "Bump every manifest to ${tag} before tagging the release." + exit 1 + fi + echo "All manifests match ${tag}." + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.2.14 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: desktop/src-tauri -> target + + - name: Install Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev \ + libssl-dev \ + libsecret-1-dev \ + patchelf \ + file \ + build-essential + + - name: Install root dependencies + run: npm ci + + - name: Install desktop dependencies + run: npm ci + working-directory: desktop + + - name: Type check + run: npm run typecheck + + - name: Test + run: npm test + + - name: Audit root dependencies + run: npm audit --audit-level=moderate + + - name: Audit desktop dependencies + run: npm audit --audit-level=high + working-directory: desktop + + - name: Build engine sidecar + run: npm run build:sidecar + working-directory: desktop + + - name: Build desktop frontend + run: npm run build + working-directory: desktop + + - name: Cargo test + run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --locked + release: name: release (${{ matrix.os }}) + needs: gate runs-on: ${{ matrix.os }} strategy: # Build every platform even if one fails, so a single platform issue does @@ -117,6 +223,7 @@ jobs: # to sign + notarize. Windows: configure a code-signing cert in tauri.conf # (bundle.windows.certificateThumbprint or a signCommand) + secret. # - Confirm the bundle identifier (currently dev.loopwright.desktop) maps to a -# domain the org controls before first public release. +# domain the org controls before first public release. The keychain service +# namespace follows the identifier automatically (see secrets.rs). # - Auto-update: add the tauri updater plugin + endpoints and a signing key # (TAURI_SIGNING_PRIVATE_KEY) to ship update artifacts. diff --git a/desktop/src-tauri/src/secrets.rs b/desktop/src-tauri/src/secrets.rs index 6220e89..8fcb50b 100644 --- a/desktop/src-tauri/src/secrets.rs +++ b/desktop/src-tauri/src/secrets.rs @@ -14,8 +14,13 @@ use std::path::PathBuf; use keyring::Entry; use tauri::{AppHandle, Manager}; -/// Keychain service namespace for all Loopwright secrets. -const SERVICE: &str = "dev.loopwright.desktop"; +/// Keychain service namespace for all Loopwright secrets, derived from the app +/// bundle identifier so it always matches the app's identity (and stays correct +/// if the identifier changes for a production release) rather than being a +/// second hardcoded copy of the namespace. +fn service(app: &AppHandle) -> String { + app.config().identifier.clone() +} fn index_path(app: &AppHandle) -> Result { let dir = app.path().app_config_dir().map_err(|e| e.to_string())?; @@ -71,7 +76,7 @@ pub fn list_keys(app: &AppHandle) -> Result, String> { /// Stores (or replaces) a secret in the keychain and records its name. pub fn set(app: &AppHandle, key: &str, value: &str) -> Result<(), String> { validate_key(key)?; - let entry = Entry::new(SERVICE, key).map_err(|e| e.to_string())?; + let entry = Entry::new(&service(app), key).map_err(|e| e.to_string())?; entry.set_password(value).map_err(|e| e.to_string())?; let mut keys = read_index(app)?; if !keys.iter().any(|k| k == key) { @@ -83,7 +88,7 @@ pub fn set(app: &AppHandle, key: &str, value: &str) -> Result<(), String> { /// Removes a secret from the keychain and the index. Idempotent. pub fn delete(app: &AppHandle, key: &str) -> Result<(), String> { - if let Ok(entry) = Entry::new(SERVICE, key) { + if let Ok(entry) = Entry::new(&service(app), key) { // Ignore "no such entry" so repeated deletes don't error. let _ = entry.delete_credential(); } @@ -96,8 +101,9 @@ pub fn delete(app: &AppHandle, key: &str) -> Result<(), String> { /// Names whose keychain value has gone missing are silently skipped. pub fn all(app: &AppHandle) -> Result, String> { let mut out = Vec::new(); + let svc = service(app); for key in read_index(app)? { - if let Ok(entry) = Entry::new(SERVICE, &key) { + if let Ok(entry) = Entry::new(&svc, &key) { if let Ok(value) = entry.get_password() { out.push((key, value)); } From 4958d1d74bd21023529c64b3cb8932ba8422d08f Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 22:31:27 +0000 Subject: [PATCH 18/19] Address review: serialize engine lifecycle; harden release workflow - engine.rs: guard url/token/child as one Lifecycle behind a single mutex held across start/restart/shutdown (via *_locked helpers). Three separate mutexes let a concurrent restart_engine, or a restart racing app exit, interleave - spawning a second sidecar, overwriting the child handle, or clearing the URL/token of a freshly-started process. One lock makes the lifecycle operations mutually exclusive. - release.yml: manual (workflow_dispatch) dev builds are now prerelease:true (ref_type != 'tag'); real version tags stay non-prerelease, so publishing a dev draft can't be treated as a normal release. - release.yml: run the gate across ubuntu/macos/windows so direct tag pushes exercise the JS + Rust suites on every release platform. Tag/version match, dependency audits, and Linux deps stay Linux-only. --- .github/workflows/release.yml | 29 ++++++++--- desktop/src-tauri/src/engine.rs | 87 +++++++++++++++++++++++---------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3c786d..f7c25ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,10 +23,17 @@ concurrency: cancel-in-progress: false jobs: - # Quality gate: must pass before any artifact is built or published. + # Quality gate: must pass before any artifact is built or published. Runs on + # every release platform so a direct tag push exercises the JS + Rust test + # suites on macOS/Windows too, not just Linux. One-time, platform-independent + # checks (tag/version match, dependency audits) run on Linux only. gate: - name: gate - runs-on: ubuntu-latest + name: gate (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4 @@ -37,9 +44,11 @@ jobs: node-version: 22 # Fail fast (before the expensive toolchain/build steps) if a tag does not - # match the versions committed to every manifest. + # match the versions committed to every manifest. Platform-independent, so + # run it once on Linux. - name: Verify tag matches manifest versions - if: github.ref_type == 'tag' + if: matrix.os == 'ubuntu-latest' && github.ref_type == 'tag' + shell: bash run: | set -euo pipefail tag="${GITHUB_REF_NAME#v}" @@ -79,6 +88,7 @@ jobs: workspaces: desktop/src-tauri -> target - name: Install Linux system dependencies + if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y \ @@ -107,10 +117,13 @@ jobs: - name: Test run: npm test + # Dependency audits are platform-independent; run them once on Linux. - name: Audit root dependencies + if: matrix.os == 'ubuntu-latest' run: npm audit --audit-level=moderate - name: Audit desktop dependencies + if: matrix.os == 'ubuntu-latest' run: npm audit --audit-level=high working-directory: desktop @@ -214,7 +227,11 @@ jobs: releaseName: Loopwright ${{ github.ref_type == 'tag' && github.ref_name || format('dev build {0}', github.run_number) }} # Publish as a draft so a human reviews artifacts before release. releaseDraft: true - prerelease: false + # Manual (workflow_dispatch) builds produce a throwaway dev tag, so + # mark them prerelease — otherwise publishing the draft would let + # GitHub treat a dev build as a normal release. Real version tags stay + # non-prerelease. + prerelease: ${{ github.ref_type != 'tag' }} args: ${{ matrix.args }} # Production distribution checklist (requires team-owned credentials/decisions): diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 9d2e20e..994331b 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -30,33 +30,76 @@ const STARTUP_TIMEOUT: Duration = Duration::from_secs(20); pub struct EngineManager { app: AppHandle, - url: Mutex>, - token: Mutex>, - child: Mutex>, + /// All mutable sidecar state lives behind one lock so the lifecycle calls + /// (start / restart / shutdown) are serialized end-to-end. Guarding url, + /// token, and the child handle separately let a concurrent `restart_engine` + /// — or a restart racing app exit — interleave: spawning a second sidecar, + /// overwriting the stored child handle, or clearing the URL/token of a + /// freshly-started process. A single mutex held across each operation makes + /// them mutually exclusive. + lifecycle: Mutex, +} + +/// The mutable engine state, guarded as one unit by `EngineManager::lifecycle`. +struct Lifecycle { + /// The engine base URL once started (e.g. "http://127.0.0.1:53187"). + url: Option, + /// The per-process bearer token the engine requires on its API. + token: Option, + /// Handle to the spawned sidecar, used to force-kill as a last resort. + child: Option, } impl EngineManager { pub fn new(app: AppHandle) -> Self { Self { app, - url: Mutex::new(None), - token: Mutex::new(None), - child: Mutex::new(None), + lifecycle: Mutex::new(Lifecycle { + url: None, + token: None, + child: None, + }), } } /// The engine base URL once started (e.g. "http://127.0.0.1:53187"). pub fn url(&self) -> Option { - self.url.lock().unwrap().clone() + self.lifecycle.lock().unwrap().url.clone() } /// The per-process bearer token the engine requires on its API. pub fn token(&self) -> Option { - self.token.lock().unwrap().clone() + self.lifecycle.lock().unwrap().token.clone() } /// Spawns the sidecar and blocks until it reports its listening port. + /// Holds the lifecycle lock for the whole operation so it cannot race + /// another start/restart/shutdown. pub fn start(&self) -> Result { + let mut state = self.lifecycle.lock().unwrap(); + self.start_locked(&mut state) + } + + /// Kills the running sidecar (if any) and starts a fresh one. Used after + /// secrets change so the new values are picked up. The shutdown and the + /// subsequent start run under a single held lock, so a second restart (or + /// app exit) can't slip in between and spawn an extra sidecar. + pub fn restart(&self) -> Result { + let mut state = self.lifecycle.lock().unwrap(); + self.shutdown_locked(&mut state); + self.start_locked(&mut state) + } + + /// Stops the running sidecar gracefully (see `shutdown_locked`). Holds the + /// lifecycle lock so it serializes with any in-flight start/restart. + pub fn shutdown(&self) { + let mut state = self.lifecycle.lock().unwrap(); + self.shutdown_locked(&mut state); + } + + /// Spawns the sidecar and records its URL/token/child into `state`. The + /// caller must hold the lifecycle lock. + fn start_locked(&self, state: &mut Lifecycle) -> Result { let data_dir = self.app.path().app_data_dir().map_err(|e| e.to_string())?; std::fs::create_dir_all(&data_dir).map_err(|e| e.to_string())?; let db_path = data_dir.join("sessions.json"); @@ -96,27 +139,21 @@ impl EngineManager { // engine during a long run. tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); - *self.url.lock().unwrap() = Some(ready.url.clone()); - *self.token.lock().unwrap() = ready.token; - *self.child.lock().unwrap() = Some(child); + state.url = Some(ready.url.clone()); + state.token = ready.token; + state.child = Some(child); Ok(ready.url) } - /// Kills the running sidecar (if any) and starts a fresh one. Used after - /// secrets change so the new values are picked up. - pub fn restart(&self) -> Result { - self.shutdown(); - self.start() - } - /// Stops the running sidecar gracefully: asks the engine to shut itself /// down over HTTP first — so it can cancel in-flight runs and kill their /// detached subprocess trees — and only then force-kills as a fallback. - /// A direct `child.kill()` would orphan those detached descendants. - pub fn shutdown(&self) { - let child = self.child.lock().unwrap().take(); - let url = self.url.lock().unwrap().clone(); - let token = self.token.lock().unwrap().clone(); + /// A direct `child.kill()` would orphan those detached descendants. The + /// caller must hold the lifecycle lock. + fn shutdown_locked(&self, state: &mut Lifecycle) { + let child = state.child.take(); + let url = state.url.clone(); + let token = state.token.clone(); if let Some(url) = url.as_deref() { request_shutdown(url, token.as_deref()); @@ -131,8 +168,8 @@ impl EngineManager { let _ = child.kill(); } - *self.url.lock().unwrap() = None; - *self.token.lock().unwrap() = None; + state.url = None; + state.token = None; } } From ee10a496a339e56e57b3c70f47eca892636cdd1a Mon Sep 17 00:00:00 2001 From: Inzimam Date: Sat, 13 Jun 2026 22:53:40 +0000 Subject: [PATCH 19/19] Address review: least-privilege release workflow; guard engine restart - release.yml: default the workflow to permissions: contents: read so a compromised dependency install/test step in the gate has no repo write authority. Grant contents: write only to the publish (release) job, and set persist-credentials: false on both checkouts so the GITHUB_TOKEN isn't left in .git/config for npm ci / test steps to read. tauri-action publishes via the GITHUB_TOKEN env var, not persisted git creds. - Guard the Secrets-view 'Restart engine' action against silently killing active runs (backend restart = shutdown + start, which aborts in-flight work). The engine now reports a process-scoped activeRuns count on /api/health; the desktop UI checks it and, when runs are active, shows an inline confirmation naming how many will be cancelled before restarting. Using the in-memory count (not stale store status) avoids false warnings after a crash. typecheck + 154 tests pass (root); desktop tsc + vite build clean. --- .github/workflows/release.yml | 19 ++++++++++++- desktop/src/api.ts | 15 +++++++++++ desktop/src/main.ts | 51 +++++++++++++++++++++++++++++++++-- desktop/src/styles.css | 4 +++ src/server/server.ts | 6 +++-- test/server.test.ts | 2 +- 6 files changed, 91 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7c25ee..d64a26f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,11 @@ on: tags: ["v*"] workflow_dispatch: +# Least privilege: default the whole workflow to read-only so a compromised +# dependency install script or test step in the gate cannot mutate the repo. +# Only the publish job is granted contents: write (to create the Release). permissions: - contents: write + contents: read concurrency: group: release-${{ github.ref }} @@ -29,6 +32,9 @@ jobs: # checks (tag/version match, dependency audits) run on Linux only. gate: name: gate (${{ matrix.os }}) + # Read-only: this job runs npm ci / tests and must never need write scope. + permissions: + contents: read strategy: fail-fast: false matrix: @@ -37,6 +43,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + # Don't leave the GITHUB_TOKEN in .git/config where the subsequent + # `npm ci` / test steps (and their dependency scripts) could read it. + persist-credentials: false - name: Set up Node.js 22 uses: actions/setup-node@v4 @@ -141,6 +151,11 @@ jobs: release: name: release (${{ matrix.os }}) needs: gate + # The only job that needs write scope — it creates/uploads the GitHub + # Release. tauri-action authenticates via the GITHUB_TOKEN env var below, + # not persisted git credentials, so the checkout stays credential-free. + permissions: + contents: write runs-on: ${{ matrix.os }} strategy: # Build every platform even if one fails, so a single platform issue does @@ -160,6 +175,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Node.js 22 uses: actions/setup-node@v4 diff --git a/desktop/src/api.ts b/desktop/src/api.ts index 771fada..619e19a 100644 --- a/desktop/src/api.ts +++ b/desktop/src/api.ts @@ -117,6 +117,21 @@ export async function health(): Promise { } } +/** + * Number of runs currently executing inside the engine process. Used to warn + * before a restart (which re-spawns the sidecar and aborts in-flight runs). + * Returns 0 if the engine can't be reached — the caller treats "unknown" as + * "nothing to lose" so a transient blip never blocks applying new secrets. + */ +export async function activeRunCount(): Promise { + try { + const { activeRuns } = await getJson<{ ok: boolean; activeRuns?: number }>("/api/health"); + return typeof activeRuns === "number" && activeRuns > 0 ? activeRuns : 0; + } catch { + return 0; + } +} + /** * Subscribes to a run's live event stream. Returns a function that closes it. * Uses the native EventSource, which transparently reconnects and replays via diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 29d04b1..fb3c97a 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -1,6 +1,7 @@ import "./styles.css"; import { apiBase, + activeRunCount, cancelRun, deleteSecret, getTrace, @@ -517,7 +518,14 @@ async function renderSecrets(): Promise { card.append(form, formError); const restart = h("button", {}, ["Restart engine to apply secret changes"]); - restart.addEventListener("click", async () => { + // A restart re-spawns the sidecar (backend restart = shutdown + start), which + // ABORTS any in-flight runs. Guard it: if runs are active, require an explicit + // confirmation naming how many will be cancelled rather than silently killing + // them. The confirm prompt lives inline (Tauri intercepts window.confirm). + const confirmPanel = h("div", { class: "confirm-restart", hidden: "true" }); + + async function performRestart(): Promise { + confirmPanel.hidden = true; restart.textContent = "Restarting…"; restart.setAttribute("disabled", "true"); try { @@ -531,8 +539,47 @@ async function renderSecrets(): Promise { restart.textContent = "Restart engine to apply secret changes"; restart.removeAttribute("disabled"); } + } + + function askRestartConfirm(active: number): void { + confirmPanel.innerHTML = ""; + const runWord = active === 1 ? "run is" : "runs are"; + const itWord = active === 1 ? "it" : "them"; + const proceed = h("button", { class: "danger" }, [ + `Cancel ${active} run${active === 1 ? "" : "s"} & restart`, + ]); + proceed.addEventListener("click", () => void performRestart()); + const keep = h("button", {}, ["Keep runs going"]); + keep.addEventListener("click", () => { + confirmPanel.hidden = true; + }); + confirmPanel.append( + h("div", { class: "warn" }, [ + `${active} ${runWord} still active. Restarting will abort ${itWord}.`, + ]), + h("div", { class: "actions" }, [proceed, keep]), + ); + confirmPanel.hidden = false; + } + + restart.addEventListener("click", async () => { + formError.hidden = true; + confirmPanel.hidden = true; + restart.setAttribute("disabled", "true"); + let active = 0; + try { + active = await activeRunCount(); + } catch { + /* unknown — fall through and restart (treat as nothing to lose) */ + } + restart.removeAttribute("disabled"); + if (active > 0) { + askRestartConfirm(active); + return; + } + await performRestart(); }); - card.append(h("div", { class: "actions" }, [restart, pending])); + card.append(h("div", { class: "actions" }, [restart, pending]), confirmPanel); await refresh(); } diff --git a/desktop/src/styles.css b/desktop/src/styles.css index fbde23b..e2c4bbf 100644 --- a/desktop/src/styles.css +++ b/desktop/src/styles.css @@ -96,6 +96,10 @@ button.small { font-size: 12px; padding: 3px 8px; } .hint.error, .error { color: var(--red); } .hint.pending { color: var(--yellow); } +/* Inline confirmation shown before a restart that would abort active runs. */ +.confirm-restart { margin-top: 10px; } +.confirm-restart .warn { color: var(--yellow); font-size: 13px; margin-bottom: 6px; } + .goal { font-size: 15px; margin-bottom: 4px; } .session-id { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-bottom: 10px; } diff --git a/src/server/server.ts b/src/server/server.ts index e1b7dcc..23c05d7 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -254,9 +254,11 @@ export function createServer(opts: CreateServerOptions): LoopwrightServer { } // Health is intentionally unauthenticated (no sensitive data) so the UI can - // show connectivity before it has resolved the token. + // show connectivity before it has resolved the token. `activeRuns` lets the + // desktop shell warn before a restart that would abort in-flight runs; it's + // a process-scoped count, not session data, so it's safe to expose here. if (pathname === "/api/health") { - return send(res, 200, { ok: true }, cors); + return send(res, 200, { ok: true, activeRuns }, cors); } // Everything else under /api requires the token. This is the boundary that diff --git a/test/server.test.ts b/test/server.test.ts index 7d537e5..99e2dbe 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -160,7 +160,7 @@ describe("server: health + validation", () => { it("reports health without a token", async () => { const res = await fetch(`${base}/api/health`); expect(res.status).toBe(200); - expect(await res.json()).toEqual({ ok: true }); + expect(await res.json()).toEqual({ ok: true, activeRuns: 0 }); }); it("rejects a run with no goal", async () => {