diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72c84da..744ab9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,3 +42,124 @@ 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 + # 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. 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: 1.2.14 + + - 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. 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 + + # 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..d64a26f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,263 @@ +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. +# +# 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: + 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: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + # 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 (${{ matrix.os }}) + # Read-only: this job runs npm ci / tests and must never need write scope. + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + 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 + with: + node-version: 22 + + # Fail fast (before the expensive toolchain/build steps) if a tag does not + # match the versions committed to every manifest. Platform-independent, so + # run it once on Linux. + - name: Verify tag matches manifest versions + if: matrix.os == 'ubuntu-latest' && github.ref_type == 'tag' + shell: bash + 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 + 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 + + - 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 + + # 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 + + - 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 + # 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 + # 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 + with: + persist-credentials: false + + - 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 + # 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): +# - 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. 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/.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..3b761da 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 (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 +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/app-icon.png b/desktop/app-icon.png new file mode 100644 index 0000000..e999869 Binary files /dev/null and b/desktop/app-icon.png differ 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..0175e93 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,1118 @@ +{ + "name": "loopwright-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "loopwright-desktop", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "typescript": "^5.6.3", + "vite": "^8.0.16" + } + }, + "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/@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, + "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/@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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "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/@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": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "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/@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/@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, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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, + "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", + "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/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", + "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/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", + "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/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": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@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": { + "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/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", + "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": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..3df47b4 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,22 @@ +{ + "name": "loopwright-desktop", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Tauri desktop shell for the Loopwright actor-critic engine", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "build:sidecar": "node ../scripts/build-sidecar.mjs", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "typescript": "^5.6.3", + "vite": "^8.0.16" + } +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..c70e063 --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4881 @@ +# 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.1.0" +dependencies = [ + "keyring", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-shell", + "tokio", +] + +[[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..b433f27 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "loopwright-desktop" +version = "0.1.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"] } +# 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 +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 0000000..6ba45a6 Binary files /dev/null and b/desktop/src-tauri/icons/128x128.png differ diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..0b66e62 Binary files /dev/null and b/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000..8770b70 Binary files /dev/null and b/desktop/src-tauri/icons/32x32.png differ diff --git a/desktop/src-tauri/icons/64x64.png b/desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000..7a8d871 Binary files /dev/null and b/desktop/src-tauri/icons/64x64.png differ diff --git a/desktop/src-tauri/icons/Square107x107Logo.png b/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..4cca5d9 Binary files /dev/null and b/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/desktop/src-tauri/icons/Square142x142Logo.png b/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..592ed54 Binary files /dev/null and b/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/desktop/src-tauri/icons/Square150x150Logo.png b/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..c238870 Binary files /dev/null and b/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/desktop/src-tauri/icons/Square284x284Logo.png b/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..3233db5 Binary files /dev/null and b/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/desktop/src-tauri/icons/Square30x30Logo.png b/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..79902bd Binary files /dev/null and b/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/desktop/src-tauri/icons/Square310x310Logo.png b/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..3bd82dd Binary files /dev/null and b/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/desktop/src-tauri/icons/Square44x44Logo.png b/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..57731fc Binary files /dev/null and b/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/desktop/src-tauri/icons/Square71x71Logo.png b/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..c3522be Binary files /dev/null and b/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/desktop/src-tauri/icons/Square89x89Logo.png b/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..d582296 Binary files /dev/null and b/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/desktop/src-tauri/icons/StoreLogo.png b/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..fd6a082 Binary files /dev/null and b/desktop/src-tauri/icons/StoreLogo.png differ 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 0000000..4aefe4d Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4985219 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ 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 0000000..50ba939 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..7c3f19a Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..cd7d10f Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..e7271f5 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..67e48d3 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ 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 0000000..0023ecb Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ 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 0000000..2f71f00 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..43c27ab Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ 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 0000000..c02a16c Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..1864a05 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..2c6735b Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ 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 0000000..a187253 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..ff757b9 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ 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 0000000..c0300d2 Binary files /dev/null and b/desktop/src-tauri/icons/icon.icns differ diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000..c8eb88d Binary files /dev/null and b/desktop/src-tauri/icons/icon.ico differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000..b93f909 Binary files /dev/null and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..aa9ed3d Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..5d34410 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ 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 0000000..5d34410 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png differ 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 0000000..f15d005 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png differ 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 0000000..121df93 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png differ 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 0000000..63efc4c Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..63efc4c Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..dbd002f Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..5d34410 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png differ 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 0000000..385c778 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ 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 0000000..385c778 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png differ 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 0000000..a296b5b Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png differ 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 0000000..29eb8a0 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-512@2x.png differ 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 0000000..a296b5b Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png differ 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 0000000..2698b24 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png differ 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 0000000..234a900 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..a09b3b4 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..ab10b70 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs new file mode 100644 index 0000000..994331b --- /dev/null +++ b/desktop/src-tauri/src/engine.rs @@ -0,0 +1,426 @@ +//! Engine sidecar lifecycle (Task 25.2). +//! +//! The headless engine ships as a compiled Node binary (see +//! `scripts/build-sidecar.mjs`) and runs as a Tauri sidecar. This module spawns +//! it on a loopback ephemeral port, reads the single JSON readiness line it +//! prints to discover that port, and exposes the resulting base URL to the +//! frontend. Stored secrets are injected into the sidecar's environment so the +//! engine's runner profiles can reference API keys by env-var name without any +//! 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}; + +use tauri::async_runtime::Receiver; +use tauri::{AppHandle, Manager}; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; + +use crate::secrets; + +/// Sidecar program name; must match `bundle.externalBin` in tauri.conf.json +/// and the scoped name in capabilities/default.json. +const SIDECAR: &str = "binaries/loopwright-engine"; + +/// How long to wait for the engine to announce its port before giving up. +const STARTUP_TIMEOUT: Duration = Duration::from_secs(20); + +pub struct EngineManager { + app: AppHandle, + /// 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, + 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.lifecycle.lock().unwrap().url.clone() + } + + /// The per-process bearer token the engine requires on its API. + pub fn token(&self) -> Option { + 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"); + + 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 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. + let _ = child.kill(); + return Err(e); + } + }; + + // 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() {} }); + + state.url = Some(ready.url.clone()); + state.token = ready.token; + state.child = Some(child); + Ok(ready.url) + } + + /// 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. 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()); + // 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. + let _ = child.kill(); + } + + state.url = None; + state.token = None; + } +} + +/// 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); +} + +/// 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, + 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(); + + 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(ready) = parse_ready(&buf) { + return Ok(ready); + } + } + 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_ready(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())?; + let token = v.get("token").and_then(|x| x.as_str()).map(str::to_string); + return Some(Ready { + url: format!("http://{host}:{port}"), + token, + }); + } + } + } + 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 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. + 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(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(3), + "should return on connection refused, not wait out the timeout (elapsed {:?})", + start.elapsed() + ); + } + + /// 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/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..70133e7 --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,85 @@ +//! 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()) +} + +/// 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 { + 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() { + 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 + // 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, + engine_token, + restart_engine, + set_secret, + delete_secret, + list_secret_keys + ]) + .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/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..8fcb50b --- /dev/null +++ b/desktop/src-tauri/src/secrets.rs @@ -0,0 +1,113 @@ +//! 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, 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())?; + 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()), + // 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()) +} + +/// 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> { + validate_key(key)?; + 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) { + 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(app), 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(); + let svc = service(app); + for key in read_index(app)? { + if let Ok(entry) = Entry::new(&svc, &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..635117b --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,43 @@ +{ + "productName": "Loopwright", + "version": "0.1.0", + "identifier": "dev.loopwright.desktop", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1420", + "beforeDevCommand": "npm run build:sidecar && npm run dev", + "beforeBuildCommand": "npm run build:sidecar && 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", + "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", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/desktop/src/api.ts b/desktop/src/api.ts new file mode 100644 index 0000000..619e19a --- /dev/null +++ b/desktop/src/api.ts @@ -0,0 +1,179 @@ +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; +let cachedToken: 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()) { + // 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 = ""; + } + 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, { headers: await authHeaders() }); + 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: await authHeaders({ "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; +} + +/** 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; +} + +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; + } +} + +/** + * 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 + * 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> { + // 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) => { + 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..fb3c97a --- /dev/null +++ b/desktop/src/main.ts @@ -0,0 +1,607 @@ +import "./styles.css"; +import { + apiBase, + activeRunCount, + cancelRun, + 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]); +} + +/** 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"; +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); + + // 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; + 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" || 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 + // 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); + } else if (msg.data.phase === "error") { + phase.className = "phase error"; + phase.textContent = `error: ${msg.data.error}`; + } + } + } + + const closePromise = openStream(sessionId, onMessage, () => { + /* EventSource auto-reconnects with Last-Event-ID; nothing to do here */ + }); + // 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 ----------------------------------------------------------- + +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); + } + + // 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, ...blocking, 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); + + // 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(); + 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(); + markPending(); + }); + 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(); + formError.hidden = true; + const data = new FormData(form); + 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, formError); + + const restart = h("button", {}, ["Restart engine to apply secret changes"]); + // 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 { + 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"); + } + } + + 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]), confirmPanel); + + 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..e2c4bbf --- /dev/null +++ b/desktop/src/styles.css @@ -0,0 +1,157 @@ +: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); } +.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; } + +.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); } + +.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; } +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-lock.json b/package-lock.json index 0da5a2c..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" }, @@ -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 bf673fa..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", @@ -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" @@ -22,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/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/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; + 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 e677552..9d0ad25 100644 --- a/src/engine/mechanicalGate.ts +++ b/src/engine/mechanicalGate.ts @@ -24,6 +24,7 @@ export interface CommandOutcome { export type CommandExecutor = ( command: string, cwd: string, + signal?: AbortSignal, ) => Promise; const DEFAULT_TIMEOUT_MS = 5 * 60_000; @@ -50,12 +51,21 @@ 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; + } + // Spawn detached on POSIX so the timeout/cancellation handlers can kill + // the entire process group, not just the immediate shell. See + // killProcessTree / detachForTreeKill. const child = spawn(command, { cwd, shell: true, detached: detachForTreeKill }); let output = ""; let timedOut = false; + let cancelled = false; const append = (buf: Buffer) => { output += buf.toString(); @@ -67,10 +77,23 @@ export function createShellExecutor( killProcessTree(child); }, timeoutMs); + // Cancellation: kill the whole process tree so a long verify/build + // command (and any descendants it spawned, e.g. `npm test`) stops + // promptly when the run is cancelled. + const onAbort = (): void => { + cancelled = true; + killProcessTree(child); + }; + 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}`, @@ -78,12 +101,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, }); }); @@ -96,6 +121,8 @@ export const defaultExecutor: CommandExecutor = createShellExecutor(); export interface MechanicalGateOptions { cwd: string; executor?: CommandExecutor; + /** cancels in-flight commands when aborted */ + signal?: AbortSignal; } /** @@ -118,7 +145,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 2a6992c..862e561 100644 --- a/src/observability/events.ts +++ b/src/observability/events.ts @@ -37,6 +37,9 @@ export const EVENT_TYPES = { planReviewed: "plan_reviewed", runnerCall: "runner_call", sessionFinished: "session_finished", + integration: "integration", + sessionFailed: "session_failed", + sessionInterrupted: "session_interrupted", } as const; function num(v: unknown): number { diff --git a/src/runners/agentRunner.ts b/src/runners/agentRunner.ts index 9b8a96c..adf658f 100644 --- a/src/runners/agentRunner.ts +++ b/src/runners/agentRunner.ts @@ -49,6 +49,12 @@ export interface RunRequest { cwd: string; /** optional system/role framing */ system?: string; + /** + * Cooperative cancellation. When aborted, an HTTP backend aborts its in-flight + * fetch and a CLI backend kills its subprocess tree, so clicking Stop during a + * long model call returns promptly instead of waiting for the runner timeout. + */ + signal?: AbortSignal; } export interface RunResult { diff --git a/src/runners/cliRunner.ts b/src/runners/cliRunner.ts index 3c48561..099be7a 100644 --- a/src/runners/cliRunner.ts +++ b/src/runners/cliRunner.ts @@ -188,6 +188,18 @@ export class CliRunner implements AgentRunner { const { command, promptVia, timeoutMs, maxCapturedChars } = this.opts; return new Promise((resolve) => { 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/server/hub.ts b/src/server/hub.ts new file mode 100644 index 0000000..76495d4 --- /dev/null +++ b/src/server/hub.ts @@ -0,0 +1,159 @@ +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 { + /** Monotonic per-session ordinal, 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; + /** 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"; + +/** 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) { + 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", 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); + } + + 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.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; + } + 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. + * + * 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.channels.get(sessionId); + if (!ch) return () => {}; + 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. 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) return; + if (generation !== undefined && ch.generation !== generation) return; + 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..493db86 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,97 @@ +/** + * 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,"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) + * 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 + * 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 { reconcileInterruptedSessions } from "../session.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(); + 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; + 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, + // When a graceful shutdown finishes (signal or POST /api/shutdown), exit. + onShutdown: () => process.exit(0), + ...(staticDir ? { staticDir } : {}), + }); + + 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. + 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); + 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..23c05d7 --- /dev/null +++ b/src/server/server.ts @@ -0,0 +1,634 @@ +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 { 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"; +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). + * + * 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. */ +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; + /** + * 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; + /** 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; + /** + * 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 { + /** 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; +} + +const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" }; + +function send(res: ServerResponse, status: number, body: unknown, cors: Record = {}): void { + const payload = JSON.stringify(body); + res.writeHead(status, { ...JSON_HEADERS, ...cors }); + res.end(payload); +} + +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]); + +/** True for hosts that only accept connections from the local machine. */ +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; + 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": origin, + vary: "Origin", + "access-control-allow-methods": "GET,POST,OPTIONS", + "access-control-allow-headers": "authorization,content-type,last-event-id", + "access-control-max-age": "600", + }; +} + +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")); +} + +/** 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", + ".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 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(); + /** + * 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; + + /** + * Constant-time bearer-token check. The token is read from the Authorization + * header; the `token` query param is accepted ONLY where `allowQuery` is set + * (the SSE stream, since EventSource can't send headers) so tokens don't leak + * into URLs/logs for ordinary requests. + */ + function authorized(req: IncomingMessage, url: URL, allowQuery: boolean): boolean { + const auth = req.headers.authorization; + let provided: string | undefined; + if (typeof auth === "string" && auth.startsWith("Bearer ")) provided = auth.slice(7); + else if (allowQuery) 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) }); + 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; + const cors = corsHeadersFor(req); + + if (method === "OPTIONS") { + 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. `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, activeRuns }, 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). Only the SSE + // stream may carry the token as a query param (EventSource can't set + // headers); all other routes require the Authorization header. + const isStream = method === "GET" && /^\/api\/runs\/[^/]+\/stream$/.test(pathname); + if (pathname.startsWith("/api/")) { + if (!authorized(req, url, isStream)) return send(res, 401, { error: "unauthorized" }, cors); + } + + if (pathname === "/api/runs" && method === "POST") { + 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), 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); + } + + // 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)); + 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}"` }, 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" }, cors); + } + + if (staticDir) return serveStatic(res, pathname); + return send(res, 404, { error: "not found" }, cors); + } + + 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; + } catch (err) { + 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" }, cors); + + // 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)}` }, cors); + } + 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); + } + // Admission control: cap concurrent background runs so repeated clicks, a + // buggy UI, or a leaked token can't kick off unbounded expensive work. + if (activeRuns >= 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 + // 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)); + + 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. + 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). The + // 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 (including cancellation) surface as a terminal + // 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, + observer, + log: (line) => void hub.publish(sessionId, "log", { line }), + signal: controller.signal, + }) + .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); + }) + .finally(() => { + settle(); + activeRunPromises.delete(run); + }); + activeRunPromises.add(run); + + send(res, 202, { sessionId }, cors); + } + + 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}"` }, cors); + } + + res.writeHead(200, { + "content-type": "text/event-stream; charset=utf-8", + "cache-control": "no-cache, no-transform", + connection: "keep-alive", + "x-accel-buffering": "no", + ...cors, + }); + + // 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, + ); + + // 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); + + const close = (): void => { + clearInterval(heartbeat); + unsubscribe(); + sseClients.delete(res); + }; + req.on("close", close); + res.on("close", close); + } + + 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. + const root = path.resolve(dir); + const rel = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, ""); + 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" }); + } + const file = await resolveFile(resolved, root); + 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 + // 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; + // 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; + } + + const ext = path.extname(file).toLowerCase(); + res.writeHead(200, { + "content-type": STATIC_CONTENT_TYPES[ext] ?? "application/octet-stream", + }); + 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; + } + + /** + * Graceful teardown: stop in-flight work and close connections so the + * 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. 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 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(); + } 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(); + }, remaining()); + if (typeof timer.unref === "function") timer.unref(); + }); + })(); + return closing; + } + + return { + http, + hub, + token, + 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 closeServer(); + }, + }; +} 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/src/session.ts b/src/session.ts index 6262f3d..960c9df 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"; @@ -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 ?? "."; @@ -118,129 +121,135 @@ 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, + 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 } : {}), + ...(signal ? { signal } : {}), }); - await store.recordEvent({ - sessionId, - at: new Date().toISOString(), - type: EVENT_TYPES.planReviewed, - data: { approved: plan.approved, revisions: plan.revisions, openItems: plan.openItems.length }, - }); - } - // 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 } : {}), + ...(signal ? { signal } : {}), }; - } 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.updateSession(sessionId, { - status: needsHuman.length > 0 ? "needs_human" : "completed", + // 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 } + : {}), }); - 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 (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,25 +263,117 @@ export async function runGoal( branches: greenBranches, ...(verifyCommands.length ? { verifyCommands } : {}), ...(executor ? { executor } : {}), + ...(signal ? { signal } : {}), ...(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, + }, + }); + } + 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 } : {}), + }; + } 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 */ + } + } + } } +} + +/** + * 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"; +} - return { - goal, - ...(sessionId ? { sessionId } : {}), - plan, - results, - green, - unverified, - needsHuman, - skipped, - allVerified: green.length === plan.plan.tasks.length, - ...(integration ? { integration } : {}), - }; +/** + * 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. */ 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/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; } 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/mechanicalGate.test.ts b/test/mechanicalGate.test.ts index 8fac62f..3276370 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 new file mode 100644 index 0000000..99e2dbe --- /dev/null +++ b/test/server.test.ts @@ -0,0 +1,504 @@ +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"; +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). 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 { + 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: {}, 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(); +}); + +/** 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; +} + +/** 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()); + + 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, activeRuns: 0 }); + }); + + it("rejects a run with no goal", async () => { + const res = await fetch(`${base}/api/runs`, { + method: "POST", + 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`, { 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`, { headers: auth() })).status).toBe(404); + }); +}); + +describe("server: auth boundary", () => { + beforeEach(() => startServer()); + + 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: "do it" }), + }); + 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(); + }); + + 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: 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 = () => {}; + 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(); + // 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()); + + 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`, { headers: auth() }); + 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 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"); + expect(trace.trace.usage.total.calls).toBe(1); + expect(typeof trace.text).toBe("string"); + + const list = (await (await fetch(`${base}/api/sessions`, { headers: auth() })).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: {}, + token: TOKEN, + runGoalImpl: async () => { + throw new Error("boom"); + }, + }); + base = `http://127.0.0.1:${await server.start(0)}`; + + 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"); + }); +}); + +describe("server: live SSE stream", () => { + beforeEach(() => startServer()); + + it("replays buffered messages then signals done, in order with monotonic ids", async () => { + 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); + 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 startRun("resume me"); + + // Let the run finish so the whole buffer exists. + 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: 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); + // 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); + }); +}); + +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); + }); +}); diff --git a/test/session.test.ts b/test/session.test.ts index cb4e1af..9bd8209 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -1,9 +1,10 @@ import { describe, it, expect } from "vitest"; -import { runGoal, openBlockers } 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"; 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,4 +117,60 @@ 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", () => { + 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"); + }); +}); + +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); + }); }); 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");