diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44caea3..50c4838 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 - name: Install web dependencies working-directory: web @@ -82,6 +84,8 @@ jobs: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 - name: Install dependencies working-directory: web @@ -98,3 +102,34 @@ jobs: - name: Prettier working-directory: web run: bun run prettier --check "src/**/*.{ts,css}" + + desktop: + name: Desktop (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install desktop dependencies + working-directory: desktop + run: bun install --frozen-lockfile + + - name: Validate desktop app + working-directory: desktop + run: bun run check + + - name: Smoke-test desktop packaging (Linux only) + if: matrix.os == 'ubuntu-latest' + working-directory: desktop + run: bun run pack diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..13fc3fa --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +name: Dependency Review + +on: + pull_request: + branches: [main, development] + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Review dependency changes + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6a6aec3..ae9b2cd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,6 +29,8 @@ jobs: fetch-depth: 0 - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 - name: Install dependencies working-directory: docs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c2cc08..b6fb623 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,6 +91,8 @@ jobs: key: ${{ matrix.target }} - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 - name: Build web UI run: | @@ -132,7 +134,7 @@ jobs: # Build checksums for all artifacts checksums: name: Generate checksums - needs: build-and-upload + needs: [build-and-upload, desktop] runs-on: ubuntu-latest steps: - name: Download all release assets @@ -142,10 +144,23 @@ jobs: fileName: "checkai-*" out-file-path: artifacts + - name: Download updater manifests + uses: robinraju/release-downloader@v1 + with: + tag: ${{ github.ref_name }} + fileName: "latest*.yml" + out-file-path: artifacts + - name: Generate SHA256 checksums run: | cd artifacts - sha256sum checkai-* > checksums-sha256.txt + shopt -s nullglob + files=(checkai-* latest*.yml) + if [ ${#files[@]} -eq 0 ]; then + echo "No release artifacts found for checksum generation." >&2 + exit 1 + fi + sha256sum "${files[@]}" > checksums-sha256.txt cat checksums-sha256.txt - name: Upload checksums @@ -214,6 +229,66 @@ jobs: bun publish --ignore-scripts rm -f .npmrc + desktop: + name: Desktop ${{ matrix.label }} + needs: create-release + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - label: Linux + os: ubuntu-latest + build_command: bun x electron-builder --config electron-builder.yml --publish never --linux AppImage deb + artifact_glob: | + desktop/release/checkai-desktop-*.AppImage + desktop/release/checkai-desktop-*.deb + desktop/release/latest-linux.yml + - label: macOS + os: macos-latest + build_command: bun x electron-builder --config electron-builder.yml --publish never --mac zip dmg + artifact_glob: | + desktop/release/checkai-desktop-*.dmg + desktop/release/checkai-desktop-*.zip + desktop/release/checkai-desktop-*.zip.blockmap + desktop/release/latest-mac.yml + - label: Windows + os: windows-latest + build_command: bun x electron-builder --config electron-builder.yml --publish never --win nsis msi + artifact_glob: | + desktop/release/checkai-desktop-*.exe + desktop/release/checkai-desktop-*.exe.blockmap + desktop/release/checkai-desktop-*.msi + desktop/release/latest.yml + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install desktop dependencies + working-directory: desktop + run: bun install --frozen-lockfile + + - name: Build desktop app + working-directory: desktop + run: bun run build + + - name: Build desktop release artifact + working-directory: desktop + run: ${{ matrix.build_command }} + + - name: Upload desktop release asset + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + files: ${{ matrix.artifact_glob }} + # Build and push Docker image to GHCR docker: name: Docker image diff --git a/.gitignore b/.gitignore index d7b60ef..c458ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ */node_modules */dist +*/dist-electron +*/release diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d7f31..de9cca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.7.0] - 2026-05-13 + +### Added + +- **Engine test coverage** — Added perft suites for the standard starting position (depths 1–3, with depth-4 guarded by `#[ignore]`) and the Kiwipete benchmark (depths 1–2, depth-3 ignored); new mate-in-one verification through the full search; transposition-table reuse test across consecutive iterative-deepening runs +- **Evaluation test coverage** — Added colour-mirror symmetry tests (starting position and asymmetric material imbalance), tapered-evaluation phase verification (full midgame phase at startpos, pure endgame phase in K+P vs K), and bishop-pair bonus delta test +- **REST API documentation for the archive** — Added an "Archive & Storage" section to `docs/api/rest.md` covering `GET /api/archive`, `GET /api/archive/stats`, `GET /api/archive/{game_id}`, and `GET /api/archive/{game_id}/replay`, including request/response shapes and error codes +- **Desktop packaging smoke test in CI** — The desktop CI job now runs `bun run pack` on Ubuntu to validate the full electron-builder pipeline end-to-end on every push + +### Changed + +- **Version metadata** — Bumped Rust crate, WASM crate, npm package, web UI, desktop app, and VitePress version label to 0.7.0 +- **Released previously unreleased 0.6.0 desktop work** — Promoted the prior `[Unreleased]` section to a proper `[0.6.0] - 2026-03-09` entry + +## [0.6.0] - 2026-03-09 + +### Added + +- **Electron desktop app** — Added a dedicated Svelte-based Electron renderer alongside the web UI + - Includes persistent desktop sessions, native file/folder pickers, local backend launch controls, inline logs, and a multi-view workspace shell + - Packaged desktop builds can check GitHub Releases for updates, download them, and install on restart +- **Native desktop installers** — Release automation now publishes platform-native Electron installers in addition to updater-compatible artifacts + - Linux releases include `.deb` alongside AppImage + - macOS releases include `.dmg` alongside updater-compatible `.zip` + - Windows releases include `.msi` alongside NSIS for in-app update compatibility +- **Desktop CI and release automation** — GitHub Actions now validate the Electron app on Ubuntu, macOS, and Windows and publish desktop release assets with dependency review coverage + +### Changed + +- **Version metadata** — Updated project/package version references, install snippets, OpenAPI metadata, and documentation to align with the 0.6.0 desktop release ## [0.5.2] - 2026-03-07 @@ -225,7 +254,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Game archiving with zstd compression - Web UI for browser-based game viewing -[Unreleased]: https://github.com/JosunLP/checkai/compare/v0.5.2...HEAD +[Unreleased]: https://github.com/JosunLP/checkai/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/JosunLP/checkai/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/JosunLP/checkai/compare/v0.5.2...v0.6.0 [0.5.2]: https://github.com/JosunLP/checkai/compare/v0.5.1...v0.5.2 [0.5.1]: https://github.com/JosunLP/checkai/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/JosunLP/checkai/compare/v0.4.0...v0.5.0 diff --git a/Cargo.lock b/Cargo.lock index 86597cc..b7c06e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "actix-macros", "actix-rt", "actix_derive", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "crossbeam-channel", "futures-core", @@ -33,7 +33,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-sink", @@ -70,7 +70,7 @@ dependencies = [ "actix-service", "actix-utils", "base64", - "bitflags 2.11.0", + "bitflags 2.11.1", "brotli", "bytes", "bytestring", @@ -207,7 +207,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.2", + "socket2 0.6.3", "time", "tracing", "url", @@ -286,9 +286,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -301,15 +301,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -351,9 +351,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -366,9 +366,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "base62" -version = "2.2.3" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111" +checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b" [[package]] name = "base64" @@ -384,9 +384,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -451,18 +451,18 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] [[package]] name = "cc" -version = "1.2.56" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -495,7 +495,7 @@ dependencies = [ [[package]] name = "checkai" -version = "0.5.2" +version = "0.7.0" dependencies = [ "actix", "actix-cors", @@ -521,9 +521,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -531,9 +531,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -543,9 +543,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -555,15 +555,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -736,9 +736,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid", @@ -773,9 +773,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -783,9 +783,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -928,20 +928,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", @@ -1007,9 +1007,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1075,18 +1075,18 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1097,7 +1097,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1105,15 +1104,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1137,7 +1135,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1145,12 +1143,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1158,9 +1157,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1171,9 +1170,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1185,15 +1184,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1205,15 +1204,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1243,9 +1242,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1275,31 +1274,21 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1318,15 +1307,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.21" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -1337,9 +1326,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.21" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -1358,10 +1347,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.89" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4eacb0641a310445a4c513f2a5e23e19952e269c6a38887254d5f837a305506" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1386,15 +1377,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-channel" @@ -1468,9 +1459,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -1480,24 +1471,24 @@ dependencies = [ [[package]] name = "normpath" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" +checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -1505,9 +1496,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1517,9 +1508,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1558,21 +1549,15 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -1582,18 +1567,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1625,11 +1610,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -1678,7 +1663,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -1694,7 +1679,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -1715,16 +1700,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.52.0", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1735,6 +1720,12 @@ 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 = "rand" version = "0.9.4" @@ -1752,7 +1743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.1", ] @@ -1787,7 +1778,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -1821,9 +1812,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -1969,9 +1960,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -1984,9 +1975,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring", @@ -1998,9 +1989,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2046,9 +2037,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2135,7 +2126,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -2167,15 +2158,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -2201,12 +2192,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2350,9 +2341,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2360,9 +2351,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2375,9 +2366,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -2385,16 +2376,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -2447,9 +2438,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -2465,28 +2456,28 @@ dependencies = [ "serde_spanned", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.2", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.2", ] [[package]] @@ -2512,20 +2503,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-util", "http 1.4.0", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2591,9 +2582,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" @@ -2609,9 +2600,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -2700,11 +2691,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -2743,11 +2734,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -2756,14 +2747,14 @@ 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", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.112" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d7d0fce354c88b7982aec4400b3e7fcf723c32737cef571bd165f7613557ee" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -2774,23 +2765,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.62" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee85afca410ac4abba5b584b12e77ea225db6ee5471d0aebaae0861166f9378a" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.112" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55839b71ba921e4f75b674cb16f843f4b1f3b26ddfcb3454de1cf65cc021ec0f" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2798,9 +2785,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.112" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf2e969c2d60ff52e7e98b7392ff1588bffdd1ccd4769eba27222fd3d621571" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -2811,9 +2798,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.112" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0861f0dcdf46ea819407495634953cdcc8a8c7215ab799a7a7ce366be71c7b30" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -2846,7 +2833,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -2854,9 +2841,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.89" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10053fbf9a374174094915bbce141e87a6bf32ecd9a002980db4b638405e8962" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -2874,9 +2861,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -3063,9 +3050,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -3079,6 +3075,12 @@ 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" @@ -3128,7 +3130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -3160,15 +3162,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3177,9 +3179,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3189,18 +3191,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -3209,18 +3211,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3236,9 +3238,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3247,9 +3249,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3258,9 +3260,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c9de4ab..61ecabb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "checkai" -version = "0.5.2" +version = "0.7.0" edition = "2024" description = "A chess server and CLI that lets AI agents play chess against each other via REST API" license = "MIT" diff --git a/README.md b/README.md index 857f20c..3d875d4 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ A Rust-powered chess server and CLI with REST, WebSocket, and deep analysis APIs ### Web & Deployment - **Modern Web UI** — TypeScript SPA with @bquery/bquery, Tailwind CSS v4, Vite — interactive SVG board, analysis panel, FEN/PGN tools, promotion dialog, WebSocket auto-reconnect. Compiled into the binary via `rust-embed` +- **Desktop UI** — Electron app built with Svelte — dedicated desktop shell with persistent sessions, local backend launch controls, dashboard/game/analysis/archive views, inline log inspection, and desktop-focused settings - **Docker Support** — Multi-stage Dockerfile and docker-compose.yml with volume mounts for game data, opening books, and tablebases - **Internationalization** — 8 languages (EN, DE, FR, ES, ZH, JA, PT, RU) with auto-detection and per-request API selection - **Self-Update** — Automatic version checks and `checkai update` for in-place binary updates @@ -47,26 +48,79 @@ A Rust-powered chess server and CLI with REST, WebSocket, and deep analysis APIs ### Install -**Linux / macOS:** +Recommended: pin the release you want and verify the downloaded binary against the +published SHA-256 checksums before installing it. The examples below use +`v0.7.0`; check the [Releases](https://github.com/JosunLP/checkai/releases) +page and replace it with the current or desired release tag. ```bash -VERSION="0.5.2" -curl -fsSL -o install.sh \ - "https://raw.githubusercontent.com/JosunLP/checkai/v${VERSION}/scripts/install.sh" -sh install.sh +# Linux / macOS +CHECKAI_VERSION=v0.7.0 +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +[ "$OS" = "darwin" ] || OS="linux" +ARCH="$(uname -m)" +case "$ARCH" in x86_64|amd64) ARCH=x86_64 ;; arm64|aarch64) ARCH=aarch64 ;; esac +ASSET="checkai-${OS}-${ARCH}" +BASE_URL="https://github.com/JosunLP/checkai/releases/download/${CHECKAI_VERSION}" + +curl -fSLO "${BASE_URL}/${ASSET}" +curl -fSLO "${BASE_URL}/checksums-sha256.txt" +CHECKSUM_LINE="$(grep " ${ASSET}$" checksums-sha256.txt)" || { + echo "Error: Asset ${ASSET} not found in checksums-sha256.txt" >&2 + exit 1 +} +if command -v sha256sum >/dev/null 2>&1; then + echo "${CHECKSUM_LINE}" | sha256sum -c - +elif command -v shasum >/dev/null 2>&1; then + echo "${CHECKSUM_LINE}" | shasum -a 256 -c - +else + echo "Error: Neither sha256sum nor shasum found. On Linux, install coreutils; on macOS, shasum should be pre-installed." >&2 + exit 1 +fi +chmod +x "${ASSET}" +sudo install -m 0755 "${ASSET}" /usr/local/bin/checkai ``` -**Windows (PowerShell):** +```powershell +# Windows (PowerShell) +$Version = "v0.7.0" +$Asset = "checkai-windows-x86_64.exe" +$BaseUrl = "https://github.com/JosunLP/checkai/releases/download/$Version" +Invoke-WebRequest "$BaseUrl/$Asset" -OutFile $Asset +Invoke-WebRequest "$BaseUrl/checksums-sha256.txt" -OutFile checksums-sha256.txt +$Expected = ((Select-String .\checksums-sha256.txt -Pattern " $([regex]::Escape($Asset))$").Line -split "\s+")[0].ToLowerInvariant() +$Actual = (Get-FileHash ".\$Asset" -Algorithm SHA256).Hash.ToLowerInvariant() +if ($Actual -ne $Expected) { throw "Checksum verification failed for $Asset" } +New-Item -ItemType Directory "$env:LOCALAPPDATA\checkai" -Force | Out-Null +Move-Item -Force ".\$Asset" "$env:LOCALAPPDATA\checkai\checkai.exe" +``` + +For the shortest install path, you can pipe the installer script directly to your +shell. This executes the current `main` branch script immediately, so only use it +if you accept that trade-off: + +```bash +curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | sh +``` + +```powershell +irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | iex +``` + +### Uninstall + +```bash +# Linux / macOS +curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.sh | sh +``` ```powershell -$Version = "0.5.2" -Invoke-WebRequest ` - -Uri "https://raw.githubusercontent.com/JosunLP/checkai/v$Version/scripts/install.ps1" ` - -OutFile install.ps1 -.\install.ps1 +# Windows (PowerShell) +irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.sh | iex ``` -> **Tip:** For production use, download and verify the script before running it. +> **Tip:** The installer script automatically detects the operating system, architecture, and latest release, while the uninstaller script detects the operating system — no manual changes required. The direct installer shortcut is quick, but it executes a remote script before you can verify the release asset yourself. +> Prefer the pinned release commands above when you want release integrity checks before installation. > See the [Getting Started guide](https://josunlp.github.io/checkai/guide/getting-started) for details. ### Build from Source @@ -82,6 +136,19 @@ cd web && bun install && bun run build && cd .. cargo build --release ``` +### Desktop App + +The repository now also includes a dedicated Electron desktop shell in `desktop/`. + +```bash +cd desktop +bun install --frozen-lockfile +bun run build +bun run start +``` + +By default the desktop app targets `http://127.0.0.1:8080`, can persist backend launch settings between sessions, and can start a local `checkai serve` process for you. The embedded live workspace is intentionally limited to loopback URLs for safety; non-local targets can still be opened in your browser. Packaged desktop releases can also check GitHub Releases for updates, download them, and prompt for restart-based installation from inside the app. Release automation now publishes updater-compatible artifacts (AppImage/zip/NSIS) together with native installer packages per platform (`.deb`, `.dmg`, `.msi`). To keep Windows desktop updates working, release builds continue to ship the updater-compatible NSIS package alongside the MSI installer. + ### Start the Server ```bash @@ -279,10 +346,8 @@ checkai/ │ ├── release.yml # Release (binaries + Docker image) │ └── docs.yml # Documentation → GitHub Pages ├── scripts/ -│ ├── install.sh # Installer (Linux / macOS) -│ ├── install.ps1 # Installer (Windows) -│ ├── uninstall.sh # Uninstaller (Linux / macOS) -│ └── uninstall.ps1 # Uninstaller (Windows) +│ ├── install.sh # Installer (Linux / macOS / Windows) +│ └── uninstall.sh # Uninstaller (Linux / macOS / Windows) ├── docs/ # VitePress documentation site ├── locales/ # i18n YAML files (8 languages) ├── wasm/ # WebAssembly crate (wasm-pack) @@ -295,6 +360,17 @@ checkai/ │ ├── bin/checkai.mjs # Node.js CLI entry point │ ├── src/index.mjs # Library API exports │ └── README.md # package documentation +├── desktop/ # Electron desktop UI (Svelte renderer + native shell) +│ ├── bun.lock # Bun lockfile for desktop workspace +│ ├── package.json # Desktop build + packaging scripts +│ ├── index.html # Renderer entry point +│ └── src/ +│ ├── shared-types.ts # Shared IPC contract (main, preload, renderer) +│ ├── main.ts # Svelte renderer bootstrap +│ ├── App.svelte # Root desktop UI component +│ ├── electron-main.ts +│ ├── preload.ts +│ └── styles.scss # Desktop-specific styles ├── web/ # TypeScript Web UI (bQuery + Tailwind + Vite) │ ├── src/ # 12 TypeScript source modules │ ├── dist/ # Vite production build (embedded into binary) diff --git a/desktop/bun.lock b/desktop/bun.lock new file mode 100644 index 0000000..094c3e5 --- /dev/null +++ b/desktop/bun.lock @@ -0,0 +1,1019 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@checkai/desktop", + "dependencies": { + "electron-updater": "^6.6.2", + }, + "devDependencies": { + "@bquery/bquery": "^1.4.0", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/vite": "^4.2.1", + "@types/node": "^24.3.0", + "electron": "^36.0.0", + "electron-builder": "^26.0.12", + "sass": "^1.98.0", + "svelte": "^5.53.10", + "svelte-check": "^4.4.5", + "tailwindcss": "^4.2.1", + "typescript": "^5.9.3", + "vite": "^7.3.1", + }, + }, + }, + "packages": { + "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + + "@bquery/bquery": ["@bquery/bquery@1.4.0", "", {}, "sha512-a0/w70oI3lAdLUXFNrZhXFgVbTnB+YEwxlV3hpbfL1ckSgBogzmPaEayjo+h7JHQL2y1n5iianyzkLCBgJSqfA=="], + + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], + + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], + + "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], + + "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], + + "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], + + "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], + + "@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], + + "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], + + "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], + + "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + + "@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], + + "@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + + "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + + "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + + "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], + + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], + + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + + "abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], + + "acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], + + "app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="], + + "builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], + + "cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], + + "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], + + "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], + + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + + "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], + + "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + + "devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="], + + "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], + + "dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="], + + "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": "bin/dmg-license.js" }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": "bin/cli.js" }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + + "electron": ["electron@36.9.5", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": "cli.js" }, "sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA=="], + + "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], + + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="], + + "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], + + "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], + + "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": "bin/esbuild" }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": "cli.js" }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + + "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": "bin/cli.js" }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + + "jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], + + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], + + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="], + + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], + + "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": "bin/cmd.js" }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-abi": ["node-abi@4.27.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-M6PypnnjGwr6Wv1O7twPpnJiie8MeLoBSh99zF1jnRRU2NLgxO/fUEQW1rfAhAKzPa/fvQ4PKitt1O+c8l243Q=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], + + "node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": "bin/node-gyp.js" }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], + + "nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": "bin/nopt.js" }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + + "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": "dist/cli.js" }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], + + "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": "cli.js" }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + + "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], + + "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="], + + "sass": ["sass@1.98.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": "sass.js" }, "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A=="], + + "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + + "semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + + "slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + + "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], + + "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "svelte": ["svelte@5.53.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-GYmqRjRhJYLQBonfdfGAt28gkfWEShrtXKGXcFGneXi502aBE+I1dJcs/YQriByvP6xqXRz/OdBGC6tfvUQHyQ=="], + + "svelte-check": ["svelte-check@4.4.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": "bin/svelte-check" }, "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw=="], + + "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tar": ["tar@7.5.11", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ=="], + + "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], + + "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + + "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], + + "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + + "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + + "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + + "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], + + "unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["less", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" } }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + + "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@electron/get/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + + "@electron/universal/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + + "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], + + "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "electron/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + + "lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + + "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + } +} diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml new file mode 100644 index 0000000..2a3d8f8 --- /dev/null +++ b/desktop/electron-builder.yml @@ -0,0 +1,26 @@ +appId: io.github.josunlp.checkai.desktop +productName: CheckAI Desktop +artifactName: checkai-desktop-${version}-${os}-${arch}.${ext} +publish: + provider: github + owner: JosunLP + repo: checkai + releaseType: release +directories: + output: release +files: + - dist/**/* + - dist-electron/**/* +mac: + category: public.app-category.developer-tools + target: + - zip + - dmg +linux: + target: + - AppImage + - deb +win: + target: + - nsis + - msi diff --git a/desktop/index.html b/desktop/index.html new file mode 100644 index 0000000..ebcc4cf --- /dev/null +++ b/desktop/index.html @@ -0,0 +1,16 @@ + + + + + + + CheckAI Desktop + + +
+ + + diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..e9c1b32 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,41 @@ +{ + "name": "@checkai/desktop", + "version": "0.7.0", + "private": true, + "type": "module", + "main": "dist-electron/electron-main.js", + "packageManager": "bun@1.3.10", + "description": "Dedicated Electron desktop app for CheckAI", + "author": "JosunLP <20913954+JosunLP@users.noreply.github.com>", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/JosunLP/checkai.git" + }, + "scripts": { + "build": "bun run build:renderer && bun run build:electron", + "build:renderer": "vite build", + "build:electron": "tsc -p tsconfig.electron.json", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.electron.json --noEmit && vite build", + "start": "bun run build && bun x electron .", + "pack": "bun run build && bun x electron-builder --config electron-builder.yml --dir --publish never", + "dist": "bun run build && bun x electron-builder --config electron-builder.yml --publish never" + }, + "dependencies": { + "electron-updater": "^6.6.2" + }, + "devDependencies": { + "@bquery/bquery": "^1.4.0", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/vite": "^4.2.1", + "@types/node": "^24.3.0", + "electron": "^36.0.0", + "electron-builder": "^26.0.12", + "sass": "^1.98.0", + "svelte": "^5.53.10", + "svelte-check": "^4.4.5", + "tailwindcss": "^4.2.1", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } +} diff --git a/desktop/src/App.svelte b/desktop/src/App.svelte new file mode 100644 index 0000000..0cb179d --- /dev/null +++ b/desktop/src/App.svelte @@ -0,0 +1,136 @@ + + +
+ + +
+ {#if $toastMsg} + + {/if} + + {#if $errorMsg} + + {/if} + + + +
+ {#if $currentView === 'dashboard'} + + {:else if $currentView === 'games'} + + {:else if $currentView === 'board'} + + {:else if $currentView === 'archive'} + + {:else if $currentView === 'analysis'} + + {:else if $currentView === 'engine'} + + {:else if $currentView === 'logs'} + + {:else if $currentView === 'settings'} + + {/if} +
+
+
+ +{#if $paletteOpen} + +{/if} + +{#if $modalState} + +{/if} diff --git a/desktop/src/accessibility.ts b/desktop/src/accessibility.ts new file mode 100644 index 0000000..52609c0 --- /dev/null +++ b/desktop/src/accessibility.ts @@ -0,0 +1,57 @@ +const FOCUSABLE_SELECTOR = [ + 'button:not([disabled])', + 'a[href]', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + '[tabindex]:not([tabindex="-1"])', +].join(', '); + +function isFocusable(element: HTMLElement): boolean { + const style = window.getComputedStyle(element); + return ( + element.getAttribute('aria-hidden') !== 'true' && + style.visibility !== 'hidden' && + style.display !== 'none' && + element.offsetWidth > 0 && + element.offsetHeight > 0 && + element.getClientRects().length > 0 + ); +} + +export function trapTabKey(event: KeyboardEvent, container: HTMLElement | null): void { + if (!container) { + return; + } + + const focusable = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter( + isFocusable + ); + + if (focusable.length === 0) { + event.preventDefault(); + if (!container.hasAttribute('tabindex')) { + container.tabIndex = -1; + } + container.focus(); + return; + } + + const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + const focusIsInside = activeElement ? container.contains(activeElement) : false; + + if (event.shiftKey) { + if (!focusIsInside || activeElement === first) { + event.preventDefault(); + last.focus(); + } + return; + } + + if (!focusIsInside || activeElement === last) { + event.preventDefault(); + first.focus(); + } +} diff --git a/desktop/src/api-client.ts b/desktop/src/api-client.ts new file mode 100644 index 0000000..437ea64 --- /dev/null +++ b/desktop/src/api-client.ts @@ -0,0 +1,174 @@ +// ============================================================================ +// CheckAI Desktop — Engine REST API Client +// ============================================================================ + +import type { + AnalysisJob, + ArchivedGameSummary, + Game, + GameSummary, + LegalMove, + MoveResponse, + ReplayState, + StorageStats, +} from './shared-types.js'; + +let apiBase = 'http://127.0.0.1:8080/api'; + +export function setApiBase(backendUrl: string): void { + const withoutTrailingSlashes = backendUrl.replace(/\/+$/, ''); + const withoutTrailingApi = withoutTrailingSlashes.replace(/\/api$/i, ''); + apiBase = `${withoutTrailingApi}/api`; +} + +async function request( + method: string, + path: string, + body?: unknown +): Promise { + const opts: RequestInit = { + method, + }; + if (body !== undefined) { + opts.headers = { 'Content-Type': 'application/json' }; + opts.body = JSON.stringify(body); + } + + const res = await fetch(`${apiBase}${path}`, opts); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error((err as Record).error || res.statusText); + } + const ct = res.headers.get('Content-Type') || ''; + if (ct.includes('application/json')) return res.json() as Promise; + const text = await res.text(); + try { + return JSON.parse(text) as T; + } catch { + return text as unknown as T; + } +} + +// ── Game CRUD ──────────────────────────────────────────────────────────────── + +export function createGame(): Promise<{ game_id: string }> { + return request('POST', '/games'); +} + +export function listGames(): Promise<{ games: GameSummary[]; total: number }> { + return request('GET', '/games'); +} + +export function getGame(id: string): Promise { + return request('GET', `/games/${encodeURIComponent(id)}`); +} + +export function deleteGame(id: string): Promise<{ message: string }> { + return request('DELETE', `/games/${encodeURIComponent(id)}`); +} + +// ── Moves & Actions ────────────────────────────────────────────────────────── + +export function submitMove( + id: string, + from: string, + to: string, + promotion?: string +): Promise { + return request('POST', `/games/${encodeURIComponent(id)}/move`, { + from, + to, + promotion, + }); +} + +export function submitAction( + id: string, + action: string, + reason?: string +): Promise { + return request('POST', `/games/${encodeURIComponent(id)}/action`, { + action, + reason, + }); +} + +export function getLegalMoves( + id: string +): Promise<{ moves: LegalMove[]; turn: string; count: number }> { + return request('GET', `/games/${encodeURIComponent(id)}/moves`); +} + +export function getBoardAscii(id: string): Promise { + return request('GET', `/games/${encodeURIComponent(id)}/board`); +} + +// ── Archive ────────────────────────────────────────────────────────────────── + +export function listArchived(): Promise<{ + games: ArchivedGameSummary[]; + total: number; + storage: StorageStats; +}> { + return request('GET', '/archive'); +} + +export function getArchived(id: string): Promise { + return request('GET', `/archive/${encodeURIComponent(id)}`); +} + +export function replayArchived( + id: string, + moveNum?: number +): Promise { + const q = moveNum !== undefined ? `?move_number=${moveNum}` : ''; + return request('GET', `/archive/${encodeURIComponent(id)}/replay${q}`); +} + +export function getStorageStats(): Promise { + return request('GET', '/archive/stats'); +} + +// ── Analysis ───────────────────────────────────────────────────────────────── + +export function startAnalysis( + gameId: string, + depth?: number +): Promise<{ job_id: string; message: string }> { + return request( + 'POST', + `/analysis/game/${encodeURIComponent(gameId)}`, + depth ? { depth } : {} + ); +} + +export function listAnalysisJobs(): Promise<{ + jobs: AnalysisJob[]; + count: number; +}> { + return request('GET', '/analysis/jobs'); +} + +export function getAnalysisJob(jobId: string): Promise { + return request('GET', `/analysis/jobs/${encodeURIComponent(jobId)}`); +} + +export function cancelAnalysisJob( + jobId: string +): Promise<{ message: string }> { + return request('DELETE', `/analysis/jobs/${encodeURIComponent(jobId)}`); +} + +// ── FEN / PGN ──────────────────────────────────────────────────────────────── + +export function exportFen(id: string): Promise<{ fen: string }> { + return request('GET', `/games/${encodeURIComponent(id)}/fen`); +} + +export function importFen(fen: string): Promise<{ game_id: string }> { + return request('POST', '/games/fen', { fen }); +} + +export function exportPgn(id: string): Promise { + return request('GET', `/games/${encodeURIComponent(id)}/pgn`); +} diff --git a/desktop/src/backend-url.ts b/desktop/src/backend-url.ts new file mode 100644 index 0000000..bbb7552 --- /dev/null +++ b/desktop/src/backend-url.ts @@ -0,0 +1,55 @@ +const BACKEND_URL_EXAMPLE = 'http://127.0.0.1:8080'; +const URL_WITH_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i; +const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']); +export const DEFAULT_BACKEND_PORT = '8080'; + +export function normalizeBackendUrl(value: string): string { + const trimmed = value.trim(); + if (!trimmed) { + throw new Error(`Enter a backend URL such as ${BACKEND_URL_EXAMPLE}.`); + } + + const candidate = URL_WITH_SCHEME_PATTERN.test(trimmed) ? trimmed : `http://${trimmed}`; + + let url: URL; + try { + url = new URL(candidate); + } catch { + throw new Error(`Enter a valid backend URL such as ${BACKEND_URL_EXAMPLE}.`); + } + + if (url.protocol !== 'http:') { + throw new Error(`Backend URL must start with http://, for example ${BACKEND_URL_EXAMPLE}.`); + } + + if (!url.hostname) { + throw new Error('Backend URL must include a hostname.'); + } + + if (!LOOPBACK_HOSTS.has(url.hostname.toLowerCase())) { + throw new Error( + 'Backend URL must use a local loopback host such as 127.0.0.1, localhost, or [::1].' + ); + } + + url.username = ''; + url.password = ''; + url.search = ''; + url.hash = ''; + if (!url.port) { + url.port = DEFAULT_BACKEND_PORT; + } + url.pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, ''); + + return `${url.origin}${url.pathname}`; +} + +export function normalizeBackendUrlOrFallback(value: unknown, fallback: string): string { + const candidate = typeof value === 'string' ? value : fallback; + + try { + return normalizeBackendUrl(candidate); + } catch { + return fallback; + } +} diff --git a/desktop/src/components/CommandPalette.svelte b/desktop/src/components/CommandPalette.svelte new file mode 100644 index 0000000..d3ba64f --- /dev/null +++ b/desktop/src/components/CommandPalette.svelte @@ -0,0 +1,208 @@ + + + + + diff --git a/desktop/src/components/ModalDialog.svelte b/desktop/src/components/ModalDialog.svelte new file mode 100644 index 0000000..400f14a --- /dev/null +++ b/desktop/src/components/ModalDialog.svelte @@ -0,0 +1,159 @@ + + + + +{#if $modalState} + +{/if} diff --git a/desktop/src/components/Sidebar.svelte b/desktop/src/components/Sidebar.svelte new file mode 100644 index 0000000..102eda8 --- /dev/null +++ b/desktop/src/components/Sidebar.svelte @@ -0,0 +1,166 @@ + + + diff --git a/desktop/src/components/Toast.svelte b/desktop/src/components/Toast.svelte new file mode 100644 index 0000000..1cea7b7 --- /dev/null +++ b/desktop/src/components/Toast.svelte @@ -0,0 +1,13 @@ + + +
+ {message} +
diff --git a/desktop/src/components/Topbar.svelte b/desktop/src/components/Topbar.svelte new file mode 100644 index 0000000..7753b09 --- /dev/null +++ b/desktop/src/components/Topbar.svelte @@ -0,0 +1,97 @@ + + +
+
+ CheckAI Desktop +

{title}

+
+ {subtitle} + + + {$updateStatus.state === 'available' + ? `Update ${$updateStatus.availableVersion ?? ''} available` + : $updateStatus.message ?? 'No pending desktop updates'} + + {#if runningAnalyses > 0} + + {runningAnalyses} analysis job(s) running + {/if} +
+
+ +
+ + + {#if $currentView === 'dashboard' || $currentView === 'games'} + + + {:else if $currentView === 'board' && $activeGame} + + + + {:else if $currentView === 'analysis'} + + + {:else} + + {/if} + + {#if $backendStatus.running} + + {:else} + + {/if} +
+
diff --git a/desktop/src/desktop-api.ts b/desktop/src/desktop-api.ts new file mode 100644 index 0000000..17e166b --- /dev/null +++ b/desktop/src/desktop-api.ts @@ -0,0 +1,172 @@ +import { get } from 'svelte/store'; +import { + desktopState, + currentView, + backendStatus, + backendLogs, + updateStatus, +} from './stores.js'; +import { pushError, pushToast } from './notifications.js'; +import { + DEFAULT_DESKTOP_STATE, + type DesktopApi, +} from './shared-types.js'; + +declare global { + interface Window { + checkaiDesktop?: DesktopApi; + } +} + +// ── Fallback API for running outside Electron ─────────────────────────────── + +const fallbackApi: DesktopApi = { + async getState() { + return { ...DEFAULT_DESKTOP_STATE }; + }, + async saveState(s) { + return s; + }, + async getBackendStatus() { + return { + running: false, + pid: null, + command: null, + startedAt: null, + exitCode: null, + lastError: 'Electron preload bridge unavailable.', + }; + }, + async getBackendLogs() { + return 'Open this build inside Electron to use native features.'; + }, + async startBackend() { + return fallbackApi.getBackendStatus(); + }, + async stopBackend() { + return fallbackApi.getBackendStatus(); + }, + async getUpdateStatus() { + return { + supported: false, + currentVersion: 'dev', + state: 'unsupported' as const, + availableVersion: null, + percent: null, + transferredBytes: null, + totalBytes: null, + message: 'Desktop updates are available in packaged builds.', + }; + }, + async setProgressBar() {}, + async checkForUpdates() { + return fallbackApi.getUpdateStatus(); + }, + async downloadUpdate() { + return fallbackApi.getUpdateStatus(); + }, + async installUpdate() {}, + async pickFile() { + return null; + }, + async pickDirectory() { + return null; + }, + async readTextFile() { + throw new Error('Local file reading is only available inside Electron.'); + }, + async saveTextFile() { + return null; + }, + async openPath() {}, + async openExternal() {}, + async notify() {}, + onBackendStatus() { + return () => undefined; + }, + onBackendLogs() { + return () => undefined; + }, + onUpdateStatus() { + return () => undefined; + }, + onMenuCommand() { + return () => undefined; + }, +}; + +export const desktop = window.checkaiDesktop ?? fallbackApi; + +// Helper functions +export async function loadDesktopState(): Promise { + try { + const state = await desktop.getState(); + desktopState.set(state); + currentView.set(state.lastView); + } catch (error) { + console.error('Failed to load desktop state:', error); + pushError('Failed to load desktop state.'); + } +} + +export async function saveDesktopState(): Promise { + try { + const state = get(desktopState); + const savedState = await desktop.saveState(state); + desktopState.set(savedState); + currentView.set(savedState.lastView); + } catch (error) { + console.error('Failed to save desktop state:', error); + pushError('Failed to save desktop state.'); + } +} + +export function initializeBackendListener(): () => void { + const unsubscribeStatus = desktop.onBackendStatus((status) => { + backendStatus.set(status); + }); + + const unsubscribeLogs = desktop.onBackendLogs((logs) => { + backendLogs.set(logs); + }); + + return () => { + unsubscribeStatus(); + unsubscribeLogs(); + }; +} + +export function initializeUpdateListener(): () => void { + const unsubscribeUpdate = desktop.onUpdateStatus((status) => { + updateStatus.set(status); + }); + + return () => { + unsubscribeUpdate(); + }; +} + +export async function startBackend(): Promise { + try { + const state = get(desktopState); + const status = await desktop.startBackend(state); + backendStatus.set(status); + if (status.running) { + pushToast('Backend started successfully'); + } + } catch (error) { + console.error('Failed to start backend:', error); + pushError('Failed to start backend.'); + } +} + +export async function stopBackend(): Promise { + try { + const status = await desktop.stopBackend(); + backendStatus.set(status); + pushToast('Backend stopped'); + } catch (error) { + console.error('Failed to stop backend:', error); + pushError('Failed to stop backend.'); + } +} diff --git a/desktop/src/electron-main.ts b/desktop/src/electron-main.ts new file mode 100644 index 0000000..53772c3 --- /dev/null +++ b/desktop/src/electron-main.ts @@ -0,0 +1,1206 @@ +import { + app, + BrowserWindow, + dialog, + ipcMain, + Menu, + Notification, + shell, + type MenuItemConstructorOptions, +} from 'electron'; +import { autoUpdater } from 'electron-updater'; +import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; +import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + DEFAULT_DESKTOP_STATE, + DESKTOP_VIEWS, + type BackendPreset, + type BackendStatusPayload, + type DesktopState, + type SaveTextFileOptions, + type UpdateStatusPayload, +} from './shared-types.js'; +import { DEFAULT_BACKEND_PORT, normalizeBackendUrlOrFallback } from './backend-url.js'; + +const MAX_LOG_LINES = 400; +const LOG_PUSH_DELAY_MS = 250; +const BACKEND_FORCE_KILL_TIMEOUT_MS = 10_000; +const DEFAULT_NOTIFICATION_TITLE = 'CheckAI Desktop'; +const MAX_READABLE_FILE_SELECTIONS = 32; +// Keep this in sync with CLI flags that consume the following argv entry as a value +// before the subcommand appears, e.g. `checkai --lang de serve`. +const CLI_FLAGS_WITH_SEPARATE_VALUE = new Set(['--lang', '-l']); + +let mainWindow: BrowserWindow | null = null; +let backendProcess: ChildProcessWithoutNullStreams | null = null; +let backendExitListener: + | ((code: number | null, signal: NodeJS.Signals | null) => void) + | null = null; +let backendStopRequested = false; +let backendLogs = ''; +let backendLogsFlushTimer: NodeJS.Timeout | null = null; +let backendForceKillTimer: NodeJS.Timeout | null = null; +const readableFileSelections = new Map(); +let backendStatus: BackendStatusPayload = { + running: false, + pid: null, + command: null, + startedAt: null, + exitCode: null, + lastError: null, +}; +let updateStatus: UpdateStatusPayload = { + supported: app.isPackaged, + currentVersion: app.getVersion(), + state: app.isPackaged ? 'idle' : 'unsupported', + availableVersion: null, + percent: null, + transferredBytes: null, + totalBytes: null, + message: app.isPackaged + ? 'Ready to check for desktop updates.' + : 'Desktop updates are available in packaged builds.', +}; + +function stateFilePath(): string { + return join(app.getPath('userData'), 'desktop-state.json'); +} + +function normalizeString(value: unknown, fallback = ''): string { + return typeof value === 'string' ? value : fallback; +} + +function normalizeTheme(value: unknown): 'dark' | 'light' | 'system' { + if (value === 'dark' || value === 'light' || value === 'system') return value; + return DEFAULT_DESKTOP_STATE.theme; +} + +function normalizePreset(value: unknown): BackendPreset | null { + const candidate = typeof value === 'object' && value !== null ? value : null; + if (!candidate) return null; + + const record = candidate as Record; + const id = normalizeString(record.id).trim(); + const name = normalizeString(record.name).trim(); + if (!id || !name) return null; + + return { + id, + name, + backendExecutable: normalizeString(record.backendExecutable), + backendArgs: normalizeString(record.backendArgs), + backendWorkingDirectory: normalizeString(record.backendWorkingDirectory), + backendUrl: normalizeBackendUrlOrFallback( + record.backendUrl, + DEFAULT_DESKTOP_STATE.backendUrl + ), + openingBookPath: normalizeString(record.openingBookPath), + tablebasePath: normalizeString(record.tablebasePath), + autoStartBackend: + typeof record.autoStartBackend === 'boolean' + ? record.autoStartBackend + : DEFAULT_DESKTOP_STATE.autoStartBackend, + createdAt: + typeof record.createdAt === 'number' && Number.isFinite(record.createdAt) + ? record.createdAt + : Date.now(), + }; +} + +function normalizeStringArray(value: unknown, limit = 8): string[] { + if (!Array.isArray(value)) return []; + const seen = new Set(); + const normalized: string[] = []; + + for (const entry of value) { + const str = normalizeString(entry).trim(); + if (!str || seen.has(str)) continue; + seen.add(str); + normalized.push(str); + if (normalized.length >= limit) break; + } + + return normalized; +} + +function normalizeDesktopState(value: unknown): DesktopState { + const candidate = typeof value === 'object' && value !== null ? value : {}; + const record = candidate as Record; + const lastView = normalizeString( + record.lastView, + DEFAULT_DESKTOP_STATE.lastView + ); + const normalizedLastView = + DESKTOP_VIEWS.find((view) => view === lastView) ?? + DEFAULT_DESKTOP_STATE.lastView; + + return { + backendUrl: normalizeBackendUrlOrFallback( + record.backendUrl, + DEFAULT_DESKTOP_STATE.backendUrl + ), + autoStartBackend: + typeof record.autoStartBackend === 'boolean' + ? record.autoStartBackend + : DEFAULT_DESKTOP_STATE.autoStartBackend, + backendExecutable: normalizeString( + record.backendExecutable, + DEFAULT_DESKTOP_STATE.backendExecutable + ), + backendArgs: normalizeString( + record.backendArgs, + DEFAULT_DESKTOP_STATE.backendArgs + ), + backendWorkingDirectory: normalizeString(record.backendWorkingDirectory), + openingBookPath: normalizeString(record.openingBookPath), + tablebasePath: normalizeString(record.tablebasePath), + lastView: normalizedLastView, + theme: normalizeTheme(record.theme), + boardFlipped: + typeof record.boardFlipped === 'boolean' + ? record.boardFlipped + : DEFAULT_DESKTOP_STATE.boardFlipped, + recentWorkspaces: normalizeStringArray(record.recentWorkspaces, 10), + backendPresets: Array.isArray(record.backendPresets) + ? record.backendPresets + .map((preset) => normalizePreset(preset)) + .filter((preset): preset is BackendPreset => preset !== null) + .slice(0, 20) + : DEFAULT_DESKTOP_STATE.backendPresets, + notificationsEnabled: + typeof record.notificationsEnabled === 'boolean' + ? record.notificationsEnabled + : DEFAULT_DESKTOP_STATE.notificationsEnabled, + compactMode: + typeof record.compactMode === 'boolean' + ? record.compactMode + : DEFAULT_DESKTOP_STATE.compactMode, + developerMode: + typeof record.developerMode === 'boolean' + ? record.developerMode + : DEFAULT_DESKTOP_STATE.developerMode, + lastGameId: + record.lastGameId === null + ? null + : normalizeString(record.lastGameId) || + DEFAULT_DESKTOP_STATE.lastGameId, + }; +} + +async function loadState(): Promise { + const file = stateFilePath(); + if (!existsSync(file)) { + return { ...DEFAULT_DESKTOP_STATE }; + } + + try { + return normalizeDesktopState( + JSON.parse(await readFile(file, 'utf8')) as Partial + ); + } catch (error) { + console.error('Failed to load desktop state:', error); + return { ...DEFAULT_DESKTOP_STATE }; + } +} + +async function saveState(next: unknown): Promise { + const sanitized = normalizeDesktopState(next); + const file = stateFilePath(); + try { + await mkdir(dirname(file), { recursive: true }); + await writeFile(file, JSON.stringify(sanitized, null, 2), 'utf8'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to save desktop state: ${message}`); + } + return sanitized; +} + +function pushBackendStatus(): void { + mainWindow?.webContents.send('checkai:backend-status', backendStatus); +} + +function flushBackendLogs(): void { + if (backendLogsFlushTimer) { + clearTimeout(backendLogsFlushTimer); + backendLogsFlushTimer = null; + } + mainWindow?.webContents.send('checkai:backend-logs', backendLogs); +} + +function scheduleBackendLogsPush(): void { + if (backendLogsFlushTimer) { + return; + } + + backendLogsFlushTimer = setTimeout(() => { + backendLogsFlushTimer = null; + mainWindow?.webContents.send('checkai:backend-logs', backendLogs); + }, LOG_PUSH_DELAY_MS); +} + +function appendBackendLogs(chunk: string): void { + const combined = `${backendLogs}${chunk}`; + const lines = combined.split(/\r?\n/); + backendLogs = lines.slice(-MAX_LOG_LINES).join('\n'); + scheduleBackendLogsPush(); +} + +function pushUpdateStatus(): void { + mainWindow?.webContents.send('checkai:update-status', updateStatus); +} + +function splitArgs(value: string): string[] { + const matches = value.match(/"[^"]*"|'[^']*'|\S+/g); + return (matches ?? []).map((part) => part.replace(/^['"]|['"]$/g, '')); +} + +function defaultBackendPort(): string { + return DEFAULT_BACKEND_PORT; +} + +function hasCliFlag(args: string[], flag: string): boolean { + return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`)); +} + +function withoutCliFlag(args: string[], flag: string): string[] { + const nextArgs: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === flag) { + if (index + 1 < args.length) { + index += 1; + } + continue; + } + + if (arg.startsWith(`${flag}=`)) { + continue; + } + + nextArgs.push(arg); + } + + return nextArgs; +} + +function findSubcommandIndex(args: string[]): number { + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (!arg.startsWith('-')) { + return index; + } + + if (CLI_FLAGS_WITH_SEPARATE_VALUE.has(arg) && index + 1 < args.length) { + // Skip the flag value so tokens like `--lang de serve` do not treat `de` + // as the subcommand before we reach the real `serve` token. + index += 1; + } + } + + return -1; +} + +function buildBackendArgs(state: DesktopState): string[] { + const args = splitArgs(state.backendArgs); + + let subcommand: string; + const subcommandIndex = findSubcommandIndex(args); + if (subcommandIndex === -1) { + subcommand = 'serve'; + args.push(subcommand); + } else { + subcommand = args[subcommandIndex]; + } + + if (subcommand === 'serve') { + if (state.openingBookPath && !hasCliFlag(args, '--book-path')) { + args.push('--book-path', state.openingBookPath); + } + if (state.tablebasePath && !hasCliFlag(args, '--tablebase-path')) { + args.push('--tablebase-path', state.tablebasePath); + } + + try { + const url = new URL(state.backendUrl); + const loopbackHost = url.hostname.replace(/^\[(.*)\]$/, '$1'); + const port = url.port || defaultBackendPort(); + if (port && /^\d+$/.test(port) && !hasCliFlag(args, '--port') && !hasCliFlag(args, '-p')) { + args.push('--port', port); + } + args.splice(0, args.length, ...withoutCliFlag(args, '--host')); + args.push('--host', loopbackHost); + } catch { + // Ignore invalid URLs here; persisted desktop state is normalized before use. + } + } + + return args; +} + +function clearBackendForceKillTimer(): void { + if (backendForceKillTimer) { + clearTimeout(backendForceKillTimer); + backendForceKillTimer = null; + } +} + +function notify(title: unknown, body: unknown): void { + const normalizedTitle = validateNotificationTitle(title); + const normalizedBody = validateNotificationBody(body); + if (!Notification.isSupported() || !normalizedBody) return; + new Notification({ title: normalizedTitle, body: normalizedBody }).show(); +} + +function validateNotificationTitle(value: unknown): string { + const normalized = normalizeString(value).trim(); + return normalized || DEFAULT_NOTIFICATION_TITLE; +} + +function validateNotificationBody(value: unknown): string { + return normalizeString(value).trim(); +} + +function getBackendExitError( + code: number | null, + signal: NodeJS.Signals | null, + stopRequested: boolean +): string | null { + if (signal === 'SIGTERM' || code === 0) { + return null; + } + + if (stopRequested) { + return `Backend exited with code ${code ?? -1} while stopping.`; + } + + return `Backend exited with code ${code ?? -1}.`; +} + +function validateOpenPathTarget(target: unknown): string { + const value = normalizeString(target).trim(); + if (!value) { + throw new Error('Select a local path first.'); + } + + if (hasNonFileScheme(value)) { + throw new Error( + 'Only local filesystem paths can be opened from the desktop shell.' + ); + } + + const resolvedPath = resolve(value); + + if (!existsSync(resolvedPath)) { + throw new Error('The selected path does not exist.'); + } + + return resolvedPath; +} + +function canonicalizeExistingPath(path: string): string { + return realpathSync(path); +} + +function hasNonFileScheme(value: string): boolean { + const looksLikeWindowsDrivePath = /^[a-zA-Z]:[\\/]/.test(value); + return ( + !looksLikeWindowsDrivePath && /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value) + ); +} + +function validateProgressBarValue(value: unknown): number | null { + if (value === null || value === undefined) { + return null; + } + + if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) { + throw new Error('Progress bar value must be a finite number between 0 and 1.'); + } + + if (value < 0 || value > 1) { + throw new Error('Progress bar value must be between 0 and 1.'); + } + + return value; +} + +function validateExternalTarget(target: unknown): string { + const value = normalizeString(target).trim(); + let url: URL; + + try { + url = new URL(value); + } catch { + throw new Error('Enter a valid HTTP or HTTPS URL.'); + } + + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error('Only HTTP and HTTPS URLs can be opened externally.'); + } + + return url.toString(); +} + +function configureAutoUpdater(): void { + if (!app.isPackaged) { + updateStatus = { + ...updateStatus, + supported: false, + state: 'unsupported', + message: 'Desktop updates are available in packaged builds.', + }; + return; + } + + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + + autoUpdater.on('checking-for-update', () => { + updateStatus = { + ...updateStatus, + supported: true, + currentVersion: app.getVersion(), + state: 'checking', + availableVersion: null, + percent: null, + transferredBytes: null, + totalBytes: null, + message: 'Checking GitHub Releases for a newer desktop build…', + }; + pushUpdateStatus(); + }); + + autoUpdater.on('update-available', (info) => { + updateStatus = { + ...updateStatus, + supported: true, + state: 'available', + availableVersion: info.version, + percent: null, + transferredBytes: null, + totalBytes: null, + message: `Version ${info.version} is available for download.`, + }; + pushUpdateStatus(); + notify('CheckAI Desktop', `Desktop update ${info.version} is available.`); + }); + + autoUpdater.on('update-not-available', () => { + updateStatus = { + ...updateStatus, + supported: true, + state: 'up-to-date', + availableVersion: null, + percent: null, + transferredBytes: null, + totalBytes: null, + message: `CheckAI Desktop ${app.getVersion()} is up to date.`, + }; + pushUpdateStatus(); + }); + + autoUpdater.on('download-progress', (progress) => { + updateStatus = { + ...updateStatus, + supported: true, + state: 'downloading', + percent: progress.percent, + transferredBytes: progress.transferred, + totalBytes: progress.total, + message: `Downloading version ${updateStatus.availableVersion ?? 'update'}…`, + }; + pushUpdateStatus(); + }); + + autoUpdater.on('update-downloaded', (info) => { + updateStatus = { + ...updateStatus, + supported: true, + state: 'downloaded', + availableVersion: info.version, + percent: 100, + transferredBytes: updateStatus.totalBytes, + totalBytes: updateStatus.totalBytes, + message: `Version ${info.version} is ready to install. Restart the app to finish updating.`, + }; + pushUpdateStatus(); + notify('CheckAI Desktop', `Update ${info.version} is ready to install.`); + }); + + autoUpdater.on('error', (error) => { + updateStatus = { + ...updateStatus, + supported: true, + state: 'error', + percent: null, + transferredBytes: null, + totalBytes: null, + message: error instanceof Error ? error.message : String(error), + }; + pushUpdateStatus(); + }); +} + +async function checkForUpdates(): Promise { + if (!app.isPackaged) { + updateStatus = { + ...updateStatus, + supported: false, + state: 'unsupported', + message: 'Desktop updates are available in packaged builds.', + }; + pushUpdateStatus(); + return updateStatus; + } + + try { + await autoUpdater.checkForUpdates(); + } catch (error) { + console.error('Failed to check for desktop updates:', error); + updateStatus = { + ...updateStatus, + supported: true, + state: 'error', + message: error instanceof Error ? error.message : String(error), + }; + pushUpdateStatus(); + } + return updateStatus; +} + +async function downloadUpdate(): Promise { + if (!app.isPackaged) { + return checkForUpdates(); + } + + if (updateStatus.state !== 'available') { + updateStatus = { + ...updateStatus, + state: updateStatus.state === 'downloaded' ? 'downloaded' : 'error', + message: + updateStatus.state === 'downloaded' + ? updateStatus.message + : 'No downloadable desktop update is currently available.', + }; + pushUpdateStatus(); + return updateStatus; + } + + updateStatus = { + ...updateStatus, + supported: true, + state: 'downloading', + percent: 0, + transferredBytes: 0, + totalBytes: null, + message: `Downloading version ${updateStatus.availableVersion ?? 'update'}…`, + }; + pushUpdateStatus(); + + try { + await autoUpdater.downloadUpdate(); + } catch (error) { + updateStatus = { + ...updateStatus, + supported: true, + state: 'error', + percent: null, + transferredBytes: null, + totalBytes: null, + message: error instanceof Error ? error.message : String(error), + }; + pushUpdateStatus(); + } + return updateStatus; +} + +function installUpdate(): void { + if (updateStatus.state !== 'downloaded') { + return; + } + + autoUpdater.quitAndInstall(); +} + +function dispatchMenuCommand(command: string): void { + mainWindow?.webContents.send('checkai:menu-command', command); +} + +function buildApplicationMenu(): Menu { + const template: MenuItemConstructorOptions[] = [ + { + label: 'File', + submenu: [ + { + label: 'New Game', + accelerator: 'CmdOrCtrl+N', + click: () => dispatchMenuCommand('new-game'), + }, + { + label: 'Import FEN from File…', + accelerator: 'CmdOrCtrl+O', + click: () => dispatchMenuCommand('import-fen-file'), + }, + { type: 'separator' }, + { + label: 'Copy FEN', + accelerator: 'CmdOrCtrl+Shift+C', + click: () => dispatchMenuCommand('export-fen'), + }, + { + label: 'Save FEN…', + accelerator: 'CmdOrCtrl+Shift+S', + click: () => dispatchMenuCommand('save-fen'), + }, + { + label: 'Copy PGN', + accelerator: 'CmdOrCtrl+Alt+C', + click: () => dispatchMenuCommand('export-pgn'), + }, + { + label: 'Save PGN…', + accelerator: 'CmdOrCtrl+Alt+S', + click: () => dispatchMenuCommand('save-pgn'), + }, + { type: 'separator' }, + process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' }, + ], + }, + { + label: 'Navigate', + submenu: [ + { + label: 'Dashboard', + accelerator: 'CmdOrCtrl+1', + click: () => dispatchMenuCommand('nav:dashboard'), + }, + { + label: 'Games', + accelerator: 'CmdOrCtrl+2', + click: () => dispatchMenuCommand('nav:games'), + }, + { + label: 'Board', + accelerator: 'CmdOrCtrl+3', + click: () => dispatchMenuCommand('nav:board'), + }, + { + label: 'Archive', + accelerator: 'CmdOrCtrl+4', + click: () => dispatchMenuCommand('nav:archive'), + }, + { + label: 'Analysis', + accelerator: 'CmdOrCtrl+5', + click: () => dispatchMenuCommand('nav:analysis'), + }, + { + label: 'Engine', + accelerator: 'CmdOrCtrl+6', + click: () => dispatchMenuCommand('nav:engine'), + }, + { + label: 'Logs', + accelerator: 'CmdOrCtrl+7', + click: () => dispatchMenuCommand('nav:logs'), + }, + { + label: 'Settings', + accelerator: 'CmdOrCtrl+,', + click: () => dispatchMenuCommand('nav:settings'), + }, + { type: 'separator' }, + { + label: 'Command Palette', + accelerator: 'CmdOrCtrl+K', + click: () => dispatchMenuCommand('open-command-palette'), + }, + ], + }, + { + label: 'Tools', + submenu: [ + { + label: 'Start Backend', + accelerator: 'CmdOrCtrl+Shift+R', + click: () => dispatchMenuCommand('start-backend'), + }, + { + label: 'Stop Backend', + accelerator: 'CmdOrCtrl+Shift+.', + click: () => dispatchMenuCommand('stop-backend'), + }, + { type: 'separator' }, + { + label: 'Open Working Directory', + accelerator: 'CmdOrCtrl+Shift+O', + click: () => dispatchMenuCommand('open-working-directory'), + }, + { + label: 'Check for Updates', + accelerator: 'CmdOrCtrl+Shift+U', + click: () => dispatchMenuCommand('check-for-updates'), + }, + { type: 'separator' }, + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + ], + }, + { + label: 'Window', + submenu: [{ role: 'minimize' }, { role: 'zoom' }, { role: 'togglefullscreen' }], + }, + { + label: 'Help', + submenu: [ + { + label: 'Open CheckAI Documentation', + click: () => { + void shell.openExternal('https://github.com/JosunLP/checkai/tree/main/docs'); + }, + }, + { + label: 'Open Repository', + click: () => { + void shell.openExternal('https://github.com/JosunLP/checkai'); + }, + }, + ], + }, + ]; + + return Menu.buildFromTemplate(template); +} + +function startBackend(state: DesktopState): BackendStatusPayload { + if (!backendProcess && backendStopRequested) { + backendStopRequested = false; + clearBackendForceKillTimer(); + } + + if (backendProcess && backendStopRequested) { + backendStatus = { + ...backendStatus, + lastError: 'Waiting for the local backend to finish stopping.', + }; + pushBackendStatus(); + return backendStatus; + } + + if (backendProcess) { + return backendStatus; + } + + const executable = state.backendExecutable.trim(); + if (!executable) { + backendStatus = { + ...backendStatus, + lastError: 'Set a backend executable before starting the local engine.', + }; + pushBackendStatus(); + return backendStatus; + } + + const args = buildBackendArgs(state); + const command = [executable, ...args].join(' '); + backendLogs = ''; + flushBackendLogs(); + + try { + clearBackendForceKillTimer(); + backendStopRequested = false; + backendProcess = spawn(executable, args, { + cwd: state.backendWorkingDirectory.trim() || undefined, + stdio: 'pipe', + }); + } catch (error) { + backendStatus = { + running: false, + pid: null, + command, + startedAt: null, + exitCode: null, + lastError: error instanceof Error ? error.message : String(error), + }; + flushBackendLogs(); + pushBackendStatus(); + return backendStatus; + } + + backendStatus = { + running: true, + pid: backendProcess.pid ?? null, + command, + startedAt: Date.now(), + exitCode: null, + lastError: null, + }; + pushBackendStatus(); + notify('CheckAI Desktop', 'Local backend started.'); + + const processRef = backendProcess; + const startedAt = backendStatus.startedAt; + + processRef.stdout.on('data', (chunk: Buffer) => { + appendBackendLogs(chunk.toString('utf8')); + }); + processRef.stderr.on('data', (chunk: Buffer) => { + appendBackendLogs(chunk.toString('utf8')); + }); + processRef.on('error', (error) => { + if (backendProcess !== processRef) { + return; + } + + if (backendExitListener) { + processRef.removeListener('exit', backendExitListener); + backendExitListener = null; + } + + clearBackendForceKillTimer(); + backendStopRequested = false; + backendProcess = null; + backendStatus = { + running: false, + pid: null, + command, + startedAt, + exitCode: null, + lastError: error instanceof Error ? error.message : String(error), + }; + flushBackendLogs(); + pushBackendStatus(); + }); + + backendExitListener = ( + code: number | null, + signal: NodeJS.Signals | null + ) => { + if (backendProcess !== processRef) { + return; + } + + const stopRequested = backendStopRequested; + const lastError = getBackendExitError(code, signal, stopRequested); + clearBackendForceKillTimer(); + backendProcess = null; + backendExitListener = null; + backendStatus = { + running: false, + pid: null, + command, + startedAt, + exitCode: code, + lastError, + }; + flushBackendLogs(); + pushBackendStatus(); + backendStopRequested = false; + notify( + 'CheckAI Desktop', + stopRequested || lastError === null + ? 'Local backend stopped.' + : `Local backend exited unexpectedly: ${lastError}` + ); + }; + processRef.on('exit', backendExitListener); + + return backendStatus; +} + +function stopBackend(): BackendStatusPayload { + if (!backendProcess) { + return backendStatus; + } + + if (backendStopRequested) { + backendStatus = { + ...backendStatus, + lastError: 'Backend stop already in progress.', + }; + pushBackendStatus(); + return backendStatus; + } + + const processRef = backendProcess; + backendStopRequested = true; + + try { + processRef.kill(); + } catch (error) { + clearBackendForceKillTimer(); + backendProcess = null; + backendStopRequested = false; + backendStatus = { + ...backendStatus, + running: false, + pid: null, + lastError: error instanceof Error ? error.message : String(error), + }; + pushBackendStatus(); + return backendStatus; + } + + clearBackendForceKillTimer(); + backendForceKillTimer = setTimeout(() => { + if (!backendStopRequested || backendProcess !== processRef) { + clearBackendForceKillTimer(); + return; + } + + if (backendExitListener) { + processRef.removeListener('exit', backendExitListener); + backendExitListener = null; + } + + try { + processRef.kill('SIGKILL'); + } catch { + // Ignore errors from killing an already-terminated process. + } + + backendProcess = null; + backendStopRequested = false; + backendStatus = { + ...backendStatus, + running: false, + pid: null, + lastError: + backendStatus.lastError ?? + 'Backend did not exit after the stop request and was force-killed.', + }; + pushBackendStatus(); + clearBackendForceKillTimer(); + }, BACKEND_FORCE_KILL_TIMEOUT_MS); + + backendStatus = { ...backendStatus, lastError: null }; + pushBackendStatus(); + return backendStatus; +} + +async function selectPath(kind: 'file' | 'directory'): Promise { + if (!mainWindow) return null; + const result = await dialog.showOpenDialog(mainWindow, { + properties: kind === 'directory' ? ['openDirectory'] : ['openFile'], + }); + if (result.canceled || result.filePaths.length === 0) { + return null; + } + const selectedPath = result.filePaths[0] ?? null; + if (!selectedPath) { + return null; + } + + const resolvedPath = resolve(selectedPath); + if (kind === 'file') { + try { + const canonicalPath = canonicalizeExistingPath(resolvedPath); + readableFileSelections.delete(resolvedPath); + readableFileSelections.set(resolvedPath, canonicalPath); + while (readableFileSelections.size > MAX_READABLE_FILE_SELECTIONS) { + const oldestPath = readableFileSelections.keys().next().value!; + readableFileSelections.delete(oldestPath); + } + } catch { + throw new Error('The selected file could not be prepared for reading.'); + } + } + // Directory selections are not tracked because the renderer has no IPC that can + // read directory contents; this allowlist only protects text-file reads. + + return resolvedPath; +} + +function validateReadableTextFileTarget(target: unknown): string { + const value = normalizeString(target).trim(); + if (!value) { + throw new Error('Select a local file first.'); + } + + if (hasNonFileScheme(value)) { + throw new Error( + 'Only files selected through the native picker can be read.' + ); + } + + const resolvedPath = resolve(value); + const expectedCanonicalPath = readableFileSelections.get(resolvedPath); + if (!expectedCanonicalPath) { + throw new Error('Only files selected through the native picker can be read.'); + } + + let canonicalPath: string; + try { + canonicalPath = canonicalizeExistingPath(resolvedPath); + } catch { + throw new Error('The selected file can no longer be read.'); + } + + if (canonicalPath !== expectedCanonicalPath) { + throw new Error('The selected file can no longer be read.'); + } + + return resolvedPath; +} + +function readTextFile(target: unknown): string { + const path = validateReadableTextFileTarget(target); + const content = readFileSync(path, 'utf8'); + // Consume the selection token after one successful read so a compromised + // renderer cannot keep reusing an old picker approval indefinitely. + readableFileSelections.delete(path); + return content; +} + +async function saveTextFile(options: unknown): Promise { + if (!mainWindow) return null; + + const candidate = + typeof options === 'object' && options !== null + ? (options as Partial) + : {}; + const defaultPath = normalizeString(candidate.defaultPath).trim(); + const content = normalizeString(candidate.content); + const filters = Array.isArray(candidate.filters) + ? candidate.filters + .map((filter) => { + const name = normalizeString(filter?.name).trim(); + const extensions = Array.isArray(filter?.extensions) + ? filter.extensions + .map((extension) => + normalizeString(extension).trim().replace(/^\./, '') + ) + .filter(Boolean) + : []; + return name && extensions.length > 0 ? { name, extensions } : null; + }) + .filter( + ( + filter + ): filter is { + name: string; + extensions: string[]; + } => filter !== null + ) + : []; + + const result = await dialog.showSaveDialog(mainWindow, { + defaultPath: defaultPath || undefined, + filters: filters.length > 0 ? filters : undefined, + }); + + if (result.canceled || !result.filePath) { + return null; + } + + writeFileSync(result.filePath, content, 'utf8'); + return result.filePath; +} + +function createWindow(): void { + const __dirname = dirname(fileURLToPath(import.meta.url)); + const preload = join(__dirname, 'preload.js'); + const rendererIndex = resolve(__dirname, '../dist/index.html'); + + mainWindow = new BrowserWindow({ + width: 1560, + height: 980, + minWidth: 1180, + minHeight: 760, + backgroundColor: '#0b1020', + title: 'CheckAI Desktop', + webPreferences: { + preload, + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + }, + }); + + void mainWindow.loadFile(rendererIndex); + Menu.setApplicationMenu(buildApplicationMenu()); + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +function registerIpcHandlers(): void { + ipcMain.handle('checkai:get-state', () => loadState()); + ipcMain.handle('checkai:save-state', (_event, state: unknown) => + saveState(state) + ); + ipcMain.handle('checkai:get-backend-status', () => backendStatus); + ipcMain.handle('checkai:get-backend-logs', () => backendLogs); + ipcMain.handle('checkai:get-update-status', () => updateStatus); + ipcMain.handle('checkai:set-progress-bar', (_event, progress: unknown) => { + const normalized = validateProgressBarValue(progress); + mainWindow?.setProgressBar(normalized ?? -1); + }); + ipcMain.handle('checkai:start-backend', async (_event, state: unknown) => { + const saved = await saveState(state); + return startBackend(saved); + }); + ipcMain.handle('checkai:stop-backend', () => stopBackend()); + ipcMain.handle('checkai:check-for-updates', () => checkForUpdates()); + ipcMain.handle('checkai:download-update', () => downloadUpdate()); + ipcMain.handle('checkai:install-update', () => installUpdate()); + ipcMain.handle('checkai:pick-file', () => selectPath('file')); + ipcMain.handle('checkai:pick-directory', () => selectPath('directory')); + ipcMain.handle('checkai:read-text-file', (_event, target: unknown) => + readTextFile(target) + ); + ipcMain.handle('checkai:save-text-file', (_event, options: unknown) => + saveTextFile(options) + ); + ipcMain.handle('checkai:open-path', async (_event, target: unknown) => { + const path = validateOpenPathTarget(target); + const result = await shell.openPath(path); + if (result) { + throw new Error(result); + } + }); + ipcMain.handle('checkai:open-external', (_event, target: unknown) => + shell.openExternal(validateExternalTarget(target)) + ); + ipcMain.handle('checkai:notify', (_event, title: unknown, body: unknown) => + notify(title, body) + ); +} + +registerIpcHandlers(); + +app.whenReady().then(async () => { + configureAutoUpdater(); + createWindow(); + + const state = await loadState(); + if (state.autoStartBackend) { + startBackend(state); + } + + void checkForUpdates(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('before-quit', () => { + stopBackend(); +}); + +app.on('window-all-closed', () => { + stopBackend(); + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/desktop/src/main.ts b/desktop/src/main.ts new file mode 100644 index 0000000..d14d3d7 --- /dev/null +++ b/desktop/src/main.ts @@ -0,0 +1,9 @@ +import './styles.scss'; +import { mount } from 'svelte'; +import App from './App.svelte'; + +const app = mount(App, { + target: document.getElementById('app')!, +}); + +export default app; diff --git a/desktop/src/notifications.ts b/desktop/src/notifications.ts new file mode 100644 index 0000000..43bc31f --- /dev/null +++ b/desktop/src/notifications.ts @@ -0,0 +1,41 @@ +import { errorMsg, toastMsg } from './stores.js'; + +const TOAST_DURATION_MS = 3200; +const ERROR_DURATION_MS = 5000; + +let toastTimer: ReturnType | null = null; +let errorTimer: ReturnType | null = null; + +export function clearNotificationTimers(): void { + if (toastTimer) { + clearTimeout(toastTimer); + toastTimer = null; + } + + if (errorTimer) { + clearTimeout(errorTimer); + errorTimer = null; + } +} + +export function pushToast(message: string): void { + toastMsg.set(message); + if (toastTimer) { + clearTimeout(toastTimer); + } + toastTimer = setTimeout(() => { + toastMsg.set(null); + toastTimer = null; + }, TOAST_DURATION_MS); +} + +export function pushError(message: string): void { + errorMsg.set(message); + if (errorTimer) { + clearTimeout(errorTimer); + } + errorTimer = setTimeout(() => { + errorMsg.set(null); + errorTimer = null; + }, ERROR_DURATION_MS); +} diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts new file mode 100644 index 0000000..89dec08 --- /dev/null +++ b/desktop/src/preload.ts @@ -0,0 +1,87 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import type { + BackendStatusPayload, + DesktopApi, + DesktopState, + SaveTextFileOptions, + UpdateStatusPayload, +} from './shared-types.js'; + +const api: DesktopApi = { + getState: (): Promise => + ipcRenderer.invoke('checkai:get-state'), + saveState: (state: DesktopState): Promise => + ipcRenderer.invoke('checkai:save-state', state), + getBackendStatus: (): Promise => + ipcRenderer.invoke('checkai:get-backend-status'), + getBackendLogs: (): Promise => + ipcRenderer.invoke('checkai:get-backend-logs'), + getUpdateStatus: (): Promise => + ipcRenderer.invoke('checkai:get-update-status'), + setProgressBar: (progress: number | null): Promise => + ipcRenderer.invoke('checkai:set-progress-bar', progress), + startBackend: (state: DesktopState): Promise => + ipcRenderer.invoke('checkai:start-backend', state), + stopBackend: (): Promise => + ipcRenderer.invoke('checkai:stop-backend'), + checkForUpdates: (): Promise => + ipcRenderer.invoke('checkai:check-for-updates'), + downloadUpdate: (): Promise => + ipcRenderer.invoke('checkai:download-update'), + installUpdate: (): Promise => + ipcRenderer.invoke('checkai:install-update'), + pickFile: (): Promise => + ipcRenderer.invoke('checkai:pick-file'), + pickDirectory: (): Promise => + ipcRenderer.invoke('checkai:pick-directory'), + readTextFile: (path: string): Promise => + ipcRenderer.invoke('checkai:read-text-file', path), + saveTextFile: (options: SaveTextFileOptions): Promise => + ipcRenderer.invoke('checkai:save-text-file', options), + openPath: (path: string): Promise => + ipcRenderer.invoke('checkai:open-path', path), + openExternal: (url: string): Promise => + ipcRenderer.invoke('checkai:open-external', url), + notify: (title: string, body: string): Promise => + ipcRenderer.invoke('checkai:notify', title, body), + onBackendStatus: ( + callback: (status: BackendStatusPayload) => void + ): (() => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: BackendStatusPayload + ) => { + callback(payload); + }; + ipcRenderer.on('checkai:backend-status', listener); + return () => ipcRenderer.removeListener('checkai:backend-status', listener); + }, + onBackendLogs: (callback: (logs: string) => void): (() => void) => { + const listener = (_event: Electron.IpcRendererEvent, payload: string) => { + callback(payload); + }; + ipcRenderer.on('checkai:backend-logs', listener); + return () => ipcRenderer.removeListener('checkai:backend-logs', listener); + }, + onUpdateStatus: ( + callback: (status: UpdateStatusPayload) => void + ): (() => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: UpdateStatusPayload + ) => { + callback(payload); + }; + ipcRenderer.on('checkai:update-status', listener); + return () => ipcRenderer.removeListener('checkai:update-status', listener); + }, + onMenuCommand: (callback: (command: string) => void): (() => void) => { + const listener = (_event: Electron.IpcRendererEvent, payload: string) => { + callback(payload); + }; + ipcRenderer.on('checkai:menu-command', listener); + return () => ipcRenderer.removeListener('checkai:menu-command', listener); + }, +}; + +contextBridge.exposeInMainWorld('checkaiDesktop', api); diff --git a/desktop/src/renderer.ts b/desktop/src/renderer.ts new file mode 100644 index 0000000..4d7d352 --- /dev/null +++ b/desktop/src/renderer.ts @@ -0,0 +1,3242 @@ +import './styles.scss'; + +import { component, html, safeHtml } from '@bquery/bquery/component'; +import { computed, effect, signal } from '@bquery/bquery/reactive'; +import { + cancelAnalysisJob as apiCancelAnalysisJob, + createGame as apiCreateGame, + deleteGame as apiDeleteGame, + exportFen as apiExportFen, + exportPgn as apiExportPgn, + getAnalysisJob as apiGetAnalysisJob, + getBoardAscii as apiGetBoardAscii, + getGame as apiGetGame, + getLegalMoves as apiGetLegalMoves, + getStorageStats as apiGetStorageStats, + importFen as apiImportFen, + listAnalysisJobs as apiListAnalysisJobs, + listArchived as apiListArchived, + listGames as apiListGames, + replayArchived as apiReplayArchived, + startAnalysis as apiStartAnalysis, + submitAction as apiSubmitAction, + submitMove as apiSubmitMove, + setApiBase, +} from './api-client.js'; +import { + DEFAULT_DESKTOP_STATE, + FILES, + PIECE_UNICODE, + PROMOTION_PIECE_LIST, + RANKS, + type AnalysisJob, + type AnalysisResultPayload, + type ArchivedGameSummary, + type BackendPreset, + type BackendStatusPayload, + type BoardMap, + type DesktopApi, + type DesktopState, + type DesktopView, + type FenChar, + type Game, + type GameSummary, + type LegalMove, + type ReplayState, + type SaveTextFileOptions, + type StorageStats, + type UpdateStatusPayload, +} from './shared-types.js'; + +declare global { + interface Window { + checkaiDesktop?: DesktopApi; + } +} + +// ── Fallback API for running outside Electron ─────────────────────────────── + +const fallbackApi: DesktopApi = { + async getState() { + return { ...DEFAULT_DESKTOP_STATE }; + }, + async saveState(s) { + return s; + }, + async getBackendStatus() { + return { + running: false, + pid: null, + command: null, + startedAt: null, + exitCode: null, + lastError: 'Electron preload bridge unavailable.', + }; + }, + async getBackendLogs() { + return 'Open this build inside Electron to use native features.'; + }, + async startBackend() { + return fallbackApi.getBackendStatus(); + }, + async stopBackend() { + return fallbackApi.getBackendStatus(); + }, + async getUpdateStatus() { + return { + supported: false, + currentVersion: 'dev', + state: 'unsupported' as const, + availableVersion: null, + percent: null, + transferredBytes: null, + totalBytes: null, + message: 'Desktop updates are available in packaged builds.', + }; + }, + async setProgressBar() {}, + async checkForUpdates() { + return fallbackApi.getUpdateStatus(); + }, + async downloadUpdate() { + return fallbackApi.getUpdateStatus(); + }, + async installUpdate() {}, + async pickFile() { + return null; + }, + async pickDirectory() { + return null; + }, + async readTextFile() { + throw new Error('Local file reading is only available inside Electron.'); + }, + async saveTextFile() { + return null; + }, + async openPath() {}, + async openExternal() {}, + async notify() {}, + onBackendStatus() { + return () => undefined; + }, + onBackendLogs() { + return () => undefined; + }, + onUpdateStatus() { + return () => undefined; + }, + onMenuCommand() { + return () => undefined; + }, +}; + +const desktop = window.checkaiDesktop ?? fallbackApi; + +// ── Reactive state ────────────────────────────────────────────────────────── + +const desktopState = signal({ ...DEFAULT_DESKTOP_STATE }); +const currentView = signal('dashboard'); +const backendStatus = signal({ + running: false, + pid: null, + command: null, + startedAt: null, + exitCode: null, + lastError: null, +}); +const backendLogs = signal(''); +const updateStatus = signal({ + supported: false, + currentVersion: 'dev', + state: 'unsupported', + availableVersion: null, + percent: null, + transferredBytes: null, + totalBytes: null, + message: null, +}); +const paletteOpen = signal(false); +const paletteQuery = signal(''); +const toastMsg = signal(null); +const errorMsg = signal(null); +const boardAscii = signal(''); +const liveConnection = signal<'connecting' | 'connected' | 'disconnected'>( + 'disconnected' +); +const liveMessage = signal('Live sync offline'); +const importDropActive = signal(false); + +// Engine data +const gamesList = signal([]); +const activeGame = signal(null); +const legalMoves = signal([]); +const selectedSquare = signal(null); +const archivedList = signal([]); +const storageStats = signal(null); +const replayState = signal(null); +const replayGameId = signal(null); +const analysisJobs = signal([]); +const activeAnalysis = signal(null); +const fenInput = signal(''); +const analysisDepth = signal(30); +const analysisPolling = signal(false); + +const notifiedAnalysisJobs = new Set(); +let liveWs: WebSocket | null = null; +let liveReconnectTimer: ReturnType | null = null; +let liveReconnectDelay = 1000; +let liveSubscribedGameId: string | null = null; +const LIVE_RECONNECT_MAX_DELAY = 30000; + +// Computed +const boardFlipped = computed(() => desktopState.value.boardFlipped); + +const highlightSquares = computed((): Set => { + const sq = selectedSquare.value; + if (!sq) return new Set(); + return new Set( + legalMoves.value.filter((m) => m.from === sq).map((m) => m.to) + ); +}); + +const lastMove = computed((): { from: string; to: string } | null => { + const g = activeGame.value; + if (!g || g.move_history.length === 0) return null; + const last = g.move_history[g.move_history.length - 1]; + return { from: last.move_json.from, to: last.move_json.to }; +}); + +const currentWorkspace = computed( + () => + desktopState.value.backendWorkingDirectory.trim() || 'No workspace selected' +); + +const backendUptimeLabel = computed(() => { + const startedAt = backendStatus.value.startedAt; + if (!backendStatus.value.running || !startedAt) return '—'; + const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000)); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + if (hours > 0) return `${hours}h ${minutes}m`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; +}); + +const filteredPaletteActions = computed(() => { + const query = paletteQuery.value.trim().toLowerCase(); + const actions = [ + { id: 'create-game', label: 'New game', meta: 'Start a fresh game' }, + { + id: 'start-backend', + label: 'Start backend', + meta: 'Launch local engine', + }, + { id: 'stop-backend', label: 'Stop backend', meta: 'Stop local engine' }, + { + id: 'refresh-logs', + label: 'Refresh logs', + meta: 'Reload backend console output', + }, + { + id: 'import-fen-file', + label: 'Import FEN file', + meta: 'Load a FEN from disk', + }, + { + id: 'save-settings', + label: 'Save workspace', + meta: 'Persist desktop preferences', + }, + { + id: 'nav:dashboard', + label: 'Dashboard', + meta: 'Overview and quick actions', + }, + { id: 'nav:games', label: 'Games', meta: 'Active games and FEN import' }, + { id: 'nav:archive', label: 'Archive', meta: 'Replay completed games' }, + { id: 'nav:analysis', label: 'Analysis', meta: 'Engine analysis jobs' }, + { + id: 'nav:engine', + label: 'Engine config', + meta: 'Backend presets and assets', + }, + { id: 'nav:logs', label: 'Logs', meta: 'Backend and debug output' }, + { id: 'nav:settings', label: 'Settings', meta: 'Desktop preferences' }, + { + id: 'check-updates', + label: 'Check updates', + meta: 'Look for a newer desktop build', + }, + { id: 'flip-board', label: 'Flip board', meta: 'Swap board orientation' }, + ]; + + if (!query) return actions; + return actions.filter( + (action) => + action.label.toLowerCase().includes(query) || + action.meta.toLowerCase().includes(query) + ); +}); + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function escapeHtml(v: string): string { + return v + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function formatBytes(v: number | null): string { + if (v === null || !Number.isFinite(v)) return '—'; + if (v < 1024) return `${v} B`; + const units = ['KB', 'MB', 'GB']; + let s = v / 1024; + let i = 0; + while (s >= 1024 && i < units.length - 1) { + s /= 1024; + i++; + } + return `${s.toFixed(s >= 10 ? 0 : 1)} ${units[i]}`; +} + +function formatDateTime(value: number | null | undefined): string { + if (!value) return '—'; + return new Date(value * 1000).toLocaleString(); +} + +function basename(input: string): string { + const normalized = input.replace(/[\\/]+$/, ''); + if (!normalized) return input; + const parts = normalized.split(/[\\/]/); + return parts[parts.length - 1] || normalized; +} + +function toSlug(input: string): string { + return ( + input + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'checkai-export' + ); +} + +function buildExportOptions( + defaultPath: string, + content: string, + filters: SaveTextFileOptions['filters'] +): SaveTextFileOptions { + return { defaultPath, content, filters }; +} + +function normalizeFenText(raw: string): string { + return ( + raw + .replace(/^\uFEFF/, '') + .trim() + .split(/\r?\n/)[0] + ?.trim() ?? '' + ); +} + +function syncTheme(theme: DesktopState['theme']): void { + if (theme === 'system') { + const prefersLight = window.matchMedia( + '(prefers-color-scheme: light)' + ).matches; + document.documentElement.setAttribute( + 'data-theme', + prefersLight ? 'light' : 'dark' + ); + return; + } + document.documentElement.setAttribute('data-theme', theme); +} + +function showToast(msg: string): void { + toastMsg.value = msg; + setTimeout(() => { + if (toastMsg.value === msg) toastMsg.value = null; + }, 3000); +} + +function showError(msg: string): void { + errorMsg.value = msg; + setTimeout(() => { + if (errorMsg.value === msg) errorMsg.value = null; + }, 6000); +} + +async function selectPromotionMove(moves: LegalMove[]): Promise { + if (moves.length === 0) return null; + if (moves.length === 1) return moves[0]; + + while (true) { + const choice = window.prompt( + `Choose promotion piece: enter ${PROMOTION_PIECE_LIST}.`, + moves[0].promotion ?? 'Q' + ); + if (choice === null) return null; + + const normalized = choice.trim().toUpperCase(); + const move = moves.find((candidate) => candidate.promotion?.toUpperCase() === normalized); + if (move) return move; + + showError(`Enter ${PROMOTION_PIECE_LIST} to choose a promotion piece.`); + } +} + +function qualityColor(q: string): string { + switch (q) { + case 'Best': + return '#34d399'; + case 'Excellent': + return '#6ee7b7'; + case 'Good': + return '#a3e635'; + case 'Inaccuracy': + return '#fbbf24'; + case 'Mistake': + return '#fb923c'; + case 'Blunder': + return '#fb7185'; + case 'Book': + return '#94a3b8'; + default: + return '#94a3b8'; + } +} + +function resultLabel(r: string | null): string { + if (r === 'WhiteWins') return '1-0'; + if (r === 'BlackWins') return '0-1'; + if (r === 'Draw') return '½-½'; + return '*'; +} + +async function notifyUser(title: string, body: string): Promise { + if (!desktopState.value.notificationsEnabled) return; + try { + await desktop.notify(title, body); + } catch { + // Best-effort only. + } +} + +function updateRecentWorkspaces(path: string): void { + const normalized = path.trim(); + if (!normalized) return; + desktopState.value = { + ...desktopState.value, + recentWorkspaces: [ + normalized, + ...desktopState.value.recentWorkspaces.filter( + (entry) => entry !== normalized + ), + ].slice(0, 8), + }; +} + +function buildPresetFromState(name: string): BackendPreset { + return { + id: crypto.randomUUID(), + name, + backendExecutable: desktopState.value.backendExecutable, + backendArgs: desktopState.value.backendArgs, + backendWorkingDirectory: desktopState.value.backendWorkingDirectory, + backendUrl: desktopState.value.backendUrl, + openingBookPath: desktopState.value.openingBookPath, + tablebasePath: desktopState.value.tablebasePath, + autoStartBackend: desktopState.value.autoStartBackend, + createdAt: Date.now(), + }; +} + +function loadPresetIntoState(presetId: string): void { + const preset = desktopState.value.backendPresets.find( + (entry) => entry.id === presetId + ); + if (!preset) return; + desktopState.value = { + ...desktopState.value, + backendExecutable: preset.backendExecutable, + backendArgs: preset.backendArgs, + backendWorkingDirectory: preset.backendWorkingDirectory, + backendUrl: preset.backendUrl, + openingBookPath: preset.openingBookPath, + tablebasePath: preset.tablebasePath, + autoStartBackend: preset.autoStartBackend, + }; + if (preset.backendWorkingDirectory) { + updateRecentWorkspaces(preset.backendWorkingDirectory); + } + showToast(`Preset “${preset.name}” loaded`); +} + +function removePresetFromState(presetId: string): void { + const preset = desktopState.value.backendPresets.find( + (entry) => entry.id === presetId + ); + desktopState.value = { + ...desktopState.value, + backendPresets: desktopState.value.backendPresets.filter( + (entry) => entry.id !== presetId + ), + }; + if (preset) showToast(`Preset “${preset.name}” removed`); +} + +function saveCurrentPreset(): void { + const workspaceName = desktopState.value.backendWorkingDirectory + ? basename(desktopState.value.backendWorkingDirectory) + : 'Local engine'; + const preset = buildPresetFromState(workspaceName); + desktopState.value = { + ...desktopState.value, + backendPresets: [preset, ...desktopState.value.backendPresets].slice(0, 12), + }; + showToast(`Preset “${preset.name}” saved`); +} + +function liveWsUrl(): string | null { + try { + const url = new URL(desktopState.value.backendUrl); + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + url.pathname = '/ws'; + url.search = ''; + url.hash = ''; + return url.toString(); + } catch { + return null; + } +} + +function liveSend(payload: Record): void { + if (liveWs?.readyState === WebSocket.OPEN) { + liveWs.send(JSON.stringify(payload)); + } +} + +function subscribeLiveGame(gameId: string | null): void { + if (liveSubscribedGameId && liveSubscribedGameId !== gameId) { + liveSend({ action: 'unsubscribe', game_id: liveSubscribedGameId }); + } + liveSubscribedGameId = gameId; + if (gameId) { + liveSend({ action: 'subscribe', game_id: gameId }); + } +} + +function scheduleLiveReconnect(): void { + if (liveReconnectTimer) return; + liveReconnectTimer = setTimeout(() => { + liveReconnectTimer = null; + connectLiveUpdates(); + }, liveReconnectDelay); + liveReconnectDelay = Math.min( + liveReconnectDelay * 2, + LIVE_RECONNECT_MAX_DELAY + ); +} + +function handleLiveMessage(raw: unknown): void { + if (typeof raw !== 'string') return; + + try { + const message = JSON.parse(raw) as { + type?: string; + event?: string; + game_id?: string; + }; + + if (message.type !== 'event') return; + + if (message.event === 'game_updated' && message.game_id) { + if (activeGame.value?.game_id === message.game_id) { + void openGame(message.game_id, true); + } + void refreshGamesList(); + void refreshArchive(); + return; + } + + if (message.event === 'game_created' || message.event === 'game_deleted') { + void refreshGamesList(); + void refreshArchive(); + } + } catch { + // Ignore malformed messages. + } +} + +function connectLiveUpdates(): void { + const url = liveWsUrl(); + if (!url) { + liveConnection.value = 'disconnected'; + liveMessage.value = 'Live sync unavailable: invalid backend URL'; + return; + } + + if ( + liveWs && + (liveWs.readyState === WebSocket.OPEN || + liveWs.readyState === WebSocket.CONNECTING) + ) { + return; + } + + liveConnection.value = 'connecting'; + liveMessage.value = 'Connecting live sync…'; + + try { + liveWs = new WebSocket(url); + } catch { + liveConnection.value = 'disconnected'; + liveMessage.value = 'Live sync failed to start'; + scheduleLiveReconnect(); + return; + } + + liveWs.onopen = () => { + liveConnection.value = 'connected'; + liveMessage.value = 'Live sync active'; + liveReconnectDelay = 1000; + if (liveReconnectTimer) { + clearTimeout(liveReconnectTimer); + liveReconnectTimer = null; + } + subscribeLiveGame( + activeGame.value?.game_id ?? desktopState.value.lastGameId ?? null + ); + }; + + liveWs.onmessage = (event) => { + handleLiveMessage(event.data); + }; + + liveWs.onclose = () => { + liveConnection.value = 'disconnected'; + liveMessage.value = 'Live sync disconnected'; + liveWs = null; + scheduleLiveReconnect(); + }; + + liveWs.onerror = () => { + liveConnection.value = 'disconnected'; + liveMessage.value = 'Live sync error'; + }; +} + +// ── API Interactions ──────────────────────────────────────────────────────── + +async function refreshGamesList(): Promise { + try { + const res = await apiListGames(); + gamesList.value = res.games; + } catch (e) { + showError((e as Error).message); + } +} + +async function createNewGame(): Promise { + try { + const res = await apiCreateGame(); + await refreshGamesList(); + await openGame(res.game_id); + showToast('New game created'); + } catch (e) { + showError((e as Error).message); + } +} + +async function importFenText(fenText: string): Promise { + const fen = normalizeFenText(fenText); + if (!fen) { + showError('No FEN found to import.'); + return; + } + + try { + const res = await apiImportFen(fen); + fenInput.value = ''; + await refreshGamesList(); + await openGame(res.game_id); + showToast('Game imported from FEN'); + } catch (e) { + showError((e as Error).message); + } +} + +async function importFromFen(): Promise { + const fen = fenInput.value.trim(); + if (!fen) return; + await importFenText(fen); +} + +async function refreshBoardAscii(gameId: string): Promise { + try { + boardAscii.value = await apiGetBoardAscii(gameId); + } catch { + boardAscii.value = 'Board ASCII preview unavailable.'; + } +} + +async function openGame(id: string, keepCurrentView = false): Promise { + try { + const g = await apiGetGame(id); + activeGame.value = g; + selectedSquare.value = null; + boardAscii.value = ''; + updateDesktopState({ lastGameId: id }); + subscribeLiveGame(id); + if (!g.is_over) { + const res = await apiGetLegalMoves(id); + legalMoves.value = res.moves; + } else { + legalMoves.value = []; + } + await refreshBoardAscii(id); + if (!keepCurrentView) currentView.value = 'board'; + } catch (e) { + showError((e as Error).message); + } +} + +async function handleSquareClick(sq: string): Promise { + const g = activeGame.value; + if (!g || g.is_over) return; + + const sel = selectedSquare.value; + if (sel) { + const candidateMoves = legalMoves.value.filter((m) => m.from === sel && m.to === sq); + const move = await selectPromotionMove(candidateMoves); + if (move) { + try { + const res = await apiSubmitMove(g.game_id, sel, sq, move.promotion); + if (res.success) { + await openGame(g.game_id); + } else { + showError(res.message); + } + } catch (e) { + showError((e as Error).message); + } + selectedSquare.value = null; + return; + } + } + + const piece = g.state.board[sq]; + if (piece) { + const isWhitePiece = piece === piece.toUpperCase(); + const isWhiteTurn = g.state.turn === 'white'; + if (isWhitePiece === isWhiteTurn) { + selectedSquare.value = sq; + return; + } + } + selectedSquare.value = null; +} + +async function doDeleteGame(id: string): Promise { + try { + await apiDeleteGame(id); + if (activeGame.value?.game_id === id) { + activeGame.value = null; + legalMoves.value = []; + boardAscii.value = ''; + subscribeLiveGame(null); + } + await refreshGamesList(); + showToast('Game deleted'); + } catch (e) { + showError((e as Error).message); + } +} + +async function doAction(action: string, reason?: string): Promise { + const g = activeGame.value; + if (!g) return; + try { + await apiSubmitAction(g.game_id, action, reason); + await openGame(g.game_id); + } catch (e) { + showError((e as Error).message); + } +} + +async function doExportFen(): Promise { + const g = activeGame.value; + if (!g) return; + try { + const res = await apiExportFen(g.game_id); + await navigator.clipboard.writeText(res.fen); + showToast('FEN copied to clipboard'); + } catch (e) { + showError((e as Error).message); + } +} + +async function saveTextExport( + defaultPath: string, + content: string, + filters: SaveTextFileOptions['filters'], + successMessage: string +): Promise { + try { + const savedPath = await desktop.saveTextFile( + buildExportOptions(defaultPath, content, filters) + ); + if (savedPath) { + showToast(successMessage); + await notifyUser( + 'CheckAI Desktop', + `${successMessage} (${basename(savedPath)})` + ); + } + } catch (e) { + showError((e as Error).message); + } +} + +async function doSaveFen(): Promise { + const g = activeGame.value; + if (!g) return; + try { + const res = await apiExportFen(g.game_id); + await saveTextExport( + `${toSlug(g.game_id)}.fen`, + res.fen, + [{ name: 'FEN position', extensions: ['fen', 'txt'] }], + 'FEN saved to disk' + ); + } catch (e) { + showError((e as Error).message); + } +} + +async function doExportPgn(): Promise { + const g = activeGame.value; + if (!g) return; + try { + const pgn = await apiExportPgn(g.game_id); + await navigator.clipboard.writeText(pgn); + showToast('PGN copied to clipboard'); + } catch (e) { + showError((e as Error).message); + } +} + +async function doSavePgn(): Promise { + const g = activeGame.value; + if (!g) return; + try { + const pgn = await apiExportPgn(g.game_id); + await saveTextExport( + `${toSlug(g.game_id)}.pgn`, + pgn, + [{ name: 'PGN game', extensions: ['pgn', 'txt'] }], + 'PGN saved to disk' + ); + } catch (e) { + showError((e as Error).message); + } +} + +async function importFenFromFile(): Promise { + try { + const filePath = await desktop.pickFile(); + if (!filePath) return; + const text = await desktop.readTextFile(filePath); + await importFenText(text); + } catch (e) { + showError((e as Error).message); + } +} + +// ── Archive ───────────────────────────────────────────────────────────────── + +async function refreshArchive(): Promise { + try { + const res = await apiListArchived(); + archivedList.value = res.games; + if (res.storage) storageStats.value = res.storage; + } catch (e) { + showError((e as Error).message); + } +} + +async function openArchivedGame(id: string): Promise { + try { + replayGameId.value = id; + const rs = await apiReplayArchived(id, 0); + replayState.value = rs; + currentView.value = 'archive'; + } catch (e) { + showError((e as Error).message); + } +} + +async function replayTo(moveNum: number): Promise { + const id = replayGameId.value; + if (!id) return; + try { + replayState.value = await apiReplayArchived(id, moveNum); + } catch (e) { + showError((e as Error).message); + } +} + +// ── Analysis ──────────────────────────────────────────────────────────────── + +let analysisPollTimer: ReturnType | null = null; + +async function refreshAnalysisJobs(): Promise { + try { + const res = await apiListAnalysisJobs(); + analysisJobs.value = res.jobs; + } catch (e) { + showError((e as Error).message); + } +} + +async function submitAnalysis(gameId: string): Promise { + try { + const res = await apiStartAnalysis(gameId, analysisDepth.value); + showToast('Analysis started'); + await refreshAnalysisJobs(); + await pollAnalysisJob(res.job_id); + } catch (e) { + showError((e as Error).message); + } +} + +async function pollAnalysisJob(jobId: string): Promise { + if (analysisPollTimer) { + clearInterval(analysisPollTimer); + analysisPollTimer = null; + } + analysisPolling.value = true; + const poll = async () => { + try { + const job = await apiGetAnalysisJob(jobId); + activeAnalysis.value = job; + if ( + job.status === 'Completed' || + job.status === 'Cancelled' || + (typeof job.status === 'object' && 'Failed' in job.status) + ) { + if (analysisPollTimer) { + clearInterval(analysisPollTimer); + analysisPollTimer = null; + } + analysisPolling.value = false; + if (!notifiedAnalysisJobs.has(job.id)) { + notifiedAnalysisJobs.add(job.id); + if (job.status === 'Completed') { + await notifyUser( + 'CheckAI analysis complete', + `Job ${job.id.slice(0, 8)} finished successfully.` + ); + } + if (typeof job.status === 'object' && 'Failed' in job.status) { + await notifyUser( + 'CheckAI analysis failed', + job.status.Failed.error + ); + } + } + await refreshAnalysisJobs(); + } + } catch { + if (analysisPollTimer) { + clearInterval(analysisPollTimer); + analysisPollTimer = null; + } + analysisPolling.value = false; + } + }; + await poll(); + if (analysisPolling.value) { + analysisPollTimer = setInterval(poll, 1500); + } +} + +async function cancelAnalysis(jobId: string): Promise { + try { + await apiCancelAnalysisJob(jobId); + await refreshAnalysisJobs(); + if (activeAnalysis.value?.id === jobId) activeAnalysis.value = null; + showToast('Analysis cancelled'); + } catch (e) { + showError((e as Error).message); + } +} + +// ── Desktop actions ───────────────────────────────────────────────────────── + +function updateDesktopState(patch: Partial): void { + desktopState.value = { ...desktopState.value, ...patch }; +} + +async function saveDesktopSettings(): Promise { + if (desktopState.value.backendWorkingDirectory.trim()) { + updateRecentWorkspaces(desktopState.value.backendWorkingDirectory); + } + const state = { ...desktopState.value, lastView: currentView.value }; + desktopState.value = await desktop.saveState(state); + showToast('Settings saved'); +} + +async function persistViewSilently(): Promise { + const state = { ...desktopState.value, lastView: currentView.value }; + desktopState.value = await desktop.saveState(state); +} + +async function startBackend(): Promise { + if (desktopState.value.backendWorkingDirectory.trim()) { + updateRecentWorkspaces(desktopState.value.backendWorkingDirectory); + } + const state = { ...desktopState.value, lastView: currentView.value }; + backendStatus.value = await desktop.startBackend(state); + setApiBase(desktopState.value.backendUrl); + connectLiveUpdates(); +} + +async function stopBackend(): Promise { + backendStatus.value = await desktop.stopBackend(); +} + +async function refreshLogs(): Promise { + backendLogs.value = await desktop.getBackendLogs(); +} + +// ── Board rendering ───────────────────────────────────────────────────────── + +function renderBoard( + board: BoardMap, + flipped: boolean, + interactive: boolean +): string { + const ranks = flipped ? [...RANKS] : [...RANKS].reverse(); + const files = flipped ? [...FILES].reverse() : [...FILES]; + const sel = selectedSquare.value; + const highlights = highlightSquares.value; + const lm = lastMove.value; + const check = activeGame.value?.is_check ?? false; + const turn = activeGame.value?.state.turn; + let kingSquare: string | null = null; + if (check && turn) { + const kingChar = turn === 'white' ? 'K' : 'k'; + for (const sq of Object.keys(board)) { + if (board[sq] === kingChar) { + kingSquare = sq; + break; + } + } + } + + let rows = ''; + for (const r of ranks) { + let cells = ''; + for (const f of files) { + const sq = `${f}${r}`; + const piece = board[sq] as FenChar | undefined; + const isLight = (f.charCodeAt(0) + parseInt(r)) % 2 === 0; + const classes = [ + 'sq', + isLight ? 'sq-light' : 'sq-dark', + sel === sq ? 'sq-selected' : '', + highlights.has(sq) ? 'sq-highlight' : '', + lm && (lm.from === sq || lm.to === sq) ? 'sq-last-move' : '', + kingSquare === sq ? 'sq-check' : '', + ] + .filter(Boolean) + .join(' '); + const pieceStr = piece ? PIECE_UNICODE[piece] : ''; + const cursor = interactive ? 'cursor:pointer;' : ''; + cells += `
${pieceStr}
`; + } + rows += `
${r}${cells}
`; + } + let fileLabels = '
'; + for (const f of files) fileLabels += `${f}`; + fileLabels += '
'; + return `
${rows}${fileLabels}
`; +} + +// ── Component: Dashboard ──────────────────────────────────────────────────── + +component('cai-dashboard', { + styles: `:host { display:block; }`, + render() { + const bs = backendStatus.value; + const us = updateStatus.value; + const games = gamesList.value; + const stats = storageStats.value; + const presets = desktopState.value.backendPresets; + const recentWorkspaces = desktopState.value.recentWorkspaces; + + return html` +
+
+
+
+

♔ CheckAI Desktop

+

+ Workspace-first control room for the CheckAI chess engine. +

+
+ ${bs.running ? '● Engine online' : '○ Engine offline'} +
+
+
+ Workspace + ${safeHtml`${currentWorkspace.value}`} +
+
+ Live sync + ${safeHtml`${liveMessage.value}`} +
+
+ Saved presets + ${presets.length} +
+
+
+ + + + + + +
+
+
+
+
+

⚡ Backend status

+

+ Local engine process health and runtime details. +

+
+ ${bs.running ? 'Running' : 'Stopped'} +
+
+
+ PID${bs.pid ?? '—'} +
+
+ Command${safeHtml`${bs.command ?? '—'}`} +
+
+ Version${safeHtml`${us.currentVersion}`} +
+
+ Uptime${backendUptimeLabel.value} +
+
+
+ + +
+
+
+
+
+

📂 Recent workspaces

+

Jump between productive local setups.

+
+
+ ${recentWorkspaces.length === 0 + ? html`

+ Choose a working directory to build your first desktop + workspace. +

` + : html`
+ ${recentWorkspaces + .map( + (workspace) => html` + + ` + ) + .join('')} +
`} +
+
+
+
+

♟ Active games

+

+ ${games.length} game${games.length !== 1 ? 's' : ''} in progress +

+
+ +
+ ${games.length === 0 + ? html`

+ No active games. Create one to get started. +

` + : html`
+ ${games + .slice(0, 5) + .map( + (g) => html` + + ` + ) + .join('')} +
`} +
+
+
+
+

💾 Storage

+

Disk usage for active and archived games.

+
+
+ ${stats + ? html`
+
+ Active${stats.active_count} · + ${formatBytes(stats.active_bytes)} +
+
+ Archived${stats.archived_count} · + ${formatBytes(stats.archive_bytes)} +
+
` + : html`

+ Start the backend to view storage statistics. +

`} +
+
+
+
+

🔖 Saved presets

+

+ Reusable backend launch profiles for common workflows. +

+
+ +
+ ${presets.length === 0 + ? html`

+ Save your current backend settings as a preset from the Engine + view. +

` + : html`
+ ${presets + .slice(0, 4) + .map( + (preset) => html` + + ` + ) + .join('')} +
`} +
+
+ `; + }, +}); + +// ── Component: Games List ─────────────────────────────────────────────────── + +component('cai-games', { + styles: `:host { display:block; }`, + render() { + const games = gamesList.value; + return html` +
+
+
+

♟ Active games

+

+ ${games.length} game${games.length !== 1 ? 's' : ''} loaded · + Create, open, or delete running games. +

+
+
+ + + +
+
+
+ + +
+
+ 📄 + Drag & drop a FEN file + Drop plain text, .fen, or .txt files anywhere in this panel. +
+ ${games.length === 0 + ? html`

No active games yet.

` + : html`
+ + + + + + + + + + + + + ${games + .map( + (g) => html` + + + + + + + + + ` + ) + .join('')} + +
IDTurnMoveStatusResult
${g.game_id.slice(0, 8)}…${g.turn}${g.fullmove_number} + ${g.is_over ? 'Over' : 'Active'} + ${resultLabel(g.result)} + + +
+
`} +
+ `; + }, +}); + +// ── Component: Board view ─────────────────────────────────────────────────── + +component('cai-board', { + styles: `:host { display:block; }`, + render() { + const g = activeGame.value; + if (!g) + return html`
+

No game selected

+

+ Open or create a game from the dashboard or games list. +

+
`; + + const boardHtml = renderBoard( + g.state.board, + boardFlipped.value, + !g.is_over + ); + const history = g.move_history; + const currentFen = `${ + Object.entries(g.state.board).filter(([, piece]) => Boolean(piece)).length + } pieces on board`; + + return html` +
+
+
+
+
+

Game ${g.game_id.slice(0, 8)}

+

+ ${g.is_over + ? `Game over — ${resultLabel(g.result)}${g.end_reason ? ' (' + g.end_reason + ')' : ''}` + : `${g.state.turn}'s turn · Move ${g.state.fullmove_number}${g.is_check ? ' · Check!' : ''}`} +

+
+
+ + + +
+
+
+ ${boardHtml} +
+ ${!g.is_over + ? html` +
+ + + + +
+ ` + : html` +
+ + +
+ `} +
+
+
+
+
+

ℹ Info

+
+
+
+ Turn${g.state.turn} +
+
+ Move${g.state.fullmove_number} +
+
+ Legal moves${g.legal_move_count} +
+
+ Halfmove clock${g.state.halfmove_clock} +
+
+ En passant${g.state.en_passant ?? '—'} +
+
+ Castling W${g.state.castling.white.kingside ? 'K' : '-'}${g.state + .castling.white.queenside + ? 'Q' + : '-'} +
+
+ Castling B${g.state.castling.black.kingside ? 'k' : '-'}${g.state + .castling.black.queenside + ? 'q' + : '-'} +
+
+
+ + + +
+
+
+
+

📝 Moves

+
+
+ ${history.length === 0 + ? html`

No moves yet.

` + : html`
    + ${renderMoveList(history)} +
`} +
+
+
+
+
+

🔍 Advanced board view

+

+ Desktop-native debugging aids for the current position. +

+
+
+
+
+ Game ID + ${g.game_id} +
+
+ Board state + ${currentFen} +
+
+
+${escapeHtml(boardAscii.value || 'Loading ASCII board…')}
+ ${desktopState.value.developerMode + ? html`
+ Raw game JSON +
+${escapeHtml(JSON.stringify(g, null, 2))}
+
` + : ''} +
+
+
+ `; + }, +}); + +function renderMoveList(history: Game['move_history']): string { + const pairs: string[] = []; + for (let i = 0; i < history.length; i += 2) { + const w = history[i]; + const b = history[i + 1]; + pairs.push( + html`
  • + ${w.move_number}. + ${safeHtml`${w.notation}`}${b + ? html` ${safeHtml`${b.notation}`}` + : ''} +
  • ` + ); + } + return pairs.join(''); +} + +// ── Component: Archive ────────────────────────────────────────────────────── + +component('cai-archive', { + styles: `:host { display:block; }`, + render() { + const rs = replayState.value; + const list = archivedList.value; + + return html` +
    + ${rs + ? html` +
    +
    +
    +

    + ▶ Replay · + ${rs.game_id.slice(0, 8)} +

    +

    Move ${rs.at_move} / ${rs.total_moves}

    +
    +
    + + + + + +
    +
    +
    + ${renderBoard(rs.state.board, boardFlipped.value, false)} +
    + +
    + ` + : ''} +
    +
    +
    +

    📦 Archived games

    +

    + ${list.length} completed game${list.length !== 1 ? 's' : ''} in + the archive +

    +
    + +
    + ${list.length === 0 + ? html`
    +

    + 📦 No archived games yet. Complete a game to see it here. +

    +
    ` + : html`
    + + + + + + + + + + + + + ${list + .map( + (g) => html` + + + + + + + + + ` + ) + .join('')} + +
    IDResultReasonMovesSize
    ${g.game_id.slice(0, 8)}…${resultLabel(g.result)}${g.end_reason ?? '—'}${g.move_count}${formatBytes(g.compressed_bytes)} + + +
    +
    `} +
    +
    + `; + }, +}); + +// ── Component: Analysis ───────────────────────────────────────────────────── + +component('cai-analysis', { + styles: `:host { display:block; }`, + render() { + const jobs = analysisJobs.value; + const active = activeAnalysis.value; + const result = active?.result; + const summary = result?.summary; + + return html` +
    + ${active + ? html` +
    +
    +
    +

    + Analysis · + ${active.id.slice(0, 8)} +

    +

    ${renderAnalysisStatusText(active)}

    +
    + ${analysisPolling.value + ? html`` + : html``} +
    + ${summary + ? html` +
    +
    +
    + White accuracy + ${summary.white_accuracy.toFixed(1)}% + avg ±${summary.white_avg_cp_loss.toFixed(1)} + cp +
    +
    + Black accuracy + ${summary.black_accuracy.toFixed(1)}% + avg ±${summary.black_avg_cp_loss.toFixed(1)} + cp +
    +
    +
    + ${renderQualityBar(summary)} +
    +
    +
    + Total moves${summary.total_moves} +
    +
    + Best${summary.best_moves} +
    +
    + Excellent${summary.excellent_moves} +
    +
    + Good${summary.good_moves} +
    +
    + Inaccuracies${summary.inaccuracies} +
    +
    + Mistakes${summary.mistakes} +
    +
    + Blunders${summary.blunders} +
    +
    + Book moves${summary.book_moves} +
    +
    + Depth${result!.depth} +
    +
    + Avg CP loss${summary.average_centipawn_loss.toFixed( + 1 + )} +
    +
    +
    + ${result!.annotations.length > 0 + ? html` +
    +

    Move annotations

    +
    + + + + + + + + + + + + + + ${result!.annotations + .map( + (a) => html` + + + + + + + + + + ` + ) + .join('')} + +
    #SidePlayedBestCP lossQualityPV
    ${a.move_number}${a.side} + ${a.played_move.from}${a + .played_move.to}${a.played_move + .promotion ?? ''} + + ${a.best_move.from}${a.best_move + .to}${a.best_move.promotion ?? + ''} + ${a.centipawn_loss} + ${safeHtml`${a.quality}`} + + ${a.principal_variation + .slice(0, 3) + .join(' ')} +
    +
    +
    + ` + : ''} + ` + : ''} +
    + ` + : ''} +
    +
    +
    +

    Analysis jobs

    +

    + Submit games for deep engine analysis · Configure depth and + review results. +

    +
    +
    + + +
    +
    + ${jobs.length === 0 + ? html`
    +

    + 📊 No analysis jobs yet. Open a game and submit it for + analysis. +

    +
    ` + : html`
    + + + + + + + + + + + + ${jobs + .map( + (j) => html` + + + + + + + + ` + ) + .join('')} + +
    JobGameStatusCreated
    ${j.id.slice(0, 8)}… + ${j.game_id?.slice(0, 8) ?? '—'}… + ${renderAnalysisStatusBadge(j)}${formatDateTime(j.created_at)} + + ${isAnalysisActive(j) + ? html`` + : ''} +
    +
    `} +
    +
    + `; + }, +}); + +function isAnalysisActive(job: AnalysisJob): boolean { + return ( + job.status === 'Queued' || + (typeof job.status === 'object' && 'InProgress' in job.status) + ); +} + +function renderAnalysisStatusText(job: AnalysisJob): string { + if (job.status === 'Queued') return 'Queued…'; + if (job.status === 'Completed') return 'Completed'; + if (job.status === 'Cancelled') return 'Cancelled'; + if (typeof job.status === 'object') { + if ('InProgress' in job.status) { + const p = job.status.InProgress; + return `Analyzing… ${p.moves_analyzed}/${p.total_moves} moves`; + } + if ('Failed' in job.status) return `Failed: ${job.status.Failed.error}`; + } + return 'Unknown'; +} + +function renderAnalysisStatusBadge(job: AnalysisJob): string { + if (job.status === 'Completed') + return 'Completed'; + if (job.status === 'Queued') + return 'Queued'; + if (job.status === 'Cancelled') + return 'Cancelled'; + if (typeof job.status === 'object' && 'InProgress' in job.status) { + const p = job.status.InProgress; + return `${p.moves_analyzed}/${p.total_moves}`; + } + if (typeof job.status === 'object' && 'Failed' in job.status) + return 'Failed'; + return '?'; +} + +function renderQualityBar(summary: AnalysisResultPayload['summary']): string { + const segments = [ + { count: summary.best_moves, color: '#34d399', label: 'Best' }, + { count: summary.excellent_moves, color: '#6ee7b7', label: 'Excellent' }, + { count: summary.good_moves, color: '#a3e635', label: 'Good' }, + { count: summary.inaccuracies, color: '#fbbf24', label: 'Inaccuracy' }, + { count: summary.mistakes, color: '#fb923c', label: 'Mistake' }, + { count: summary.blunders, color: '#fb7185', label: 'Blunder' }, + { count: summary.book_moves, color: '#94a3b8', label: 'Book' }, + ]; + return `
    ${segments + .filter((s) => s.count > 0) + .map( + (s) => + `
    ` + ) + .join('')}
    `; +} + +// ── Component: Engine config ──────────────────────────────────────────────── + +component('cai-engine', { + styles: `:host { display:block; }`, + render() { + const ds = desktopState.value; + const bs = backendStatus.value; + return html` +
    +
    +
    +
    +

    ⚙ Backend configuration

    +

    + Configure executable, arguments, and working directory for the + local backend. +

    +
    +
    + + + + + +
    + + + + +
    + ${bs.lastError + ? html`
    + ${safeHtml`${bs.lastError}`} +
    ` + : ''} +
    +
    +
    +
    +

    📂 Engine assets

    +

    + Opening books and tablebases applied to backend launch. +

    +
    +
    + + +
    + Note: + These paths are appended as --book-path and + --tablebase-path flags unless already present in your + arguments. +
    +
    +
    +
    +
    +

    🗂 Workspace session

    +

    Recent folders and persistent launch presets.

    +
    +
    +
    +
    + Current workspace + ${safeHtml`${currentWorkspace.value}`} +
    +
    + Recent folders + ${ds.recentWorkspaces.length} +
    +
    + Saved presets + ${ds.backendPresets.length} +
    +
    + ${ds.recentWorkspaces.length > 0 + ? html`
    + ${ds.recentWorkspaces + .map( + (workspace) => html` + + ` + ) + .join('')} +
    ` + : ''} + ${ds.backendPresets.length === 0 + ? html`

    + No presets yet. Save a profile for recurring runs. +

    ` + : html`
    + ${ds.backendPresets + .map( + (preset) => html` +
    +
    + ${safeHtml`${preset.name}`} +

    + ${safeHtml`${preset.backendArgs || 'serve'}`} +

    +
    +
    + + +
    +
    + ` + ) + .join('')} +
    `} +
    +
    + `; + }, +}); + +// ── Component: Logs ───────────────────────────────────────────────────────── + +component('cai-logs', { + styles: `:host { display:block; }`, + render() { + return html` +
    +
    +
    +
    +

    🩺 Diagnostics

    +

    + Desktop health, live sync, and current runtime context. +

    +
    +
    +
    +
    + Engine + ${backendStatus.value.running ? 'Running' : 'Stopped'} +
    +
    + Live sync + ${liveConnection.value} +
    +
    + Workspace + ${safeHtml`${currentWorkspace.value}`} +
    +
    + Last game + ${desktopState.value.lastGameId ?? '—'} +
    +
    +
    + Live status: + ${safeHtml`${liveMessage.value}`} +
    +
    +
    +
    +
    +

    📜 Backend logs

    +

    + Tail stdout/stderr from the local engine process. +

    +
    +
    + + +
    +
    +
    +${escapeHtml(backendLogs.value || 'No logs captured yet.')}
    +
    +
    +
    +
    +

    ♟ Board ASCII

    +

    + CLI-style board snapshot for debugging and support. +

    +
    +
    +
    +${escapeHtml(
    +              boardAscii.value || 'Open a game to inspect the current board.'
    +            )}
    +
    +
    + `; + }, +}); + +// ── Component: Settings ───────────────────────────────────────────────────── + +component('cai-settings', { + styles: `:host { display:block; }`, + render() { + const us = updateStatus.value; + const ds = desktopState.value; + return html` +
    +
    +
    +
    +

    ⚙ Desktop preferences

    +

    Appearance, layout, and notification settings.

    +
    +
    +
    +
    + Theme${ds.theme} +
    +
    + Board orientation${boardFlipped.value + ? 'Black at bottom' + : 'White at bottom'} +
    +
    + Notifications${ds.notificationsEnabled ? 'Enabled' : 'Muted'} +
    +
    + Developer mode${ds.developerMode ? 'On' : 'Off'} +
    +
    + Density${ds.compactMode ? 'Compact' : 'Comfortable'} +
    +
    +
    + + + + + + +
    +
    +
    +
    +
    +

    🔄 Desktop updates

    +

    Packaged builds can auto-update from GitHub.

    +
    + ${safeHtml`${us.currentVersion}`} +
    +
    + Status: + ${safeHtml`${us.message ?? 'Ready.'}`} +
    + ${us.percent !== null + ? html` +
    +
    +
    +

    + ${Math.round(us.percent)}% · + ${formatBytes(us.transferredBytes)} / + ${formatBytes(us.totalBytes)} +

    + ` + : ''} +
    + + ${us.state === 'available' + ? html`` + : ''} + ${us.state === 'downloaded' + ? html`` + : ''} +
    +
    +
    +
    +
    +

    ⌨ Keyboard shortcuts

    +

    + Available keyboard shortcuts in the desktop app. +

    +
    +
    +
      +
    • Ctrl/⌘ + K Command palette
    • +
    • Ctrl/⌘ + 1–8 Switch views
    • +
    • Ctrl/⌘ + N New game
    • +
    • Ctrl/⌘ + F Flip board
    • +
    • Escape Close palette / deselect
    • +
    +
    +
    + `; + }, +}); + +// ── Command palette ───────────────────────────────────────────────────────── + +component('cai-palette', { + styles: `:host { display:block; }`, + render() { + if (!paletteOpen.value) return ''; + return html` +
    + +
    + `; + }, +}); + +// ── Root app shell ────────────────────────────────────────────────────────── + +const VIEW_LABELS: Record = { + dashboard: 'Dashboard', + games: 'Games', + board: 'Board', + archive: 'Archive', + analysis: 'Analysis', + engine: 'Engine', + logs: 'Logs', + settings: 'Settings', +}; + +const VIEW_ICONS: Record = { + dashboard: '⌂', + games: '♟', + board: '♔', + archive: '📦', + analysis: '📊', + engine: '⚙', + logs: '📋', + settings: '🔧', +}; + +function renderViewContent(view: DesktopView): string { + switch (view) { + case 'dashboard': + return ''; + case 'games': + return ''; + case 'board': + return ''; + case 'archive': + return ''; + case 'analysis': + return ''; + case 'engine': + return ''; + case 'logs': + return ''; + case 'settings': + return ''; + default: + return ''; + } +} + +function renderShell(): string { + const view = currentView.value; + const bs = backendStatus.value; + const ds = desktopState.value; + const workspacePath = ds.backendWorkingDirectory.trim(); + const workspaceName = workspacePath + ? basename(workspacePath) + : 'No workspace selected'; + const engineState = bs.running + ? `Running${bs.pid ? ` · PID ${bs.pid}` : ''}` + : 'Stopped'; + const activeTitle = activeGame.value?.game_id + ? `Game ${activeGame.value.game_id.slice(0, 8)}` + : VIEW_LABELS[view]; + + return ` +
    + +
    +
    +
    + ${escapeHtml(VIEW_LABELS[view])} +

    ${escapeHtml(activeTitle)}

    +
    + + Workspace + ${escapeHtml(workspaceName)} + + + Engine + ${escapeHtml(engineState)} + + + Desktop + ${escapeHtml(updateStatus.value.currentVersion)} + +
    +
    +
    + + ${escapeHtml(liveMessage.value)} + + + ${ds.notificationsEnabled ? 'Notifications on' : 'Notifications off'} + + + +
    +
    + ${toastMsg.value ? `
    ${escapeHtml(toastMsg.value)}
    ` : ''} + ${errorMsg.value ? `
    ${escapeHtml(errorMsg.value)}
    ` : ''} + ${renderViewContent(view)} +
    +
    + + `; +} + +// ── Event delegation ──────────────────────────────────────────────────────── + +const appRoot = document.getElementById('app'); +if (!appRoot) throw new Error('Missing #app root element'); + +appRoot.addEventListener('click', (e) => { + const target = e.target; + if (!(target instanceof Element)) return; + + // Close palette + const closePaletteButton = target.closest( + 'button[data-close-palette]' + ); + if (closePaletteButton) { + paletteOpen.value = false; + paletteQuery.value = ''; + return; + } + + const paletteOverlay = target.closest( + '.overlay[data-close-palette]' + ); + if (paletteOverlay && target === paletteOverlay) { + paletteOpen.value = false; + paletteQuery.value = ''; + return; + } + + // Square clicks + const sqEl = target.closest('[data-sq]'); + if (sqEl && sqEl.closest('[data-board-interactive]')) { + void handleSquareClick(sqEl.dataset.sq!); + return; + } + + // Navigation + const navEl = target.closest('[data-nav]'); + if (navEl) { + const v = navEl.dataset.nav as DesktopView; + currentView.value = v; + void persistViewSilently(); + if (v === 'games') void refreshGamesList(); + if (v === 'archive') void refreshArchive(); + if (v === 'analysis') void refreshAnalysisJobs(); + if (v === 'logs') void refreshLogs(); + if (v === 'dashboard') { + void refreshGamesList(); + void apiGetStorageStats() + .then((s) => (storageStats.value = s)) + .catch(() => {}); + } + return; + } + + // Palette actions + const paletteBtn = target.closest('[data-palette]'); + if (paletteBtn) { + paletteOpen.value = false; + paletteQuery.value = ''; + const action = paletteBtn.dataset.palette!; + void handleAction(action); + return; + } + + // General actions + const actionEl = target.closest('[data-action]'); + if (actionEl) { + void handleAction(actionEl.dataset.action!, actionEl.dataset.id); + return; + } +}); + +appRoot.addEventListener('input', (e) => { + const t = e.target; + if (!(t instanceof HTMLInputElement)) return; + switch (t.id) { + case 'cfg-executable': + updateDesktopState({ backendExecutable: t.value }); + break; + case 'cfg-args': + updateDesktopState({ backendArgs: t.value }); + break; + case 'cfg-working-dir': + updateDesktopState({ backendWorkingDirectory: t.value }); + break; + case 'cfg-url': + updateDesktopState({ backendUrl: t.value }); + break; + case 'cfg-book': + updateDesktopState({ openingBookPath: t.value }); + break; + case 'cfg-tablebase': + updateDesktopState({ tablebasePath: t.value }); + break; + case 'fen-import': + fenInput.value = t.value; + break; + case 'analysis-depth': + analysisDepth.value = parseInt(t.value) || 30; + break; + case 'palette-query': + paletteQuery.value = t.value; + break; + } +}); + +appRoot.addEventListener('change', (e) => { + const t = e.target; + if (!(t instanceof HTMLInputElement)) return; + if (t.id === 'cfg-autostart') + updateDesktopState({ autoStartBackend: t.checked }); + if (t.hasAttribute('data-replay-slider')) { + void replayTo(parseInt(t.value)); + } +}); + +appRoot.addEventListener('dragover', (e) => { + const target = e.target; + if (!(target instanceof Element)) return; + if (target.closest('[data-drop-zone]')) { + e.preventDefault(); + importDropActive.value = true; + } +}); + +appRoot.addEventListener('dragleave', (e) => { + const target = e.target; + if (!(target instanceof Element)) return; + if (target.closest('[data-drop-zone]')) { + importDropActive.value = false; + } +}); + +appRoot.addEventListener('drop', (e) => { + const target = e.target; + if (!(target instanceof Element)) return; + if (!target.closest('[data-drop-zone]')) return; + + e.preventDefault(); + importDropActive.value = false; + const file = e.dataTransfer?.files?.[0]; + if (!file) return; + void file + .text() + .then((text) => importFenText(text)) + .catch((err) => showError((err as Error).message)); +}); + +async function handleAction(action: string, id?: string): Promise { + try { + if (action.startsWith('nav:')) { + const v = action.slice(4) as DesktopView; + currentView.value = v; + void persistViewSilently(); + if (v === 'games') void refreshGamesList(); + if (v === 'archive') void refreshArchive(); + if (v === 'analysis') void refreshAnalysisJobs(); + if (v === 'logs') void refreshLogs(); + if (v === 'dashboard') { + void refreshGamesList(); + void apiGetStorageStats() + .then((s) => (storageStats.value = s)) + .catch(() => {}); + } + return; + } + switch (action) { + case 'toggle-palette': + paletteOpen.value = !paletteOpen.value; + if (!paletteOpen.value) paletteQuery.value = ''; + break; + case 'create-game': + await createNewGame(); + break; + case 'import-fen': + await importFromFen(); + break; + case 'import-fen-file': + await importFenFromFile(); + break; + case 'open-game': + if (id) await openGame(id); + break; + case 'delete-game': + if (id) await doDeleteGame(id); + break; + case 'refresh-games': + await refreshGamesList(); + break; + case 'refresh-game': + if (activeGame.value) await openGame(activeGame.value.game_id); + break; + case 'flip-board': + updateDesktopState({ boardFlipped: !boardFlipped.value }); + break; + case 'resign': + await doAction('resign'); + break; + case 'offer-draw': + await doAction('offer_draw'); + break; + case 'claim-draw-threefold': + await doAction('claim_draw', 'threefold_repetition'); + break; + case 'claim-draw-fifty': + await doAction('claim_draw', 'fifty_move_rule'); + break; + case 'export-fen': + await doExportFen(); + break; + case 'save-fen': + await doSaveFen(); + break; + case 'export-pgn': + await doExportPgn(); + break; + case 'save-pgn': + await doSavePgn(); + break; + case 'refresh-archive': + await refreshArchive(); + break; + case 'replay-archived': + if (id) await openArchivedGame(id); + break; + case 'close-replay': + replayState.value = null; + replayGameId.value = null; + break; + case 'replay-start': + await replayTo(0); + break; + case 'replay-prev': + if (replayState.value) + await replayTo(Math.max(0, replayState.value.at_move - 1)); + break; + case 'replay-next': + if (replayState.value) + await replayTo( + Math.min( + replayState.value.total_moves, + replayState.value.at_move + 1 + ) + ); + break; + case 'replay-end': + if (replayState.value) await replayTo(replayState.value.total_moves); + break; + case 'analyze-game': + case 'analyze-archived': + if (id) { + await submitAnalysis(id); + currentView.value = 'analysis'; + } + break; + case 'refresh-analysis': + await refreshAnalysisJobs(); + break; + case 'view-analysis': + if (id) await pollAnalysisJob(id); + break; + case 'cancel-analysis': + if (id) await cancelAnalysis(id); + break; + case 'close-analysis': + activeAnalysis.value = null; + break; + case 'start-backend': + await startBackend(); + break; + case 'stop-backend': + await stopBackend(); + break; + case 'save-settings': + await saveDesktopSettings(); + break; + case 'save-preset': + saveCurrentPreset(); + break; + case 'load-preset': + if (id) loadPresetIntoState(id); + break; + case 'delete-preset': + if (id) removePresetFromState(id); + break; + case 'use-recent-workspace': + if (id) { + updateDesktopState({ backendWorkingDirectory: id }); + updateRecentWorkspaces(id); + showToast(`Workspace “${basename(id)}” selected`); + } + break; + case 'refresh-logs': + await refreshLogs(); + break; + case 'open-working-dir': { + const p = desktopState.value.backendWorkingDirectory.trim(); + if (p) await desktop.openPath(p); + break; + } + case 'pick-executable': { + const f = await desktop.pickFile(); + if (f) updateDesktopState({ backendExecutable: f }); + break; + } + case 'pick-working-directory': { + const d = await desktop.pickDirectory(); + if (d) updateDesktopState({ backendWorkingDirectory: d }); + break; + } + case 'pick-opening-book': { + const f = await desktop.pickFile(); + if (f) updateDesktopState({ openingBookPath: f }); + break; + } + case 'pick-tablebase': { + const d = await desktop.pickDirectory(); + if (d) updateDesktopState({ tablebasePath: d }); + break; + } + case 'check-updates': + updateStatus.value = await desktop.checkForUpdates(); + break; + case 'download-update': + updateStatus.value = await desktop.downloadUpdate(); + break; + case 'install-update': + await desktop.installUpdate(); + break; + case 'toggle-theme': { + const next = + desktopState.value.theme === 'dark' + ? 'light' + : desktopState.value.theme === 'light' + ? 'system' + : 'dark'; + updateDesktopState({ theme: next }); + syncTheme(next); + break; + } + case 'toggle-notifications': + updateDesktopState({ + notificationsEnabled: !desktopState.value.notificationsEnabled, + }); + break; + case 'toggle-developer-mode': + updateDesktopState({ + developerMode: !desktopState.value.developerMode, + }); + break; + case 'toggle-compact-mode': + updateDesktopState({ compactMode: !desktopState.value.compactMode }); + break; + } + } catch (err) { + showError((err as Error).message); + } +} + +// ── Keyboard shortcuts ────────────────────────────────────────────────────── + +window.addEventListener('keydown', (e) => { + const mod = e.metaKey || e.ctrlKey; + if (mod && e.key.toLowerCase() === 'k') { + e.preventDefault(); + paletteOpen.value = true; + paletteQuery.value = ''; + return; + } + if (mod && e.key.toLowerCase() === 'n') { + e.preventDefault(); + void createNewGame(); + return; + } + if (mod && e.key.toLowerCase() === 'f') { + e.preventDefault(); + updateDesktopState({ boardFlipped: !boardFlipped.value }); + return; + } + if (mod && /^[1-8]$/.test(e.key)) { + e.preventDefault(); + const views: DesktopView[] = [ + 'dashboard', + 'games', + 'board', + 'archive', + 'analysis', + 'engine', + 'logs', + 'settings', + ]; + currentView.value = views[parseInt(e.key) - 1]; + void persistViewSilently(); + return; + } + if (e.key === 'Escape') { + if (paletteOpen.value) { + paletteOpen.value = false; + paletteQuery.value = ''; + return; + } + selectedSquare.value = null; + } + if (e.key === 'Enter' && paletteOpen.value) { + const first = filteredPaletteActions.value[0]; + if (!first) return; + e.preventDefault(); + paletteOpen.value = false; + paletteQuery.value = ''; + void handleAction(first.id); + } +}); + +// ── Reactive rendering ────────────────────────────────────────────────────── + +let pendingFrame: number | null = null; +let pendingMarkup = ''; + +effect(() => { + pendingMarkup = renderShell(); + if (pendingFrame !== null) return; + pendingFrame = requestAnimationFrame(() => { + pendingFrame = null; + // Save focused input + const ae = document.activeElement; + let focusId = ''; + let selStart: number | null = null; + let selEnd: number | null = null; + if (ae instanceof HTMLInputElement && ae.id) { + focusId = ae.id; + selStart = ae.selectionStart; + selEnd = ae.selectionEnd; + } + const tpl = document.createElement('template'); + tpl.innerHTML = pendingMarkup; + appRoot.replaceChildren(tpl.content); + // Restore focus + if (focusId) { + const el = appRoot.querySelector( + `#${CSS.escape(focusId)}` + ); + if (el) { + el.focus({ preventScroll: true }); + if (selStart !== null && selEnd !== null) + el.setSelectionRange(selStart, selEnd); + } + } + }); +}); + +// ── Initialize ────────────────────────────────────────────────────────────── + +async function init(): Promise { + desktopState.value = await desktop.getState(); + currentView.value = desktopState.value.lastView; + backendStatus.value = await desktop.getBackendStatus(); + backendLogs.value = await desktop.getBackendLogs(); + updateStatus.value = await desktop.getUpdateStatus(); + setApiBase(desktopState.value.backendUrl); + syncTheme(desktopState.value.theme); + connectLiveUpdates(); + + if (desktopState.value.lastGameId) { + void refreshBoardAscii(desktopState.value.lastGameId).catch(() => {}); + } + + desktop.onBackendStatus((s) => { + backendStatus.value = s; + }); + desktop.onBackendLogs((l) => { + backendLogs.value = l; + }); + desktop.onUpdateStatus((s) => { + updateStatus.value = s; + }); + + // Initial data load + void refreshGamesList().catch(() => {}); + void apiGetStorageStats() + .then((s) => (storageStats.value = s)) + .catch(() => {}); + void refreshArchive().catch(() => {}); + void refreshAnalysisJobs().catch(() => {}); + + if (desktopState.value.lastGameId) { + void openGame( + desktopState.value.lastGameId, + currentView.value === 'board' + ).catch(() => {}); + } +} + +void init(); diff --git a/desktop/src/shared-types.ts b/desktop/src/shared-types.ts new file mode 100644 index 0000000..9db613d --- /dev/null +++ b/desktop/src/shared-types.ts @@ -0,0 +1,378 @@ +// ============================================================================ +// CheckAI Desktop — Shared Types +// ============================================================================ + +// ── Desktop shell views ───────────────────────────────────────────────────── + +export const DESKTOP_VIEWS = [ + 'dashboard', + 'games', + 'board', + 'archive', + 'analysis', + 'engine', + 'logs', + 'settings', +] as const; + +export type DesktopView = (typeof DESKTOP_VIEWS)[number]; + +export interface BackendPreset { + id: string; + name: string; + backendExecutable: string; + backendArgs: string; + backendWorkingDirectory: string; + backendUrl: string; + openingBookPath: string; + tablebasePath: string; + autoStartBackend: boolean; + createdAt: number; +} + +export interface SaveTextFileOptions { + defaultPath: string; + content: string; + filters?: Array<{ + name: string; + extensions: string[]; + }>; +} + +// ── Desktop state (persisted on disk) ─────────────────────────────────────── + +export interface DesktopState { + backendUrl: string; + autoStartBackend: boolean; + backendExecutable: string; + backendArgs: string; + backendWorkingDirectory: string; + openingBookPath: string; + tablebasePath: string; + lastView: DesktopView; + theme: 'dark' | 'light' | 'system'; + boardFlipped: boolean; + recentWorkspaces: string[]; + backendPresets: BackendPreset[]; + notificationsEnabled: boolean; + compactMode: boolean; + developerMode: boolean; + lastGameId: string | null; +} + +// ── Backend & update payloads ─────────────────────────────────────────────── + +export interface BackendStatusPayload { + running: boolean; + pid: number | null; + command: string | null; + startedAt: number | null; + exitCode: number | null; + lastError: string | null; +} + +export interface UpdateStatusPayload { + supported: boolean; + currentVersion: string; + state: + | 'idle' + | 'unsupported' + | 'checking' + | 'available' + | 'downloading' + | 'downloaded' + | 'up-to-date' + | 'error'; + availableVersion: string | null; + percent: number | null; + transferredBytes: number | null; + totalBytes: number | null; + message: string | null; +} + +// ── Desktop API exposed via preload ───────────────────────────────────────── + +export interface DesktopApi { + getState(): Promise; + saveState(state: DesktopState): Promise; + getBackendStatus(): Promise; + getBackendLogs(): Promise; + getUpdateStatus(): Promise; + setProgressBar(progress: number | null): Promise; + startBackend(state: DesktopState): Promise; + stopBackend(): Promise; + checkForUpdates(): Promise; + downloadUpdate(): Promise; + installUpdate(): Promise; + pickFile(): Promise; + pickDirectory(): Promise; + readTextFile(path: string): Promise; + saveTextFile(options: SaveTextFileOptions): Promise; + openPath(path: string): Promise; + openExternal(url: string): Promise; + notify(title: string, body: string): Promise; + onBackendStatus(callback: (status: BackendStatusPayload) => void): () => void; + onBackendLogs(callback: (logs: string) => void): () => void; + onUpdateStatus(callback: (status: UpdateStatusPayload) => void): () => void; + onMenuCommand(callback: (command: string) => void): () => void; +} + +export const DEFAULT_DESKTOP_STATE: DesktopState = { + backendUrl: 'http://127.0.0.1:8080', + autoStartBackend: false, + backendExecutable: 'checkai', + backendArgs: 'serve', + backendWorkingDirectory: '', + openingBookPath: '', + tablebasePath: '', + lastView: 'dashboard', + theme: 'dark', + boardFlipped: false, + recentWorkspaces: [], + backendPresets: [], + notificationsEnabled: true, + compactMode: false, + developerMode: false, + lastGameId: null, +}; + +// ── Engine API types ──────────────────────────────────────────────────────── + +export type PieceColor = 'white' | 'black'; +export type FenChar = + | 'K' + | 'Q' + | 'R' + | 'B' + | 'N' + | 'P' + | 'k' + | 'q' + | 'r' + | 'b' + | 'n' + | 'p'; +export type SquareName = string; +export type BoardMap = Partial>; + +export interface SideCastling { + kingside: boolean; + queenside: boolean; +} +export interface CastlingRights { + white: SideCastling; + black: SideCastling; +} + +export interface GameState { + board: BoardMap; + turn: PieceColor; + castling: CastlingRights; + en_passant: string | null; + halfmove_clock: number; + fullmove_number: number; + position_history: string[]; +} + +export interface LegalMove { + from: SquareName; + to: SquareName; + promotion?: string; +} + +export interface MoveHistoryEntry { + move_number: number; + side: PieceColor; + notation: string; + move_json: { from: SquareName; to: SquareName; promotion?: string }; +} + +export type GameResult = 'WhiteWins' | 'BlackWins' | 'Draw' | null; + +export type EndReason = + | 'Checkmate' + | 'Stalemate' + | 'ThreefoldRepetition' + | 'FivefoldRepetition' + | 'FiftyMoveRule' + | 'SeventyFiveMoveRule' + | 'InsufficientMaterial' + | 'Resignation' + | 'DrawAgreement'; + +export interface Game { + game_id: string; + state: GameState; + is_over: boolean; + is_check: boolean; + result: GameResult; + end_reason: EndReason | null; + legal_move_count: number; + move_history: MoveHistoryEntry[]; +} + +export interface GameSummary { + game_id: string; + turn: PieceColor; + fullmove_number: number; + is_over: boolean; + result: GameResult; +} + +export interface MoveResponse { + success: boolean; + message: string; + state: GameState; + is_over: boolean; + result: GameResult; + end_reason: EndReason | null; + is_check: boolean; +} + +export interface ArchivedGameSummary { + game_id: string; + result: GameResult; + end_reason: EndReason | null; + move_count: number; + compressed_bytes: number; + start_timestamp: number; + end_timestamp: number; + raw_bytes: number; +} + +export interface StorageStats { + active_count: number; + archived_count: number; + active_bytes: number; + archive_bytes: number; + total_bytes: number; +} + +export interface ReplayState { + game_id: string; + at_move: number; + total_moves: number; + state: GameState; + is_over: boolean; + result: GameResult; + is_check: boolean; +} + +// ── Analysis types ────────────────────────────────────────────────────────── + +export type AnalysisMoveQuality = + | 'Best' + | 'Excellent' + | 'Good' + | 'Inaccuracy' + | 'Mistake' + | 'Blunder' + | 'Book'; +export type AnalysisWdl = 'Win' | 'Draw' | 'Loss' | 'CursedWin' | 'BlessedLoss'; + +export interface AnalysisMoveJson { + from: SquareName; + to: SquareName; + promotion: string | null; +} + +export interface AnalysisBookMoveEntry { + notation: string; + weight: number; + probability: number; +} + +export interface AnalysisBookInfo { + is_book_move: boolean; + weight: number; + total_weight: number; + book_moves: AnalysisBookMoveEntry[]; + opening_name: string | null; +} + +export interface AnalysisTablebaseInfo { + is_tablebase_position: boolean; + wdl: AnalysisWdl | null; + dtz: number | null; + configuration: string; + source: string; +} + +export interface AnalysisMoveAnnotation { + move_number: number; + side: PieceColor; + played_move: AnalysisMoveJson; + best_move: AnalysisMoveJson; + played_eval: number; + best_eval: number; + centipawn_loss: number; + quality: AnalysisMoveQuality; + is_book_move: boolean; + is_tablebase_position: boolean; + book_info?: AnalysisBookInfo; + tablebase_info?: AnalysisTablebaseInfo; + search_depth: number; + principal_variation: string[]; +} + +export interface AnalysisMoveSummary { + total_moves: number; + best_moves: number; + excellent_moves: number; + good_moves: number; + inaccuracies: number; + mistakes: number; + blunders: number; + book_moves: number; + average_centipawn_loss: number; + white_accuracy: number; + black_accuracy: number; + white_avg_cp_loss: number; + black_avg_cp_loss: number; +} + +export interface AnalysisResultPayload { + annotations: AnalysisMoveAnnotation[]; + summary: AnalysisMoveSummary; + depth: number; + book_available: boolean; + tablebase_available: boolean; +} + +export type AnalysisStatus = + | 'Queued' + | 'Completed' + | 'Cancelled' + | { InProgress: { moves_analyzed: number; total_moves: number } } + | { Failed: { error: string } }; + +export interface AnalysisJob { + id: string; + game_id: string | null; + status: AnalysisStatus; + result?: AnalysisResultPayload; + created_at: number; + completed_at?: number | null; +} + +// ── Constants ─────────────────────────────────────────────────────────────── + +export const FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] as const; +export const RANKS = ['1', '2', '3', '4', '5', '6', '7', '8'] as const; +export const PROMOTION_PIECES = ['Q', 'R', 'B', 'N'] as const; +export const PROMOTION_PIECE_LIST = PROMOTION_PIECES.join(', '); + +export const PIECE_UNICODE: Record = { + K: '♔', + Q: '♕', + R: '♖', + B: '♗', + N: '♘', + P: '♙', + k: '♚', + q: '♛', + r: '♜', + b: '♝', + n: '♞', + p: '♟', +}; diff --git a/desktop/src/stores.ts b/desktop/src/stores.ts new file mode 100644 index 0000000..d5cb64b --- /dev/null +++ b/desktop/src/stores.ts @@ -0,0 +1,110 @@ +import { writable, derived, type Readable } from 'svelte/store'; +import { + DEFAULT_DESKTOP_STATE, + type DesktopState, + type DesktopView, + type BackendStatusPayload, + type UpdateStatusPayload, + type GameSummary, + type Game, + type LegalMove, + type ArchivedGameSummary, + type StorageStats, + type ReplayState, + type AnalysisJob, +} from './shared-types.js'; + +export type ConfirmModalState = { + kind: 'confirm'; + title: string; + message: string; + confirmLabel: string; + cancelLabel: string; + resolve: (value: boolean) => void; +}; + +export type PromptModalState = { + kind: 'prompt'; + title: string; + message: string; + confirmLabel: string; + cancelLabel: string; + initialValue: string; + placeholder?: string; + resolve: (value: string | null) => void; +}; + +export type ModalState = ConfirmModalState | PromptModalState; + +// Desktop state stores +export const desktopState = writable({ ...DEFAULT_DESKTOP_STATE }); +export const currentView = writable('dashboard'); +export const backendStatus = writable({ + running: false, + pid: null, + command: null, + startedAt: null, + exitCode: null, + lastError: null, +}); +export const backendLogs = writable(''); +export const updateStatus = writable({ + supported: false, + currentVersion: 'dev', + state: 'unsupported', + availableVersion: null, + percent: null, + transferredBytes: null, + totalBytes: null, + message: null, +}); + +// UI state stores +export const paletteOpen = writable(false); +export const paletteQuery = writable(''); +export const toastMsg = writable(null); +export const errorMsg = writable(null); +export const modalState = writable(null); +export const boardAscii = writable(''); + +// Engine data stores +export const gamesList = writable([]); +export const activeGame = writable(null); +export const legalMoves = writable([]); +export const selectedSquare = writable(null); +export const archivedList = writable([]); +export const storageStats = writable(null); +export const replayState = writable(null); +export const replayGameId = writable(null); +export const analysisJobs = writable([]); +export const activeAnalysis = writable(null); +export const fenInput = writable(''); +export const analysisDepth = writable(30); + +// Computed stores (derived) +export const boardFlipped: Readable = derived( + desktopState, + ($desktopState) => $desktopState.boardFlipped +); + +export const highlightSquares: Readable> = derived( + [selectedSquare, legalMoves], + ([$selectedSquare, $legalMoves]): Set => { + if (!$selectedSquare) return new Set(); + return new Set( + $legalMoves.filter((m) => m.from === $selectedSquare).map((m) => m.to) + ); + } +); + +export const lastMove: Readable<{ from: string; to: string } | null> = derived( + activeGame, + ($activeGame) => { + if (!$activeGame || $activeGame.move_history.length === 0) return null; + const last = $activeGame.move_history[$activeGame.move_history.length - 1]; + return { + from: last.move_json.from, + to: last.move_json.to, + }; + } +); diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss new file mode 100644 index 0000000..28f0148 --- /dev/null +++ b/desktop/src/styles.scss @@ -0,0 +1,2176 @@ +@import 'tailwindcss'; + +/* ══════════════════════════════════════════════════════════════════════════ + CheckAI Desktop · Enterprise Design System + ══════════════════════════════════════════════════════════════════════════ */ + +/* ── Tailwind v4 Theme Extension ───────────────────────────────────────── */ +@theme { + --font-body: + 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', Menlo, Consolas, monospace; + + --color-surface-0: #050a14; + --color-surface-1: rgba(8, 16, 32, 0.72); + --color-surface-2: rgba(15, 25, 50, 0.55); + --color-surface-glass: rgba(255, 255, 255, 0.035); + --color-border-subtle: rgba(255, 255, 255, 0.07); + --color-border-default: rgba(255, 255, 255, 0.1); + --color-border-strong: rgba(255, 255, 255, 0.16); + --color-text-primary: #f1f5f9; + --color-text-secondary: #94a3b8; + --color-text-muted: #64748b; + --color-accent: #38bdf8; + --color-accent-strong: #0ea5e9; + --color-accent-glow: rgba(56, 189, 248, 0.18); + --color-success: #34d399; + --color-danger: #f43f5e; + --color-warning: #fbbf24; + + --radius-sm: 0.5rem; + --radius-md: 0.75rem; + --radius-lg: 1rem; + --radius-xl: 1.25rem; + --radius-2xl: 1.5rem; + --radius-pill: 9999px; + + --shadow-glow-sm: 0 0 20px -4px rgba(56, 189, 248, 0.12); + --shadow-glow-md: 0 4px 32px -6px rgba(56, 189, 248, 0.18); + --shadow-glow-lg: 0 8px 48px -8px rgba(56, 189, 248, 0.22); + --shadow-elevation-1: + 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-elevation-2: + 0 4px 16px rgba(0, 0, 0, 0.3), 0 2px 6px rgba(0, 0, 0, 0.2); + --shadow-elevation-3: + 0 12px 40px rgba(0, 0, 0, 0.35), 0 4px 12px rgba(0, 0, 0, 0.2); + + --animate-duration-fast: 120ms; + --animate-duration-normal: 200ms; + --animate-duration-slow: 350ms; + --animate-ease: cubic-bezier(0.4, 0, 0.2, 1); + --animate-ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); +} + +/* ── Design Tokens ─────────────────────────────────────────────────────── */ + +:root, +[data-theme='dark'] { + color-scheme: dark; + --font-body: var(--font-body); + --font-mono: var(--font-mono); + --bg: var(--color-surface-0); + --bg-elevated: var(--color-surface-1); + --bg-card: var(--color-surface-2); + --bg-hover: rgba(255, 255, 255, 0.06); + --border: var(--color-border-default); + --border-strong: var(--color-border-strong); + --text: var(--color-text-primary); + --text-dim: var(--color-text-secondary); + --primary: var(--color-accent); + --primary-strong: var(--color-accent-strong); + --success: var(--color-success); + --danger: var(--color-danger); + --warning: var(--color-warning); + --shadow: var(--shadow-elevation-2); + --shadow-sm: var(--shadow-elevation-1); + --sq-light: #b0c4de; + --sq-dark: #3b5278; + --sq-sel: rgba(56, 189, 248, 0.45); + --sq-highlight: rgba(52, 211, 153, 0.45); + --sq-last-move: rgba(251, 191, 36, 0.28); + --sq-check: rgba(244, 63, 94, 0.55); +} + +[data-theme='light'] { + color-scheme: light; + --bg: #f8fafc; + --bg-elevated: #ffffff; + --bg-card: #ffffff; + --bg-hover: rgba(0, 0, 0, 0.04); + --border: rgba(148, 163, 184, 0.28); + --border-strong: rgba(100, 116, 139, 0.3); + --text: #0f172a; + --text-dim: #64748b; + --primary: #0284c7; + --primary-strong: #0369a1; + --success: #059669; + --danger: #dc2626; + --warning: #d97706; + --shadow: 0 4px 24px rgba(0, 0, 0, 0.06); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06); + --sq-light: #e2e8f0; + --sq-dark: #6482a8; + --sq-sel: rgba(2, 132, 199, 0.35); + --sq-highlight: rgba(5, 150, 105, 0.35); + --sq-last-move: rgba(217, 119, 6, 0.25); + --sq-check: rgba(220, 38, 38, 0.4); +} + +/* ── Global Reset ──────────────────────────────────────────────────────── */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; +} + +body { + font-family: var(--font-body); + color: var(--text); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: + radial-gradient( + ellipse 80% 60% at 10% 0%, + rgba(56, 189, 248, 0.12), + transparent + ), + radial-gradient( + ellipse 60% 50% at 90% 5%, + rgba(139, 92, 246, 0.09), + transparent + ), + radial-gradient( + ellipse 50% 40% at 50% 100%, + rgba(16, 185, 129, 0.06), + transparent + ), + linear-gradient(180deg, #030712 0%, #0a1628 50%, #06101e 100%); + background-attachment: fixed; +} + +[data-theme='light'] body { + background: + radial-gradient( + ellipse 80% 50% at 10% 0%, + rgba(56, 189, 248, 0.08), + transparent + ), + radial-gradient( + ellipse 60% 50% at 90% 5%, + rgba(124, 58, 237, 0.05), + transparent + ), + linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%); + background-attachment: fixed; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +code { + font-family: var(--font-mono); + font-size: 0.88em; +} + +.mono { + font-family: var(--font-mono); +} + +.dim { + color: var(--text-dim); + margin: 0; +} + +#app { + min-height: 100vh; +} + +/* ── Global custom scrollbar ───────────────────────────────────────────── */ + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.2); + border-radius: var(--radius-pill); +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(148, 163, 184, 0.35); +} + +/* ══════════════════════════════════════════════════════════════════════════ + LAYOUT · Shell + ══════════════════════════════════════════════════════════════════════════ */ + +.shell { + display: grid; + grid-template-columns: 270px minmax(0, 1fr); + min-height: 100vh; +} + +.shell.shell-compact .content { + padding: 1.25rem; +} + +.shell.shell-compact .card { + padding: 1.15rem; + border-radius: var(--radius-lg); +} + +/* ── Sidebar ───────────────────────────────────────────────────────────── */ + +.sidebar { + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; + padding: 1.25rem 1rem; + border-right: 1px solid var(--color-border-subtle); + background: rgba(5, 10, 25, 0.6); + backdrop-filter: blur(32px) saturate(1.4); + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +[data-theme='light'] .sidebar { + background: rgba(255, 255, 255, 0.82); + border-color: rgba(148, 163, 184, 0.3); +} + +.sidebar-top { + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.brand { + display: flex; + gap: 0.75rem; + align-items: center; + padding: 0.85rem 1rem; + border-radius: var(--radius-xl); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); + backdrop-filter: blur(12px); + box-shadow: var(--shadow-elevation-1); +} + +.brand-icon { + width: 2.25rem; + height: 2.25rem; + border-radius: var(--radius-md); + display: grid; + place-items: center; + background: linear-gradient(135deg, #38bdf8 0%, #2563eb 55%, #8b5cf6 100%); + color: #fff; + font-size: 1.15rem; + font-weight: 700; + box-shadow: 0 8px 24px rgba(56, 189, 248, 0.3); + flex-shrink: 0; +} + +.brand > div { + display: flex; + flex-direction: column; + min-width: 0; +} + +.brand h1 { + margin: 0; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--text); +} + +.brand p { + margin: 0; + font-size: 0.68rem; + color: var(--text-dim); + letter-spacing: 0.02em; +} + +.sidebar-workspace-card { + padding: 0.75rem 1rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.sidebar-workspace-card strong { + font-size: 0.88rem; + font-weight: 600; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-workspace-card p { + font-size: 0.72rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nav-btn-inline { + width: 100%; + margin-top: 0.55rem; +} + +/* Nav */ +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.sidebar-kicker, +.sidebar-section-label { + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--text-dim); + padding: 0.35rem 0.75rem 0.25rem; + margin-top: 0.25rem; +} + +.nav-btn { + display: flex; + align-items: center; + gap: 0.7rem; + text-align: left; + border: 1px solid transparent; + border-radius: var(--radius-lg); + background: transparent; + color: var(--text-dim); + padding: 0.55rem 0.85rem; + font-size: 0.82rem; + font-weight: 500; + transition: + background var(--animate-duration-fast) var(--animate-ease), + color var(--animate-duration-fast) var(--animate-ease), + border-color var(--animate-duration-fast) var(--animate-ease), + box-shadow var(--animate-duration-fast) var(--animate-ease), + transform var(--animate-duration-fast) var(--animate-ease); +} + +.nav-btn:hover { + background: rgba(255, 255, 255, 0.06); + border-color: var(--color-border-subtle); + color: var(--text); +} + +.nav-btn.active { + background: linear-gradient( + 100deg, + rgba(56, 189, 248, 0.14) 0%, + rgba(139, 92, 246, 0.1) 100% + ); + border-color: rgba(56, 189, 248, 0.2); + color: #fff; + font-weight: 600; + box-shadow: + var(--shadow-glow-sm), + inset 0 0 0 1px rgba(56, 189, 248, 0.05); +} + +[data-theme='light'] .nav-btn { + color: #475569; +} + +[data-theme='light'] .nav-btn:hover { + background: rgba(241, 245, 249, 0.8); + color: #0f172a; +} + +[data-theme='light'] .nav-btn.active { + background: linear-gradient( + 100deg, + rgba(14, 165, 233, 0.1), + rgba(124, 58, 237, 0.06) + ); + border-color: rgba(14, 165, 233, 0.22); + color: #0f172a; + box-shadow: 0 0 16px rgba(14, 165, 233, 0.08); +} + +.nav-icon { + font-size: 1rem; + min-width: 1.25rem; + text-align: center; + line-height: 1; +} + +/* Sidebar footer */ +.sidebar-footer { + margin-top: auto; + display: flex; + align-items: flex-start; + gap: 0.65rem; + font-size: 0.78rem; + color: var(--text-dim); + padding: 0.85rem 1rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); +} + +.sidebar-footer strong { + display: block; + font-size: 0.85rem; + font-weight: 600; + color: var(--text); +} + +.sidebar-footer p { + font-size: 0.72rem; + line-height: 1.35; +} + +[data-theme='light'] .sidebar-footer strong { + color: #0f172a; +} + +.status-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + flex-shrink: 0; + margin-top: 0.3rem; +} + +.status-dot.online { + background: var(--success); + box-shadow: + 0 0 10px rgba(52, 211, 153, 0.6), + 0 0 3px rgba(52, 211, 153, 0.9); + animation: pulse-glow 2s ease-in-out infinite; +} + +.status-dot.offline { + background: var(--danger); + box-shadow: 0 0 8px rgba(244, 63, 94, 0.4); +} + +@keyframes pulse-glow { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +/* ── Content Area ──────────────────────────────────────────────────────── */ + +.content { + padding: 1.75rem 2.25rem; + overflow-y: auto; + max-height: 100vh; + position: relative; +} + +/* ── Topbar ────────────────────────────────────────────────────────────── */ + +.topbar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1.5rem; + margin-bottom: 2rem; + padding: 1.25rem 1.5rem; + border-radius: var(--radius-2xl); + border: 1px solid var(--color-border-subtle); + background: rgba(5, 10, 25, 0.4); + backdrop-filter: blur(32px) saturate(1.3); + box-shadow: var(--shadow-elevation-2); + position: sticky; + top: 0; + z-index: 30; +} + +[data-theme='light'] .topbar { + background: rgba(255, 255, 255, 0.85); + border-color: rgba(148, 163, 184, 0.25); +} + +.topbar-copy { + display: flex; + flex-direction: column; + gap: 0.65rem; + min-width: 0; +} + +.topbar-kicker { + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--color-accent); +} + +[data-theme='light'] .topbar-kicker { + color: var(--primary-strong); +} + +.topbar h1 { + margin: 0; + font-size: 1.65rem; + font-weight: 700; + letter-spacing: -0.025em; + line-height: 1.1; + color: var(--text); + background: linear-gradient(135deg, #f1f5f9 20%, #94a3b8 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +[data-theme='light'] .topbar h1 { + background: linear-gradient(135deg, #0f172a 20%, #475569 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.topbar-meta { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; +} + +.meta-chip { + display: flex; + flex-direction: column; + gap: 0.1rem; + min-width: 8rem; + padding: 0.5rem 0.85rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); +} + +.meta-chip-label { + font-size: 0.58rem; + font-weight: 700; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--text-dim); +} + +.meta-chip strong { + display: block; + font-size: 0.82rem; + font-weight: 600; + color: var(--text); +} + +.meta-chip-strong { + border-color: rgba(52, 211, 153, 0.22); + background: rgba(52, 211, 153, 0.06); +} + +[data-theme='light'] .meta-chip { + background: rgba(255, 255, 255, 0.9); + border-color: rgba(148, 163, 184, 0.2); +} + +[data-theme='light'] .meta-chip strong { + color: #0f172a; +} + +.topbar-actions { + display: flex; + gap: 0.65rem; + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; + flex-shrink: 0; +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Cards + ══════════════════════════════════════════════════════════════════════════ */ + +.card { + background: rgba(8, 16, 32, 0.5); + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-elevation-2); + backdrop-filter: blur(24px) saturate(1.2); + padding: 1.5rem; + margin-bottom: 1.5rem; + transition: + border-color var(--animate-duration-normal) var(--animate-ease), + box-shadow var(--animate-duration-normal) var(--animate-ease); +} + +.card:last-child { + margin-bottom: 0; +} + +[data-theme='light'] .card { + background: rgba(255, 255, 255, 0.85); + border-color: rgba(148, 163, 184, 0.22); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.05); +} + +.card-head { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + margin-bottom: 1.35rem; +} + +.card-head h2, +.card-head h3 { + margin: 0; + font-weight: 650; + color: var(--text); + letter-spacing: -0.01em; +} + +.card-head h2 { + font-size: 1.05rem; +} + +.card-head h3 { + font-size: 0.95rem; +} + +.card-head-spaced { + margin-top: 1rem; +} + +/* Hero card — full-width gradient header */ +.hero-card { + position: relative; + overflow: hidden; + background: + linear-gradient( + 145deg, + rgba(5, 10, 25, 0.8) 0%, + rgba(14, 116, 144, 0.2) 50%, + rgba(56, 189, 248, 0.08) 100% + ), + rgba(8, 16, 32, 0.6); + border-color: rgba(56, 189, 248, 0.12); +} + +.hero-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent 5%, + rgba(56, 189, 248, 0.7) 50%, + transparent 95% + ); +} + +.hero-card::after { + content: ''; + position: absolute; + top: -50%; + right: -10%; + width: 45%; + height: 200%; + background: radial-gradient( + ellipse, + rgba(56, 189, 248, 0.04), + transparent 65% + ); + pointer-events: none; +} + +[data-theme='light'] .hero-card { + background: linear-gradient( + 145deg, + #ffffff 0%, + rgba(224, 242, 254, 0.9) 50%, + rgba(245, 243, 255, 0.95) 100% + ); + border-color: rgba(14, 165, 233, 0.15); +} + +[data-theme='light'] .hero-card::before { + background: linear-gradient( + 90deg, + transparent 5%, + rgba(14, 165, 233, 0.4) 50%, + transparent 95% + ); +} + +.hero-card .card-head h2 { + font-size: 1.2rem; +} + +.hero-meta { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.85rem; + margin-bottom: 1.5rem; +} + +.hero-stat { + padding: 0.85rem 1rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); + display: flex; + flex-direction: column; + gap: 0.2rem; + transition: + border-color var(--animate-duration-fast) var(--animate-ease), + background var(--animate-duration-fast) var(--animate-ease); +} + +.hero-stat:hover { + border-color: var(--color-border-strong); + background: rgba(255, 255, 255, 0.06); +} + +[data-theme='light'] .hero-stat { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(148, 163, 184, 0.2); +} + +/* Empty states */ +.empty-text { + color: var(--text-dim); + font-size: 0.85rem; +} + +.empty-card { + min-height: 10rem; + display: grid; + place-items: center; + text-align: center; + border: 1px dashed var(--border-strong); + background: transparent; + box-shadow: none; +} + +/* ── View Grid ─────────────────────────────────────────────────────────── */ + +.view-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); + gap: 1.35rem; + align-items: start; +} + +.view-grid > .hero-card { + grid-column: 1 / -1; +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Buttons + ══════════════════════════════════════════════════════════════════════════ */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + border: 1px solid var(--color-border-default); + background: var(--color-surface-glass); + backdrop-filter: blur(8px); + color: var(--text); + border-radius: var(--radius-lg); + padding: 0.55rem 1rem; + font-size: 0.82rem; + font-weight: 550; + box-shadow: var(--shadow-elevation-1); + transition: + background var(--animate-duration-fast) var(--animate-ease), + border-color var(--animate-duration-fast) var(--animate-ease), + box-shadow var(--animate-duration-fast) var(--animate-ease), + transform var(--animate-duration-fast) var(--animate-ease), + color var(--animate-duration-fast) var(--animate-ease); + white-space: nowrap; +} + +.btn:hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--color-border-strong); + transform: translateY(-1px); + box-shadow: var(--shadow-elevation-2); +} + +.btn:active { + transform: translateY(0); + box-shadow: var(--shadow-elevation-1); +} + +[data-theme='light'] .btn { + background: rgba(255, 255, 255, 0.9); + border-color: rgba(148, 163, 184, 0.28); +} + +[data-theme='light'] .btn:hover { + background: #f1f5f9; + border-color: rgba(148, 163, 184, 0.4); +} + +.btn-primary { + background: linear-gradient(135deg, #0ea5e9 0%, #2563eb 100%); + color: #fff; + border-color: transparent; + box-shadow: var(--shadow-glow-md); +} + +.btn-primary:hover { + background: linear-gradient(135deg, #38bdf8 0%, #3b82f6 100%); + color: #fff; + box-shadow: var(--shadow-glow-lg); +} + +[data-theme='light'] .btn-primary { + background: linear-gradient(135deg, #0284c7 0%, #1d4ed8 100%); + color: #fff; +} + +[data-theme='light'] .btn-primary:hover { + background: linear-gradient(135deg, #0ea5e9 0%, #2563eb 100%); +} + +.btn-ghost { + background: transparent; + border-color: transparent; + box-shadow: none; + backdrop-filter: none; +} + +.btn-ghost:hover { + background: rgba(255, 255, 255, 0.06); + border-color: var(--color-border-subtle); +} + +[data-theme='light'] .btn-ghost { + background: transparent; +} + +[data-theme='light'] .btn-ghost:hover { + background: rgba(241, 245, 249, 0.9); + border-color: rgba(148, 163, 184, 0.22); +} + +.btn-danger { + color: #fff; + background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%); + border-color: transparent; + box-shadow: 0 4px 16px rgba(244, 63, 94, 0.2); +} + +.btn-danger:hover { + background: linear-gradient(135deg, #fb7185 0%, #f43f5e 100%); + box-shadow: 0 6px 24px rgba(244, 63, 94, 0.3); +} + +.btn-sm { + padding: 0.35rem 0.7rem; + font-size: 0.78rem; + border-radius: var(--radius-md); +} + +.btn-row { + display: flex; + gap: 0.5rem; + align-items: center; + flex-wrap: wrap; +} + +.btn-row > input, +.btn-row > .field-inline { + flex: 1 1 14rem; +} + +/* ── Quick Strip (Dashboard) ───────────────────────────────────────────── */ + +.quick-strip { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(185px, 1fr)); + gap: 0.65rem; + margin-top: 1.5rem; +} + +.qbtn { + text-align: left; + padding: 1rem 1.1rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); + color: var(--text); + display: flex; + flex-direction: column; + gap: 0.3rem; + box-shadow: var(--shadow-elevation-1); + transition: + background var(--animate-duration-fast) var(--animate-ease), + border-color var(--animate-duration-fast) var(--animate-ease), + box-shadow var(--animate-duration-normal) var(--animate-ease), + transform var(--animate-duration-normal) var(--animate-ease); + position: relative; + overflow: hidden; +} + +.qbtn::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(56, 189, 248, 0.04), transparent); + opacity: 0; + transition: opacity var(--animate-duration-normal) var(--animate-ease); +} + +.qbtn:hover { + border-color: rgba(56, 189, 248, 0.18); + background: rgba(255, 255, 255, 0.06); + box-shadow: var(--shadow-glow-sm), var(--shadow-elevation-2); + transform: translateY(-2px); +} + +.qbtn:hover::before { + opacity: 1; +} + +[data-theme='light'] .qbtn { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(148, 163, 184, 0.2); +} + +[data-theme='light'] .qbtn:hover { + background: #f0f9ff; + border-color: rgba(14, 165, 233, 0.2); +} + +.qbtn strong { + font-weight: 600; + font-size: 0.88rem; + position: relative; +} + +.qbtn span { + font-size: 0.75rem; + color: var(--text-dim); + position: relative; + line-height: 1.4; +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Badges + ══════════════════════════════════════════════════════════════════════════ */ + +.badge { + display: inline-flex; + align-items: center; + padding: 0.22rem 0.65rem; + border-radius: var(--radius-pill); + font-size: 0.65rem; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.12em; + border: 1px solid var(--color-border-default); +} + +.badge-ok { + color: var(--success); + border-color: rgba(52, 211, 153, 0.28); + background: rgba(52, 211, 153, 0.08); + box-shadow: 0 0 12px rgba(52, 211, 153, 0.08); +} + +.badge-dim { + color: var(--text-dim); + background: var(--color-surface-glass); +} + +.badge-active { + color: var(--warning); + border-color: rgba(251, 191, 36, 0.28); + background: rgba(251, 191, 36, 0.08); +} + +.badge-danger { + color: var(--danger); + border-color: rgba(244, 63, 94, 0.28); + background: rgba(244, 63, 94, 0.08); +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Stats + ══════════════════════════════════════════════════════════════════════════ */ + +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 0.85rem; +} + +.stat { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.stat-label { + font-size: 0.62rem; + text-transform: uppercase; + color: var(--text-dim); + font-weight: 650; + letter-spacing: 0.08em; +} + +.stat strong { + font-size: 1.05rem; + font-weight: 650; + color: var(--text); +} + +/* ── Mini List ─────────────────────────────────────────────────────────── */ + +.mini-list { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.mini-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.65rem 1rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); + font-size: 0.85rem; + transition: + background var(--animate-duration-fast) var(--animate-ease), + border-color var(--animate-duration-fast) var(--animate-ease), + transform var(--animate-duration-fast) var(--animate-ease), + box-shadow var(--animate-duration-fast) var(--animate-ease); +} + +.mini-item:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(56, 189, 248, 0.15); + transform: translateY(-1px); + box-shadow: var(--shadow-glow-sm); +} + +[data-theme='light'] .mini-item { + background: rgba(255, 255, 255, 0.85); + border-color: rgba(148, 163, 184, 0.18); +} + +[data-theme='light'] .mini-item:hover { + background: #f0f9ff; +} + +.mini-item .mono.dim { + max-width: 18rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Data Tables + ══════════════════════════════════════════════════════════════════════════ */ + +.table-wrap { + overflow-x: auto; + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-lg); + background: var(--color-surface-glass); + backdrop-filter: blur(12px); +} + +.table-wrap-spaced { + margin-top: 1rem; +} + +[data-theme='light'] .table-wrap { + background: rgba(255, 255, 255, 0.9); + border-color: rgba(148, 163, 184, 0.22); +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; +} + +.data-table thead th { + text-align: left; + padding: 0.7rem 1rem; + font-size: 0.68rem; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-dim); + border-bottom: 1px solid var(--color-border-subtle); + background: rgba(255, 255, 255, 0.02); +} + +[data-theme='light'] .data-table thead th { + background: rgba(248, 250, 252, 0.8); +} + +.data-table tbody td { + padding: 0.7rem 1rem; + border-bottom: 1px solid var(--color-border-subtle); + color: var(--text); +} + +.data-table tbody tr { + transition: background var(--animate-duration-fast) var(--animate-ease); +} + +.data-table tbody tr:hover { + background: rgba(56, 189, 248, 0.04); +} + +[data-theme='light'] .data-table tbody tr:hover { + background: rgba(239, 246, 255, 0.6); +} + +.data-table tbody tr:last-child td { + border-bottom: none; +} + +.data-table.compact td, +.data-table.compact th { + padding: 0.5rem 0.75rem; +} + +.selected-row { + background: rgba(56, 189, 248, 0.08); +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Form fields + ══════════════════════════════════════════════════════════════════════════ */ + +.field { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-bottom: 1.15rem; +} + +.field > span { + font-size: 0.75rem; + font-weight: 600; + color: var(--text); + letter-spacing: 0.02em; +} + +.field input, +.field select, +.field-inline input { + min-width: 0; + padding: 0.55rem 0.85rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-default); + background: rgba(255, 255, 255, 0.04); + color: var(--text); + font-size: 0.85rem; + box-shadow: var(--shadow-elevation-1); + transition: + border-color var(--animate-duration-fast) var(--animate-ease), + box-shadow var(--animate-duration-fast) var(--animate-ease), + background var(--animate-duration-fast) var(--animate-ease); +} + +[data-theme='light'] .field input, +[data-theme='light'] .field select, +[data-theme='light'] .field-inline input { + background: rgba(255, 255, 255, 0.95); + border-color: rgba(148, 163, 184, 0.28); +} + +.field input:focus, +.field select:focus, +.field-inline input:focus { + outline: none; + border-color: rgba(56, 189, 248, 0.5); + box-shadow: + 0 0 0 3px rgba(56, 189, 248, 0.1), + var(--shadow-elevation-1); + background: rgba(255, 255, 255, 0.06); +} + +[data-theme='light'] .field input:focus, +[data-theme='light'] .field select:focus, +[data-theme='light'] .field-inline input:focus { + border-color: rgba(14, 165, 233, 0.5); + box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.08); +} + +.field input::placeholder, +.field-inline input::placeholder { + color: var(--text-dim); + opacity: 0.5; +} + +.field-error { + margin: 0; + font-size: 0.75rem; + color: var(--color-danger); +} + +.input-with-btn { + display: flex; + gap: 0.5rem; +} + +.input-with-btn input { + flex: 1; +} + +.field-inline { + display: flex; + gap: 0.75rem; + align-items: center; + font-size: 0.85rem; +} + +.field-inline input { + flex: 1; +} + +.fen-row { + display: flex; + gap: 0.75rem; + align-items: center; + margin-bottom: 1.5rem; +} + +.fen-row .field-inline { + flex: 1; +} + +.checkbox-field { + flex-direction: row; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.checkbox-field input[type='checkbox'] { + width: 1.05rem; + height: 1.05rem; + accent-color: var(--color-accent); + border-radius: 3px; +} + +.drop-zone { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 2rem; + border-radius: var(--radius-xl); + border: 2px dashed var(--color-border-strong); + background: rgba(255, 255, 255, 0.02); + margin-bottom: 1.5rem; + transition: + border-color var(--animate-duration-normal) var(--animate-ease), + background var(--animate-duration-normal) var(--animate-ease), + box-shadow var(--animate-duration-normal) var(--animate-ease); +} + +.drop-zone-active { + border-color: var(--color-accent); + background: rgba(56, 189, 248, 0.06); + box-shadow: + inset 0 0 24px rgba(56, 189, 248, 0.06), + var(--shadow-glow-sm); +} + +/* ── Callout ───────────────────────────────────────────────────────────── */ + +.callout { + display: flex; + gap: 0.75rem; + padding: 0.85rem 1rem; + border-radius: var(--radius-lg); + background: var(--color-surface-glass); + border: 1px solid var(--color-border-subtle); + border-left: 3px solid var(--color-accent); + font-size: 0.85rem; + margin-top: 0.75rem; + color: var(--text); + backdrop-filter: blur(8px); +} + +.callout-danger { + border-left-color: var(--danger); + background: rgba(244, 63, 94, 0.04); +} + +[data-theme='light'] .callout { + background: rgba(248, 250, 252, 0.9); +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Toast Notifications + ══════════════════════════════════════════════════════════════════════════ */ + +.toast { + position: sticky; + top: 5.5rem; + z-index: 100; + padding: 0.7rem 1rem; + border-radius: var(--radius-lg); + font-size: 0.82rem; + margin-bottom: 1rem; + background: rgba(8, 16, 32, 0.85); + border: 1px solid var(--color-border-subtle); + box-shadow: var(--shadow-elevation-3); + backdrop-filter: blur(24px) saturate(1.4); + animation: toast-in 280ms var(--animate-ease-bounce); + display: flex; + align-items: center; + gap: 0.5rem; +} + +[data-theme='light'] .toast { + background: rgba(255, 255, 255, 0.96); + border-color: rgba(148, 163, 184, 0.25); +} + +.toast-ok { + border-left: 3px solid var(--success); +} + +.toast-err { + border-left: 3px solid var(--danger); +} + +@keyframes toast-in { + from { + transform: translateY(-12px) scale(0.97); + opacity: 0; + } + to { + transform: translateY(0) scale(1); + opacity: 1; + } +} + +/* ══════════════════════════════════════════════════════════════════════════ + CHESS · Board + ══════════════════════════════════════════════════════════════════════════ */ + +.board-layout { + display: grid; + grid-template-columns: minmax(400px, 640px) minmax(280px, 1fr); + gap: 1.35rem; + align-items: start; +} + +.board-main { + min-width: 0; +} + +.board-sidebar { + display: flex; + flex-direction: column; + gap: 1.35rem; +} + +.board-card { + margin-bottom: 0; +} + +.board-container { + margin: 1rem 0; + user-select: none; + padding: 0.85rem; + border-radius: var(--radius-2xl); + border: 1px solid var(--color-border-subtle); + background: rgba(0, 0, 0, 0.2); + box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.15); +} + +[data-theme='light'] .board-container { + background: rgba(226, 232, 240, 0.35); + border-color: rgba(148, 163, 184, 0.25); +} + +.chess-board { + display: grid; + gap: 0; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25); +} + +[data-theme='light'] .chess-board { + border-color: rgba(148, 163, 184, 0.3); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +.board-row { + display: grid; + grid-template-columns: 1.5rem repeat(8, 1fr); +} + +.rank-label { + display: grid; + place-items: center; + font-size: 0.65rem; + font-weight: 600; + color: var(--text-dim); + padding-right: 0.25rem; +} + +.file-labels { + display: grid; + grid-template-columns: 1.5rem repeat(8, 1fr); +} + +.file-labels > span { + text-align: center; + font-size: 0.65rem; + font-weight: 600; + color: var(--text-dim); + padding-top: 0.3rem; +} + +.sq { + aspect-ratio: 1; + display: grid; + place-items: center; + font-size: clamp(1.6rem, 4vw, 2.65rem); + line-height: 1; + user-select: none; + position: relative; + border: none; + padding: 0; + transition: + background-color 80ms ease, + filter 80ms ease, + box-shadow 80ms ease; +} + +.sq-light { + background: var(--sq-light); + color: #1e293b; +} + +.sq-dark { + background: var(--sq-dark); + color: #f1f5f9; +} + +.sq-selected { + background: var(--sq-sel) !important; + box-shadow: inset 0 0 0 2px rgba(56, 189, 248, 0.5); +} + +.sq-highlight::after { + content: ''; + position: absolute; + inset: 33%; + border-radius: 50%; + background: var(--sq-highlight); + box-shadow: 0 0 8px var(--sq-highlight); +} + +.sq-last-move { + background-color: var(--sq-last-move) !important; +} + +.sq-check { + box-shadow: inset 0 0 0 2.5px var(--sq-check); + background-color: rgba(244, 63, 94, 0.12); + animation: check-pulse 1.2s ease-in-out infinite; +} + +@keyframes check-pulse { + 0%, + 100% { + box-shadow: inset 0 0 0 2.5px var(--sq-check); + } + 50% { + box-shadow: + inset 0 0 0 2.5px var(--sq-check), + inset 0 0 12px rgba(244, 63, 94, 0.15); + } +} + +/* ── Action Bar ────────────────────────────────────────────────────────── */ + +.action-bar { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; + margin-top: 1rem; +} + +/* ── Move List ─────────────────────────────────────────────────────────── */ + +.move-list-card { + max-height: 30rem; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.move-list-card .card-head { + flex-shrink: 0; +} + +.move-list { + overflow-y: auto; + flex: 1; + min-height: 0; + padding-right: 0.35rem; +} + +.moves { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 0.15rem; +} + +.moves li { + display: flex; + gap: 0.5rem; + padding: 0.4rem 0.75rem; + border-radius: var(--radius-md); + font-size: 0.82rem; + font-family: var(--font-mono); + border: 1px solid transparent; + transition: + background var(--animate-duration-fast) var(--animate-ease), + border-color var(--animate-duration-fast) var(--animate-ease); +} + +.moves li:hover { + background: rgba(255, 255, 255, 0.04); + border-color: var(--color-border-subtle); +} + +.move-num { + color: var(--text-dim); + min-width: 2rem; + text-align: right; + user-select: none; +} + +.move-w, +.move-b { + min-width: 4rem; +} + +/* ── Replay ────────────────────────────────────────────────────────────── */ + +.replay-slider { + width: 100%; + margin-top: 1rem; + accent-color: var(--color-accent); + height: 4px; +} + +.analysis-progress { + width: 100%; + margin-bottom: 1rem; +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Analysis + ══════════════════════════════════════════════════════════════════════════ */ + +.analysis-summary { + margin-bottom: 1.5rem; +} + +.accuracy-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.85rem; + margin-bottom: 1.5rem; +} + +.accuracy-block { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 1rem 1.15rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); + backdrop-filter: blur(8px); + transition: border-color var(--animate-duration-fast) var(--animate-ease); +} + +.accuracy-block:hover { + border-color: var(--color-border-strong); +} + +[data-theme='light'] .accuracy-block { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(148, 163, 184, 0.2); +} + +.accuracy-label { + font-size: 0.62rem; + text-transform: uppercase; + color: var(--text-dim); + font-weight: 650; + letter-spacing: 0.08em; +} + +.accuracy-value { + font-size: 1.65rem; + font-weight: 700; + letter-spacing: -0.02em; + background: linear-gradient( + 135deg, + var(--text) 30%, + var(--color-accent) 100% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.quality-bar { + margin-bottom: 1.5rem; +} + +.qbar { + display: flex; + height: 7px; + border-radius: var(--radius-pill); + overflow: hidden; + background: rgba(255, 255, 255, 0.06); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.qbar-seg { + min-width: 2px; + transition: flex 400ms var(--animate-ease); +} + +.quality-dot { + font-weight: 600; + font-size: 0.78rem; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.quality-dot.quality-good { + color: var(--success); +} + +.quality-dot.quality-info { + color: var(--primary); +} + +.quality-dot.quality-warning { + color: var(--warning); +} + +.quality-dot.quality-danger { + color: var(--danger); +} + +.annotation-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.annotation-list h3 { + margin: 0 0 0.5rem; + font-size: 0.78rem; + text-transform: uppercase; + color: var(--text-dim); + letter-spacing: 0.08em; + font-weight: 650; +} + +/* ── Code/Log panels ───────────────────────────────────────────────────── */ + +.ascii-panel, +.json-panel { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + padding: 1rem; + border-radius: var(--radius-lg); + background: rgba(2, 6, 23, 0.5); + border: 1px solid var(--color-border-subtle); + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.65; + overflow: auto; + backdrop-filter: blur(8px); +} + +[data-theme='light'] .ascii-panel, +[data-theme='light'] .json-panel { + background: rgba(248, 250, 252, 0.95); +} + +.ascii-panel { + min-height: 14rem; +} + +.json-panel { + max-height: 24rem; +} + +.details-panel { + margin-top: 1rem; + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-lg); + padding: 0 1rem; + background: var(--color-surface-glass); + backdrop-filter: blur(8px); +} + +.details-panel summary { + cursor: pointer; + color: var(--text); + font-weight: 550; + padding: 1rem 0; + user-select: none; + font-size: 0.85rem; + transition: color var(--animate-duration-fast) var(--animate-ease); +} + +.details-panel summary:hover { + color: var(--color-accent); +} + +.details-panel > *:not(summary) { + padding-bottom: 1rem; +} + +/* ── Log panel ─────────────────────────────────────────────────────────── */ + +.log-panel { + margin: 0; + min-height: 20rem; + max-height: 60vh; + overflow: auto; + white-space: pre-wrap; + word-break: break-all; + padding: 1rem; + border-radius: var(--radius-lg); + background: rgba(2, 6, 23, 0.5); + border: 1px solid var(--color-border-subtle); + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.65; + backdrop-filter: blur(8px); +} + +[data-theme='light'] .log-panel { + background: rgba(248, 250, 252, 0.95); +} + +.logs-grid { + grid-template-columns: minmax(300px, 0.8fr) minmax(0, 1.4fr); +} + +.logs-grid > .card:last-child { + grid-column: 1 / -1; +} + +/* ── Presets ────────────────────────────────────────────────────────────── */ + +.preset-list { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.preset-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.85rem 1rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-subtle); + background: var(--color-surface-glass); + transition: + background var(--animate-duration-fast) var(--animate-ease), + border-color var(--animate-duration-fast) var(--animate-ease), + transform var(--animate-duration-fast) var(--animate-ease); +} + +.preset-card:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(56, 189, 248, 0.15); + transform: translateY(-1px); +} + +[data-theme='light'] .preset-card { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(148, 163, 184, 0.2); +} + +[data-theme='light'] .preset-card:hover { + background: #f0f9ff; +} + +.preset-card h3 { + margin: 0 0 0.2rem 0; + font-size: 0.88rem; + font-weight: 600; +} + +.preset-card p { + margin: 0; + color: var(--text-dim); + font-size: 0.75rem; +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Settings + ══════════════════════════════════════════════════════════════════════════ */ + +.shortcut-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0; +} + +.shortcut-list li { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.85rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--color-border-subtle); +} + +.shortcut-list li:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.shortcut-list kbd { + display: inline-block; + padding: 0.25rem 0.55rem; + border-radius: var(--radius-sm); + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--color-border-default); + font-family: var(--font-mono); + font-size: 0.72rem; + white-space: nowrap; + color: var(--text); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); +} + +[data-theme='light'] .shortcut-list kbd { + background: rgba(241, 245, 249, 0.9); + border-color: rgba(148, 163, 184, 0.3); +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Progress Bar + ══════════════════════════════════════════════════════════════════════════ */ + +.progress-bar { + height: 5px; + border-radius: var(--radius-pill); + background: rgba(255, 255, 255, 0.06); + overflow: hidden; + margin: 0.75rem 0; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.progress-fill { + height: 100%; + background: linear-gradient( + 90deg, + var(--color-accent-strong), + var(--color-accent) + ); + border-radius: var(--radius-pill); + transition: width 400ms var(--animate-ease); + box-shadow: 0 0 12px rgba(56, 189, 248, 0.3); +} + +/* ══════════════════════════════════════════════════════════════════════════ + COMPONENTS · Command Palette + ══════════════════════════════════════════════════════════════════════════ */ + +.overlay { + position: fixed; + inset: 0; + z-index: 500; + display: grid; + place-items: center; + background: rgba(2, 6, 23, 0.6); + backdrop-filter: blur(8px); + animation: overlay-in 180ms var(--animate-ease); +} + +@keyframes overlay-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.palette { + width: min(640px, calc(100vw - 2rem)); + background: rgba(8, 16, 32, 0.9); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-2xl); + box-shadow: + var(--shadow-elevation-3), + 0 0 0 1px rgba(56, 189, 248, 0.06); + overflow: hidden; + display: flex; + flex-direction: column; + backdrop-filter: blur(32px) saturate(1.4); + animation: palette-in 250ms var(--animate-ease-bounce); +} + +@keyframes palette-in { + from { + transform: translateY(-16px) scale(0.96); + opacity: 0; + } + to { + transform: translateY(0) scale(1); + opacity: 1; + } +} + +[data-theme='light'] .palette { + background: rgba(255, 255, 255, 0.96); + border-color: rgba(148, 163, 184, 0.3); +} + +.palette-head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.85rem 1.25rem; + border-bottom: 1px solid var(--color-border-subtle); + background: rgba(255, 255, 255, 0.03); +} + +[data-theme='light'] .palette-head { + background: rgba(248, 250, 252, 0.9); + border-bottom-color: rgba(148, 163, 184, 0.2); +} + +.palette-head h3 { + margin: 0; + font-size: 0.88rem; + font-weight: 600; +} + +.palette-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0; + padding: 0.5rem; + max-height: 60vh; + overflow-y: auto; +} + +.palette-search { + margin: 0 0.5rem 0.5rem; +} + +.modal-overlay { + z-index: 70; +} + +.modal-card { + width: min(32rem, calc(100vw - 2rem)); +} + +.modal-field { + margin: 0.5rem; +} + +.modal-actions { + justify-content: flex-end; + padding: 0.5rem; +} + +.palette-btn { + text-align: left; + padding: 0.7rem 0.85rem; + border-radius: var(--radius-md); + background: transparent; + border: 1px solid transparent; + color: var(--text); + font-size: 0.82rem; + transition: + background var(--animate-duration-fast) var(--animate-ease), + border-color var(--animate-duration-fast) var(--animate-ease); + display: flex; + flex-direction: column; + gap: 0.15rem; + cursor: pointer; +} + +.palette-btn:hover { + background: rgba(56, 189, 248, 0.06); + border-color: var(--color-border-subtle); +} + +[data-theme='light'] .palette-btn:hover { + background: rgba(239, 246, 255, 0.8); +} + +.palette-btn strong { + font-weight: 550; +} + +.palette-btn span { + color: var(--text-dim); + font-size: 0.72rem; +} + +/* ══════════════════════════════════════════════════════════════════════════ + RESPONSIVE + ══════════════════════════════════════════════════════════════════════════ */ + +@media (max-width: 1100px) { + .board-layout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 900px) { + .shell { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + height: auto; + border-right: 0; + border-bottom: 1px solid var(--color-border-subtle); + padding: 0.85rem 1rem; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .brand > div { + display: none; + } + + .sidebar-top { + flex-direction: row; + align-items: center; + } + + .sidebar-workspace-card { + display: none; + } + + .sidebar-nav { + flex-direction: row; + gap: 0.35rem; + } + + .nav-btn { + padding: 0.45rem 0.55rem; + } + + .nav-btn span:not(.nav-icon) { + display: none; + } + + .sidebar-footer { + display: none; + } + + .view-grid { + grid-template-columns: 1fr; + } + + .content { + padding: 1.25rem 1rem; + } + + .topbar { + flex-direction: column; + border-radius: var(--radius-xl); + padding: 1rem; + } + + .topbar-actions { + justify-content: flex-start; + width: 100%; + } + + .topbar-meta { + gap: 0.4rem; + } + + .meta-chip { + flex: 1 1 8rem; + min-width: 8rem; + } + + .logs-grid { + grid-template-columns: 1fr; + } + + .preset-card { + flex-direction: column; + align-items: stretch; + } + + .accuracy-row { + grid-template-columns: 1fr; + } +} diff --git a/desktop/src/views/AnalysisView.svelte b/desktop/src/views/AnalysisView.svelte new file mode 100644 index 0000000..42fd0db --- /dev/null +++ b/desktop/src/views/AnalysisView.svelte @@ -0,0 +1,240 @@ + + +
    +
    +
    +
    +

    Analysis Workspace

    +

    + Queue local engine analysis jobs, track progress, and inspect move-by-move verdicts. +

    +
    +
    + +
    + + + + + +
    + + {#if $activeGame} +

    + Current target: {$activeGame.game_id} +

    + {:else} +

    Open a game first to launch a new analysis run.

    + {/if} +
    + + {#if $activeAnalysis} +
    +
    +
    +

    Focused analysis

    +

    + Job {$activeAnalysis.id} · {statusLabel($activeAnalysis)} +

    +
    + {#if !isTerminal($activeAnalysis.status)} + Running + {:else if $activeAnalysis.status === 'Completed'} + Completed + {:else} + Finished + {/if} +
    + + {#if isInProgressStatus($activeAnalysis.status)} + + {/if} + + {#if $activeAnalysis.result} +
    +
    + Depth + {$activeAnalysis.result.depth} +
    +
    + Average CP loss + {$activeAnalysis.result.summary.average_centipawn_loss} +
    +
    + White accuracy + {$activeAnalysis.result.summary.white_accuracy.toFixed(1)}% +
    +
    + Black accuracy + {$activeAnalysis.result.summary.black_accuracy.toFixed(1)}% +
    +
    + +
    + + + + + + + + + + + + + {#each $activeAnalysis.result.annotations as annotation (annotation.move_number + annotation.side)} + + + + + + + + + {/each} + +
    #MoveBestCP lossQualityPV
    {annotation.move_number}{annotation.side === 'white' ? '.' : '…'}{annotationMove(annotation)} + {annotation.best_move.from}{annotation.best_move.to}{annotation.best_move.promotion ?? + ''} + {annotation.centipawn_loss} + + {annotation.quality} + + {annotation.principal_variation.slice(0, 3).join(' ')}
    +
    + {/if} +
    + {/if} + +
    +
    +
    +

    Analysis jobs

    +

    + {$analysisJobs.length} queued or completed job{$analysisJobs.length === 1 ? '' : 's'}. +

    +
    +
    + + {#if $analysisJobs.length === 0} +
    +

    No analysis jobs yet.

    +
    + {:else} +
    + + + + + + + + + + + + {#each $analysisJobs as job (job.id)} + + + + + + + + {/each} + +
    JobGameStatusCreated
    {job.id.slice(0, 8)}…{job.game_id ? `${job.game_id.slice(0, 8)}…` : '—'}{statusLabel(job)}{formatUnixTimestamp(job.created_at)} + + {#if !isTerminal(job.status)} + + {/if} +
    +
    + {/if} +
    +
    diff --git a/desktop/src/views/ArchiveView.svelte b/desktop/src/views/ArchiveView.svelte new file mode 100644 index 0000000..96e4a1a --- /dev/null +++ b/desktop/src/views/ArchiveView.svelte @@ -0,0 +1,205 @@ + + +
    + {#if $replayState} +
    +
    +
    +

    Replay · {$replayState.game_id.slice(0, 8)}…

    +

    Move {replaySliderValue} / {$replayState.total_moves}

    +
    +
    + + + + + +
    +
    + +
    +
    + {#each boardRanks as rank} +
    + {rank} + {#each boardFiles as file} + {@const square = `${file}${rank}`} + {@const piece = $replayState.state.board[square]} +
    + {piece ? PIECE_UNICODE[piece] : ''} +
    + {/each} +
    + {/each} + +
    +
    + + void replayTo(replaySliderValue)} + /> +
    + {/if} + +
    +
    +
    +

    Archived games

    +

    + {$archivedList.length} archived game{$archivedList.length === 1 ? '' : 's'} stored locally. +

    +
    + +
    + + {#if $archivedList.length === 0} +
    +

    No archived games yet. Finished games will appear here.

    +
    + {:else} +
    + + + + + + + + + + + + + {#each $archivedList as game (game.game_id)} + + + + + + + + + {/each} + +
    GameResultReasonMovesSize
    {game.game_id}{game.result ?? '—'}{game.end_reason ?? '—'}{game.move_count}{game.compressed_bytes} B + + +
    +
    + {/if} +
    +
    diff --git a/desktop/src/views/BoardView.svelte b/desktop/src/views/BoardView.svelte new file mode 100644 index 0000000..9569df9 --- /dev/null +++ b/desktop/src/views/BoardView.svelte @@ -0,0 +1,267 @@ + + +
    + {#if $activeGame} +
    +
    +
    +

    Board · {$activeGame.game_id.slice(0, 8)}…

    +

    + {$activeGame.state.turn} to move · {$activeGame.move_history.length} ply recorded + {#if $activeGame.is_check} + · Check + {/if} + {#if $activeGame.is_over} + · {$activeGame.result ?? 'Finished'} + {/if} +

    +
    +
    + + +
    +
    + +
    +
    + {#each boardRanks as rank} +
    + {rank} + {#each boardFiles as file} + {@const square = `${file}${rank}`} + {@const piece = $activeGame.state.board[square]} + {@const isSelected = $selectedSquare === square} + {@const isLegalDestination = $highlightSquares.has(square)} + {@const isLastMove = $lastMove?.from === square || $lastMove?.to === square} + {@const isCheckSquare = $activeGame.is_check && + (($activeGame.state.turn === 'white' && + $activeGame.state.board[square] === 'K') || + ($activeGame.state.turn === 'black' && + $activeGame.state.board[square] === 'k'))} + + {/each} +
    + {/each} +
    + + {#each boardFiles as file} + {file} + {/each} +
    +
    +
    + +
    + + + + + +
    +
    + +
    +
    +
    +

    Move timeline

    +

    + {$legalMoves.length} legal move{$legalMoves.length === 1 ? '' : 's'} from the current + position. +

    +
    +
    + + {#if historyPairs.length === 0} +
    +

    No moves played yet.

    +
    + {:else} +
    + + + + + + + + + + {#each historyPairs as pair} + + + + + + {/each} + +
    #WhiteBlack
    {pair.moveNumber}{pair.white ?? '—'}{pair.black ?? '—'}
    +
    + {/if} + +
    +
    +

    Advanced board view

    +

    Desktop-native debugging aids for the active position.

    +
    +
    +
    {$boardAscii || 'Loading ASCII board…'}
    + + {#if $desktopState.developerMode} +
    + Raw game JSON +
    {JSON.stringify($activeGame, null, 2)}
    +
    + {/if} +
    + {:else} +
    +
    +

    + Open a game from the Games view to use the interactive desktop board. +

    +
    +
    + {/if} +
    diff --git a/desktop/src/views/DashboardView.svelte b/desktop/src/views/DashboardView.svelte new file mode 100644 index 0000000..1648677 --- /dev/null +++ b/desktop/src/views/DashboardView.svelte @@ -0,0 +1,134 @@ + + +
    +
    +
    +

    Welcome to CheckAI Desktop

    +
    +

    + A powerful chess analysis engine with comprehensive game tracking, analysis, + and replay capabilities. +

    + +
    + + + + + {#if $backendStatus.running} + + {:else} + + {/if} + {#if $gamesList[0]} + + {/if} +
    +
    + +
    +
    +

    Active Games

    +
    +

    {$gamesList.length} game(s)

    + {#if $gamesList[0]} +

    Latest: {$gamesList[0].game_id.slice(0, 8)}…

    + {/if} +
    + +
    +
    +

    Analysis Jobs

    +
    +

    {$analysisJobs.length} job(s)

    +

    + {#if $analysisJobs.some((job) => typeof job.status === 'object' && 'InProgress' in job.status)} + Engine is currently evaluating at least one job. + {:else} + Queue is idle right now. + {/if} +

    +
    + + {#if $storageStats} +
    +
    +

    Storage

    +
    +
    +
    + Active + {$storageStats.active_count} +
    +
    + Archived + {$storageStats.archived_count} +
    +
    +
    + {/if} + +
    +
    +

    Workspace

    +
    +
    +
    + Backend URL + {$desktopState.backendUrl} +
    +
    + Status + {$backendStatus.running ? 'Running' : 'Stopped'} +
    +
    + Update + {$updateStatus.state} +
    +
    + Working Dir + + {$desktopState.backendWorkingDirectory || 'Project default'} + +
    +
    +
    +
    diff --git a/desktop/src/views/EngineView.svelte b/desktop/src/views/EngineView.svelte new file mode 100644 index 0000000..aa9a2ef --- /dev/null +++ b/desktop/src/views/EngineView.svelte @@ -0,0 +1,226 @@ + + +
    +
    +
    +
    +

    Engine configuration

    +

    + Control the local backend executable, workspace folders, and analysis assets. +

    +
    + + {$backendStatus.running ? 'Running' : 'Stopped'} + +
    + +
    + Backend URL + + {#if backendUrlError} +

    {backendUrlError}

    + {/if} +
    + +
    + Backend executable +
    + + updateField('backendExecutable', (event.currentTarget as HTMLInputElement).value)} + /> + +
    +
    + +
    + Backend arguments + + updateField('backendArgs', (event.currentTarget as HTMLInputElement).value)} + /> +
    + +
    + Working directory +
    + + updateField( + 'backendWorkingDirectory', + (event.currentTarget as HTMLInputElement).value + )} + /> + + +
    +
    + +
    + Opening book +
    + + updateField('openingBookPath', (event.currentTarget as HTMLInputElement).value)} + /> + +
    +
    + +
    + Tablebase directory +
    + + updateField('tablebasePath', (event.currentTarget as HTMLInputElement).value)} + /> + +
    +
    + + + +
    + + + +
    + +
    + Status: + {$backendStatus.running ? 'Running' : 'Stopped'} + {#if $backendStatus.pid} + (PID: {$backendStatus.pid}) + {/if} + {#if $backendStatus.lastError} +
    {$backendStatus.lastError}
    + {/if} +
    +
    + +
    +
    +
    +

    Saved backend presets

    +

    + {$desktopState.backendPresets.length} preset{$desktopState.backendPresets.length === 1 + ? '' + : 's'} available for quick workspace switching. +

    +
    +
    + + {#if $desktopState.backendPresets.length === 0} +
    +

    No presets saved yet. Save the current backend setup to reuse it.

    +
    + {:else} +
    + + + + + + + + + + + {#each $desktopState.backendPresets as preset (preset.id)} + + + + + + + {/each} + +
    NameExecutableURL
    {preset.name}{preset.backendExecutable}{preset.backendUrl} + + +
    +
    + {/if} +
    +
    diff --git a/desktop/src/views/GamesView.svelte b/desktop/src/views/GamesView.svelte new file mode 100644 index 0000000..a865319 --- /dev/null +++ b/desktop/src/views/GamesView.svelte @@ -0,0 +1,128 @@ + + +
    +
    +
    +
    +

    Active Games

    +

    + Manage live games, import positions, and launch analysis-ready sessions. +

    +
    + + {$backendStatus.running ? 'Backend online' : 'Backend offline'} + +
    + +
    + + + +
    + +
    + + +
    +
    + +
    +
    +
    +

    Live sessions

    +

    + {$gamesList.length} active game{$gamesList.length === 1 ? '' : 's'} available. +

    +
    +
    + + {#if $gamesList.length === 0} +
    +

    + No active games yet. Create a game or import a FEN to get started. +

    +
    + {:else} +
    + + + + + + + + + + + + {#each $gamesList as game (game.game_id)} + + + + + + + + {/each} + +
    GameMoveStatusTurn
    {game.game_id}{game.fullmove_number} + + {game.is_over ? 'Finished' : 'In progress'} + + {game.turn} + + + +
    +
    + {/if} +
    +
    diff --git a/desktop/src/views/LogsView.svelte b/desktop/src/views/LogsView.svelte new file mode 100644 index 0000000..1fe8409 --- /dev/null +++ b/desktop/src/views/LogsView.svelte @@ -0,0 +1,35 @@ + + +
    +
    +
    +
    +

    Backend Logs

    +

    + Inspect the native backend process output and jump to the current workspace folder. +

    +
    +
    + + + {#if $backendStatus.running} + + {:else} + + {/if} +
    +
    + +
    {$backendLogs || 'No logs available yet.'}
    +
    +
    diff --git a/desktop/src/views/SettingsView.svelte b/desktop/src/views/SettingsView.svelte new file mode 100644 index 0000000..2d0facd --- /dev/null +++ b/desktop/src/views/SettingsView.svelte @@ -0,0 +1,121 @@ + + +
    +
    +

    Settings

    +
    + +
    + Theme + +
    + + + + + + + + + +
    +

    Desktop updates

    +
    +

    {$updateStatus.message ?? 'No update information yet.'}

    +
    + + + +
    + + {#if $desktopState.recentWorkspaces.length > 0} +
    +

    Recent workspaces

    +
    +
    + {#each $desktopState.recentWorkspaces as workspace (workspace)} +
    + {workspace} +
    + {/each} +
    + {/if} +
    diff --git a/desktop/src/workspace.ts b/desktop/src/workspace.ts new file mode 100644 index 0000000..e6f890d --- /dev/null +++ b/desktop/src/workspace.ts @@ -0,0 +1,1133 @@ +import { get } from 'svelte/store'; +import { + cancelAnalysisJob, + createGame, + deleteGame, + exportFen, + exportPgn, + getAnalysisJob, + getBoardAscii, + getGame, + getLegalMoves, + importFen, + listAnalysisJobs, + listArchived, + listGames, + replayArchived, + setApiBase, + startAnalysis, + submitMove, +} from './api-client.js'; +import { desktop, loadDesktopState, saveDesktopState } from './desktop-api.js'; +import { + clearNotificationTimers, + pushError, + pushToast, +} from './notifications.js'; +import { attemptRefresh, refreshStore } from './workspace/refresh.js'; +import { + activeAnalysis, + activeGame, + analysisDepth, + analysisJobs, + archivedList, + backendLogs, + backendStatus, + boardAscii, + currentView, + desktopState, + fenInput, + gamesList, + legalMoves, + modalState, + paletteOpen, + replayGameId, + replayState, + selectedSquare, + storageStats, + updateStatus, +} from './stores.js'; +import type { + AnalysisJob, + AnalysisStatus, + BackendPreset, + DesktopState, + DesktopView, + LegalMove, + SaveTextFileOptions, +} from './shared-types.js'; +import { PROMOTION_PIECE_LIST } from './shared-types.js'; + +const ANALYSIS_POLL_INTERVAL_MS = 2000; +const WORKSPACE_REFRESH_INTERVAL_MS = 5000; +const MAX_WORKSPACE_REFRESH_BACKOFF_MULTIPLIER = 6; +const ID_DISPLAY_LENGTH = 8; +const MIN_ANALYSIS_DEPTH = 30; +const MAX_ANALYSIS_DEPTH = 99; +const systemThemeMedia = + typeof window !== 'undefined' ? window.matchMedia('(prefers-color-scheme: light)') : null; + +let analysisPollTimer: ReturnType | null = null; +let analysisRefreshInFlight = false; +let analysisPollingSession = 0; +let workspaceRefreshTimer: ReturnType | null = null; +let workspaceRefreshInFlight = false; +let workspacePollingBackoffUntil = 0; +let workspacePollingFailureCount = 0; +let menuCommandCleanup: (() => void) | null = null; + +function slugify(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'checkai-game'; +} + +export function isInProgressStatus( + status: AnalysisStatus +): status is { InProgress: { moves_analyzed: number; total_moves: number } } { + return typeof status === 'object' && status !== null && 'InProgress' in status; +} + +export function isFailedStatus( + status: AnalysisStatus +): status is { Failed: { error: string } } { + return typeof status === 'object' && status !== null && 'Failed' in status; +} + +export function isTerminalAnalysisStatus(status: AnalysisStatus): boolean { + return status === 'Completed' || status === 'Cancelled' || isFailedStatus(status); +} + +async function notifyIfEnabled(title: string, body: string): Promise { + if (!get(desktopState).notificationsEnabled) { + return; + } + + try { + await desktop.notify(title, body); + } catch { + // Ignore notification delivery failures in the renderer. + } +} + +function shortId(value: string): string { + return value.slice(0, ID_DISPLAY_LENGTH); +} + +async function confirmAction(options: { + title: string; + message: string; + confirmLabel: string; + cancelLabel?: string; +}): Promise { + return new Promise((resolve) => { + modalState.set({ + kind: 'confirm', + title: options.title, + message: options.message, + confirmLabel: options.confirmLabel, + cancelLabel: options.cancelLabel ?? 'Cancel', + resolve, + }); + }); +} + +async function promptForValue(options: { + title: string; + message: string; + confirmLabel: string; + cancelLabel?: string; + initialValue?: string; + placeholder?: string; +}): Promise { + return new Promise((resolve) => { + modalState.set({ + kind: 'prompt', + title: options.title, + message: options.message, + confirmLabel: options.confirmLabel, + cancelLabel: options.cancelLabel ?? 'Cancel', + initialValue: options.initialValue ?? '', + placeholder: options.placeholder, + resolve, + }); + }); +} + +async function selectPromotionMove(moves: LegalMove[]): Promise { + if (moves.length === 0) { + return null; + } + if (moves.length === 1) { + return moves[0]; + } + + while (true) { + const choice = await promptForValue({ + title: 'Choose promotion piece', + message: `Enter ${PROMOTION_PIECE_LIST} for the piece you want to promote to.`, + confirmLabel: 'Promote', + initialValue: 'Q', + placeholder: 'Q', + }); + if (choice === null) { + return null; + } + + const normalized = choice.trim().toUpperCase(); + const move = moves.find((candidate) => candidate.promotion?.toUpperCase() === normalized); + if (move) { + return move; + } + + pushError(`Enter ${PROMOTION_PIECE_LIST} to choose a promotion piece.`); + } +} + +async function copyText(value: string, successMessage: string): Promise { + try { + await navigator.clipboard.writeText(value); + pushToast(successMessage); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +async function saveTextExport( + defaultPath: string, + content: string, + filters: SaveTextFileOptions['filters'], + successMessage: string +): Promise { + try { + const savedPath = await desktop.saveTextFile({ + defaultPath, + content, + filters, + }); + + if (!savedPath) { + return; + } + + pushToast(successMessage); + await notifyIfEnabled('CheckAI Desktop', `${successMessage} (${savedPath})`); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +function applyDesktopState(nextState: DesktopState, persist = true): void { + desktopState.set(nextState); + setApiBase(nextState.backendUrl); + syncDocumentTheme(nextState.theme); + if (persist) { + void saveDesktopState(); + } +} + +function resolveTheme(theme: DesktopState['theme']): 'dark' | 'light' { + if (theme === 'system') { + return systemThemeMedia?.matches ? 'light' : 'dark'; + } + + return theme; +} + +function syncDocumentTheme(theme: DesktopState['theme']): void { + document.documentElement.setAttribute('data-theme', resolveTheme(theme)); +} + +export function updateDesktopState( + updater: (state: DesktopState) => DesktopState, + persist = true +): DesktopState { + const nextState = updater(get(desktopState)); + applyDesktopState(nextState, persist); + return nextState; +} + +export function navigateTo(view: DesktopView): void { + currentView.set(view); + updateDesktopState((state) => ({ ...state, lastView: view })); +} + +async function refreshBackendState(silent = false): Promise { + await refreshStore(silent, () => desktop.getBackendStatus(), (status) => { + backendStatus.set(status); + }); +} + +async function refreshUpdateState(silent = false): Promise { + await refreshStore(silent, () => desktop.getUpdateStatus(), (status) => { + updateStatus.set(status); + }); +} + +export async function refreshLogs(silent = false): Promise { + await refreshStore(silent, () => desktop.getBackendLogs(), (logs) => { + backendLogs.set(logs); + }); +} + +export async function refreshGamesList(silent = false): Promise { + return attemptRefresh(silent, async () => { + const result = await listGames(); + gamesList.set(result.games); + + const currentGame = get(activeGame); + if (currentGame && !result.games.some((game) => game.game_id === currentGame.game_id)) { + activeGame.set(null); + legalMoves.set([]); + selectedSquare.set(null); + boardAscii.set(''); + updateDesktopState((state) => ({ ...state, lastGameId: null })); + } + }); +} + +export async function refreshArchive(silent = false): Promise { + return attemptRefresh(silent, async () => { + const result = await listArchived(); + archivedList.set(result.games); + if (result.storage) { + storageStats.set(result.storage); + } + }); +} + +export async function refreshAnalysisJobs(silent = false): Promise { + return refreshStore(silent, listAnalysisJobs, (result) => { + analysisJobs.set(result.jobs); + }); +} + +async function updateAnalysisProgress(job: AnalysisJob | null): Promise { + if (!job || !isInProgressStatus(job.status)) { + await desktop.setProgressBar(null); + return; + } + + const { moves_analyzed, total_moves } = job.status.InProgress; + if (total_moves <= 0) { + await desktop.setProgressBar(null); + return; + } + + await desktop.setProgressBar(moves_analyzed / total_moves); +} + +async function refreshActiveAnalysisJob(silent = false): Promise { + const currentJob = get(activeAnalysis); + if (!currentJob) { + return false; + } + + return attemptRefresh(silent, async () => { + const previousJob = currentJob; + const nextJob = await getAnalysisJob(currentJob.id); + activeAnalysis.set(nextJob); + await updateAnalysisProgress(nextJob); + + if (!isTerminalAnalysisStatus(previousJob.status) && isTerminalAnalysisStatus(nextJob.status)) { + await refreshAnalysisJobs(true); + if (nextJob.status === 'Completed') { + pushToast('Analysis completed'); + await notifyIfEnabled( + 'CheckAI Desktop', + `Analysis ${shortId(nextJob.id)} completed successfully.` + ); + } else if (nextJob.status === 'Cancelled') { + pushToast('Analysis cancelled'); + } else if (isFailedStatus(nextJob.status)) { + pushError(nextJob.status.Failed.error); + } + } + }); +} + +async function refreshBoardAscii(gameId: string): Promise { + try { + boardAscii.set(await getBoardAscii(gameId)); + } catch { + boardAscii.set('Board ASCII preview unavailable.'); + } +} + +async function refreshActiveGame(silent = false): Promise { + const game = get(activeGame); + if (!game) { + return false; + } + + return attemptRefresh(silent, async () => { + const nextGame = await getGame(game.game_id); + activeGame.set(nextGame); + + if (!nextGame.is_over) { + const moveResult = await getLegalMoves(nextGame.game_id); + legalMoves.set(moveResult.moves); + } else { + legalMoves.set([]); + } + + await refreshBoardAscii(nextGame.game_id); + }); +} + +function stopWorkspaceRefreshPolling(): void { + if (workspaceRefreshTimer) { + clearInterval(workspaceRefreshTimer); + workspaceRefreshTimer = null; + } +} + +function startWorkspaceRefreshPolling(): void { + if (workspaceRefreshTimer) { + return; + } + + workspaceRefreshTimer = setInterval(async () => { + if (workspaceRefreshInFlight || Date.now() < workspacePollingBackoffUntil) { + return; + } + + workspaceRefreshInFlight = true; + try { + const refreshResults = [ + await refreshGamesList(true), + await refreshArchive(true), + await refreshAnalysisJobs(true), + await refreshActiveGame(true), + ]; + if (!analysisPollTimer) { + refreshResults.push(await refreshActiveAnalysisJob(true)); + } + + if (refreshResults.some(Boolean)) { + workspacePollingFailureCount = 0; + workspacePollingBackoffUntil = 0; + } else { + workspacePollingFailureCount += 1; + const backoffMultiplier = Math.min( + workspacePollingFailureCount, + MAX_WORKSPACE_REFRESH_BACKOFF_MULTIPLIER + ); + workspacePollingBackoffUntil = + Date.now() + WORKSPACE_REFRESH_INTERVAL_MS * backoffMultiplier; + } + } finally { + workspaceRefreshInFlight = false; + } + }, WORKSPACE_REFRESH_INTERVAL_MS); +} + +function stopAnalysisPolling(): void { + if (analysisPollTimer) { + clearInterval(analysisPollTimer); + analysisPollTimer = null; + } + analysisPollingSession += 1; + analysisRefreshInFlight = false; + void desktop.setProgressBar(null); +} + +function startAnalysisPolling(jobId: string): void { + stopAnalysisPolling(); + const pollingSession = analysisPollingSession; + analysisPollTimer = setInterval(async () => { + if (pollingSession !== analysisPollingSession) { + return; + } + + if (analysisRefreshInFlight) { + return; + } + + const currentJob = get(activeAnalysis); + if (!currentJob || currentJob.id !== jobId) { + stopAnalysisPolling(); + return; + } + + analysisRefreshInFlight = true; + try { + await refreshActiveAnalysisJob(true); + const nextJob = get(activeAnalysis); + if (!nextJob || isTerminalAnalysisStatus(nextJob.status)) { + stopAnalysisPolling(); + } + } finally { + if (pollingSession === analysisPollingSession) { + analysisRefreshInFlight = false; + } + } + }, ANALYSIS_POLL_INTERVAL_MS); +} + +export async function refreshWorkspaceData(silent = false): Promise { + await Promise.all([ + refreshBackendState(silent), + refreshUpdateState(silent), + refreshLogs(silent), + refreshGamesList(silent), + refreshArchive(silent), + refreshAnalysisJobs(silent), + ]); +} + +export async function initializeDesktopWorkspace(): Promise<() => void> { + await loadDesktopState(); + applyDesktopState(get(desktopState), false); + await refreshWorkspaceData(true); + + const { lastGameId } = get(desktopState); + if (lastGameId) { + await openGame(lastGameId, { keepCurrentView: true, silent: true }); + } + + startWorkspaceRefreshPolling(); + + if (!menuCommandCleanup) { + menuCommandCleanup = desktop.onMenuCommand((command) => { + void handleDesktopCommand(command); + }); + } + + const handleSystemThemeChange = () => { + if (get(desktopState).theme === 'system') { + syncDocumentTheme('system'); + } + }; + + systemThemeMedia?.addEventListener('change', handleSystemThemeChange); + + return () => { + stopWorkspaceRefreshPolling(); + stopAnalysisPolling(); + clearNotificationTimers(); + if (menuCommandCleanup) { + menuCommandCleanup(); + menuCommandCleanup = null; + } + systemThemeMedia?.removeEventListener('change', handleSystemThemeChange); + void desktop.setProgressBar(null); + }; +} + +export async function createNewGame(): Promise { + try { + const result = await createGame(); + await refreshGamesList(true); + await openGame(result.game_id); + pushToast('New game created'); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function importFenText(rawText: string): Promise { + const fen = rawText.trim(); + if (!fen) { + pushError('Paste or drop a FEN string first.'); + return; + } + + try { + const result = await importFen(fen); + fenInput.set(''); + await refreshGamesList(true); + await openGame(result.game_id); + pushToast('Game imported from FEN'); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function importFenFromField(): Promise { + await importFenText(get(fenInput)); +} + +export async function importFenFromFile(): Promise { + try { + const filePath = await desktop.pickFile(); + if (!filePath) { + return; + } + + const text = await desktop.readTextFile(filePath); + await importFenText(text); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function openGame( + gameId: string, + options: { keepCurrentView?: boolean; silent?: boolean } = {} +): Promise { + try { + const game = await getGame(gameId); + activeGame.set(game); + selectedSquare.set(null); + replayState.set(null); + replayGameId.set(null); + updateDesktopState((state) => ({ ...state, lastGameId: gameId })); + + if (!game.is_over) { + const moveResult = await getLegalMoves(game.game_id); + legalMoves.set(moveResult.moves); + } else { + legalMoves.set([]); + } + + await refreshBoardAscii(game.game_id); + + if (!options.keepCurrentView) { + navigateTo('board'); + } + } catch (error) { + if (!options.silent) { + pushError(error instanceof Error ? error.message : String(error)); + } + } +} + +export async function deleteGameById(gameId: string): Promise { + const confirmed = await confirmAction({ + title: 'Delete active game', + message: `Delete game ${shortId(gameId)}…? This removes the live session from the desktop workspace.`, + confirmLabel: 'Delete game', + }); + + if (!confirmed) { + return; + } + + try { + await deleteGame(gameId); + const currentGame = get(activeGame); + if (currentGame?.game_id === gameId) { + activeGame.set(null); + legalMoves.set([]); + selectedSquare.set(null); + boardAscii.set(''); + updateDesktopState((state) => ({ ...state, lastGameId: null })); + } + await refreshGamesList(true); + pushToast('Game deleted'); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function handleBoardSquareClick(square: string): Promise { + const game = get(activeGame); + if (!game || game.is_over) { + return; + } + + const currentlySelected = get(selectedSquare); + const moves = get(legalMoves); + if (currentlySelected) { + const candidateMoves = moves.filter( + (candidate) => candidate.from === currentlySelected && candidate.to === square + ); + const move = await selectPromotionMove(candidateMoves); + + if (move) { + try { + const response = await submitMove( + game.game_id, + currentlySelected, + square, + move.promotion + ); + if (!response.success) { + pushError(response.message); + return; + } + await openGame(game.game_id, { keepCurrentView: true }); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } finally { + selectedSquare.set(null); + } + return; + } + } + + const piece = game.state.board[square]; + if (piece) { + const isWhitePiece = piece === piece.toUpperCase(); + const isWhiteTurn = game.state.turn === 'white'; + if (isWhitePiece === isWhiteTurn) { + selectedSquare.set(square); + return; + } + } + + selectedSquare.set(null); +} + +export async function copyActiveFen(): Promise { + const game = get(activeGame); + if (!game) { + return; + } + + try { + const result = await exportFen(game.game_id); + await copyText(result.fen, 'FEN copied to clipboard'); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function saveActiveFen(): Promise { + const game = get(activeGame); + if (!game) { + return; + } + + try { + const result = await exportFen(game.game_id); + await saveTextExport( + `${slugify(game.game_id)}.fen`, + result.fen, + [{ name: 'FEN position', extensions: ['fen', 'txt'] }], + 'FEN saved to disk' + ); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function copyActivePgn(): Promise { + const game = get(activeGame); + if (!game) { + return; + } + + try { + const pgn = await exportPgn(game.game_id); + await copyText(pgn, 'PGN copied to clipboard'); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function saveActivePgn(): Promise { + const game = get(activeGame); + if (!game) { + return; + } + + try { + const pgn = await exportPgn(game.game_id); + await saveTextExport( + `${slugify(game.game_id)}.pgn`, + pgn, + [{ name: 'PGN game', extensions: ['pgn', 'txt'] }], + 'PGN saved to disk' + ); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function openBackendInBrowser(): Promise { + const url = get(desktopState).backendUrl.trim(); + if (!url) { + return; + } + + try { + await desktop.openExternal(url); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function openArchivedReplay(gameId: string): Promise { + try { + replayGameId.set(gameId); + replayState.set(await replayArchived(gameId, 0)); + navigateTo('archive'); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function replayTo(moveNumber: number): Promise { + const gameId = get(replayGameId); + if (!gameId) { + return; + } + + try { + replayState.set(await replayArchived(gameId, moveNumber)); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export function closeReplay(): void { + replayState.set(null); + replayGameId.set(null); +} + +export function setAnalysisDepth(value: number): void { + const normalized = Math.min( + MAX_ANALYSIS_DEPTH, + Math.max(MIN_ANALYSIS_DEPTH, Math.round(value || MIN_ANALYSIS_DEPTH)) + ); + analysisDepth.set(normalized); +} + +export async function submitAnalysisForGame(gameId?: string): Promise { + const targetGameId = gameId ?? get(activeGame)?.game_id ?? null; + if (!targetGameId) { + pushError('Open a game before starting analysis.'); + return; + } + + try { + const result = await startAnalysis(targetGameId, get(analysisDepth)); + pushToast('Analysis started'); + await refreshAnalysisJobs(true); + navigateTo('analysis'); + await viewAnalysisJob(result.job_id); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function viewAnalysisJob(jobId: string): Promise { + try { + const job = await getAnalysisJob(jobId); + activeAnalysis.set(job); + navigateTo('analysis'); + await updateAnalysisProgress(job); + if (isTerminalAnalysisStatus(job.status)) { + stopAnalysisPolling(); + await refreshAnalysisJobs(true); + } else { + startAnalysisPolling(jobId); + } + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function cancelAnalysis(jobId: string): Promise { + try { + await cancelAnalysisJob(jobId); + pushToast('Analysis cancelled'); + await refreshAnalysisJobs(true); + if (get(activeAnalysis)?.id === jobId) { + await viewAnalysisJob(jobId); + } + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function startBackendProcess(): Promise { + try { + const status = await desktop.startBackend(get(desktopState)); + backendStatus.set(status); + if (status.running) { + pushToast('Backend started successfully'); + await refreshWorkspaceData(true); + } + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function stopBackendProcess(): Promise { + try { + const status = await desktop.stopBackend(); + backendStatus.set(status); + pushToast('Backend stopped'); + stopAnalysisPolling(); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function checkForDesktopUpdates(): Promise { + try { + updateStatus.set(await desktop.checkForUpdates()); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function downloadDesktopUpdate(): Promise { + try { + updateStatus.set(await desktop.downloadUpdate()); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function installDesktopUpdate(): Promise { + try { + await desktop.installUpdate(); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +let presetIdCounter = 0; + +function createPresetId(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return `preset-${crypto.randomUUID()}`; + } + + presetIdCounter += 1; + return `preset-${Date.now()}-${presetIdCounter}`; +} + +function currentPresetFromState(name: string): BackendPreset { + const state = get(desktopState); + return { + id: createPresetId(), + name, + backendExecutable: state.backendExecutable, + backendArgs: state.backendArgs, + backendWorkingDirectory: state.backendWorkingDirectory, + backendUrl: state.backendUrl, + openingBookPath: state.openingBookPath, + tablebasePath: state.tablebasePath, + autoStartBackend: state.autoStartBackend, + createdAt: Date.now(), + }; +} + +export async function saveCurrentPreset(): Promise { + const name = + (await promptForValue({ + title: 'Save backend preset', + message: 'Give the current backend configuration a reusable preset name.', + confirmLabel: 'Save preset', + placeholder: 'Analysis workspace', + })) ?? ''; + if (!name) { + return; + } + + const preset = currentPresetFromState(name); + updateDesktopState((state) => ({ + ...state, + backendPresets: [preset, ...state.backendPresets.filter((entry) => entry.name !== name)], + })); + pushToast(`Preset “${name}” saved`); +} + +export function loadPresetIntoState(id: string): void { + const preset = get(desktopState).backendPresets.find((entry) => entry.id === id); + if (!preset) { + return; + } + + updateDesktopState((state) => ({ + ...state, + backendExecutable: preset.backendExecutable, + backendArgs: preset.backendArgs, + backendWorkingDirectory: preset.backendWorkingDirectory, + backendUrl: preset.backendUrl, + openingBookPath: preset.openingBookPath, + tablebasePath: preset.tablebasePath, + autoStartBackend: preset.autoStartBackend, + })); + pushToast(`Preset “${preset.name}” loaded`); +} + +export async function deletePreset(id: string): Promise { + const preset = get(desktopState).backendPresets.find((entry) => entry.id === id); + if (!preset) { + return; + } + + const confirmed = await confirmAction({ + title: 'Delete backend preset', + message: `Remove preset “${preset.name}” from the desktop workspace?`, + confirmLabel: 'Delete preset', + }); + + if (!confirmed) { + return; + } + + updateDesktopState((state) => ({ + ...state, + backendPresets: state.backendPresets.filter((entry) => entry.id !== id), + })); + pushToast(`Preset “${preset.name}” removed`); +} + +export async function pickBackendExecutable(): Promise { + try { + const file = await desktop.pickFile(); + if (!file) { + return; + } + updateDesktopState((state) => ({ ...state, backendExecutable: file })); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function pickWorkingDirectory(): Promise { + try { + const directory = await desktop.pickDirectory(); + if (!directory) { + return; + } + updateDesktopState((state) => ({ + ...state, + backendWorkingDirectory: directory, + recentWorkspaces: [directory, ...state.recentWorkspaces.filter((entry) => entry !== directory)] + .slice(0, 10), + })); + pushToast('Working directory updated'); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function pickOpeningBook(): Promise { + try { + const file = await desktop.pickFile(); + if (!file) { + return; + } + updateDesktopState((state) => ({ ...state, openingBookPath: file })); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function pickTablebaseDirectory(): Promise { + try { + const directory = await desktop.pickDirectory(); + if (!directory) { + return; + } + updateDesktopState((state) => ({ ...state, tablebasePath: directory })); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function openWorkingDirectory(): Promise { + const directory = get(desktopState).backendWorkingDirectory.trim(); + if (!directory) { + pushError('Choose a working directory first.'); + return; + } + + try { + await desktop.openPath(directory); + } catch (error) { + pushError(error instanceof Error ? error.message : String(error)); + } +} + +export async function refreshCurrentView(): Promise { + switch (get(currentView)) { + case 'dashboard': + await refreshWorkspaceData(true); + await refreshActiveGame(true); + break; + case 'games': + await refreshGamesList(); + break; + case 'board': + await refreshActiveGame(); + break; + case 'archive': + await refreshArchive(); + break; + case 'analysis': + await refreshAnalysisJobs(); + await refreshActiveAnalysisJob(true); + break; + case 'logs': + await refreshLogs(); + break; + case 'engine': + case 'settings': + await refreshBackendState(true); + await refreshUpdateState(true); + break; + } +} + +export async function handleDesktopCommand(command: string): Promise { + switch (command) { + case 'new-game': + await createNewGame(); + break; + case 'import-fen-file': + await importFenFromFile(); + break; + case 'export-fen': + await copyActiveFen(); + break; + case 'save-fen': + await saveActiveFen(); + break; + case 'export-pgn': + await copyActivePgn(); + break; + case 'save-pgn': + await saveActivePgn(); + break; + case 'nav:dashboard': + navigateTo('dashboard'); + break; + case 'nav:games': + navigateTo('games'); + break; + case 'nav:board': + navigateTo('board'); + break; + case 'nav:archive': + navigateTo('archive'); + break; + case 'nav:analysis': + navigateTo('analysis'); + break; + case 'nav:engine': + navigateTo('engine'); + break; + case 'nav:logs': + navigateTo('logs'); + break; + case 'nav:settings': + navigateTo('settings'); + break; + case 'open-command-palette': + paletteOpen.set(true); + break; + case 'start-backend': + await startBackendProcess(); + break; + case 'stop-backend': + await stopBackendProcess(); + break; + case 'open-working-directory': + await openWorkingDirectory(); + break; + case 'check-for-updates': + await checkForDesktopUpdates(); + break; + } +} diff --git a/desktop/src/workspace/refresh.ts b/desktop/src/workspace/refresh.ts new file mode 100644 index 0000000..2781008 --- /dev/null +++ b/desktop/src/workspace/refresh.ts @@ -0,0 +1,30 @@ +import { pushError } from '../notifications.js'; + +function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export async function attemptRefresh( + silent: boolean, + task: () => Promise +): Promise { + try { + await task(); + return true; + } catch (error) { + if (!silent) { + pushError(formatError(error)); + } + return false; + } +} + +export async function refreshStore( + silent: boolean, + load: () => Promise, + apply: (value: T) => void +): Promise { + return attemptRefresh(silent, async () => { + apply(await load()); + }); +} diff --git a/desktop/svelte.config.js b/desktop/svelte.config.js new file mode 100644 index 0000000..4c6b24b --- /dev/null +++ b/desktop/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: vitePreprocess(), +}; diff --git a/desktop/tsconfig.electron.json b/desktop/tsconfig.electron.json new file mode 100644 index 0000000..5ec3bcd --- /dev/null +++ b/desktop/tsconfig.electron.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "types": ["node"], + "lib": ["ES2022", "DOM"], + "rootDir": "src", + "outDir": "dist-electron" + }, + "include": ["src/electron-main.ts", "src/preload.ts", "src/shared-types.ts"] +} diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 0000000..890506d --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM"], + "types": ["vite/client", "svelte"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"], + "exclude": [ + "node_modules", + "dist", + "dist-electron", + "src/electron-main.ts", + "src/preload.ts" + ] +} diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts new file mode 100644 index 0000000..e1261f1 --- /dev/null +++ b/desktop/vite.config.ts @@ -0,0 +1,18 @@ +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import tailwindcss from '@tailwindcss/vite'; +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [svelte(), tailwindcss()], + base: './', + build: { + outDir: 'dist', + emptyOutDir: true, + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + }, + }, + }, +}); diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 11bc9d9..48bb99d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -32,7 +32,7 @@ export default defineConfig({ { text: 'API Reference', link: '/api/rest' }, { text: 'Agent Protocol', link: '/agent/overview' }, { - text: 'v0.5.0', + text: 'v0.7.0', items: [ { text: 'Changelog', @@ -62,6 +62,7 @@ export default defineConfig({ { text: 'Docker', link: '/guide/docker' }, { text: 'Configuration', link: '/guide/configuration' }, { text: 'Web UI', link: '/guide/web-ui' }, + { text: 'Desktop UI', link: '/guide/desktop-ui' }, { text: 'npm Package (WASM)', link: '/guide/npm-package' }, ], }, diff --git a/docs/api/rest.md b/docs/api/rest.md index 69cf81c..1427e15 100644 --- a/docs/api/rest.md +++ b/docs/api/rest.md @@ -298,6 +298,159 @@ Returns the game in PGN (Portable Game Notation) format with standard Seven Tag --- +## Archive & Storage + +Completed games are compressed and persisted to disk. The archive endpoints +list, retrieve, and replay archived games. The matching WebSocket actions +are documented under [WebSocket → Archive](./websocket.md). + +### List Archived Games + +```http +GET /api/archive +``` + +Returns summaries of all archived games together with overall storage +statistics. + +**Response** `200 OK`: + +```json +{ + "games": [ + { + "game_id": "550e8400-e29b-41d4-a716-446655440000", + "move_count": 42, + "result": "WhiteWins", + "end_reason": "Checkmate", + "start_timestamp": 1731000000, + "end_timestamp": 1731003600, + "compressed_bytes": 312, + "raw_bytes": 1024 + } + ], + "total": 1, + "storage": { + "active_count": 0, + "archived_count": 1, + "active_bytes": 0, + "archive_bytes": 312, + "total_bytes": 312 + } +} +``` + +--- + +### Storage Statistics + +```http +GET /api/archive/stats +``` + +Returns disk-usage statistics for both active and archived game files. + +**Response** `200 OK`: + +```json +{ + "active_count": 3, + "archived_count": 42, + "active_bytes": 8192, + "archive_bytes": 73216, + "total_bytes": 81408 +} +``` + +--- + +### Get Archived Game + +```http +GET /api/archive/{game_id} +``` + +Loads a completed game from the archive and returns the **final** position +together with the full game metadata. Use the replay endpoint below to +reconstruct any earlier position. + +**Path Parameters**: + +| Name | Type | Description | +| --------- | ------ | ------------------------- | +| `game_id` | string | UUID of the archived game | + +**Response** `200 OK`: + +```json +{ + "game_id": "550e8400-e29b-41d4-a716-446655440000", + "at_move": 42, + "total_moves": 42, + "state": { + "board": { "a1": "R", "e1": "K", "h8": "k" }, + "turn": "black", + "castling": { + "white": { "kingside": false, "queenside": false }, + "black": { "kingside": false, "queenside": false } + }, + "en_passant": null, + "halfmove_clock": 0, + "fullmove_number": 22, + "position_history": [ + "rnb1kbnr/pppp1ppp/8/4p3/8/5N2/PPPPPPPP/RNBQKB1R w KQkq - 1 2" + ] + }, + "is_over": true, + "result": "WhiteWins", + "is_check": true +} +``` + +**Errors**: + +| Status | Cause | +| ----------------- | ------------------------------- | +| `400 Bad Request` | `game_id` is not a valid UUID | +| `404 Not Found` | No archived game with this UUID | + +--- + +### Replay Archived Game + +```http +GET /api/archive/{game_id}/replay?move_number={n} +``` + +Reconstructs the exact position after `move_number` half-moves of the +archived game. This is the primary endpoint for post-game analysis. + +**Path Parameters**: + +| Name | Type | Description | +| --------- | ------ | ------------------------- | +| `game_id` | string | UUID of the archived game | + +**Query Parameters**: + +| Name | Type | Default | Description | +| ------------- | ------- | -------------- | ---------------------------------------------------------------- | +| `move_number` | integer | final position | Half-move index to replay to. `0` returns the starting position. | + +**Response** `200 OK`: same shape as `GET /api/archive/{game_id}` — +`at_move` reflects the replayed half-move index (clamped to +`total_moves` when the requested number exceeds the game length). + +**Example**: + +```bash +curl "http://localhost:8080/api/archive/550e8400-e29b-41d4-a716-446655440000/replay?move_number=10" +``` + +**Errors**: same as `GET /api/archive/{game_id}`. + +--- + ## Localization All API responses respect the requested locale: diff --git a/docs/changelog.md b/docs/changelog.md index 6ab011d..4b09a25 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,36 @@ All notable changes to CheckAI are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/). +## [0.7.0] — 2026-05-13 + +### Added + +- **Engine test coverage** — perft suites for the starting position (depths 1–3, depth-4 `#[ignore]`-gated) and the Kiwipete benchmark (depths 1–2, depth-3 `#[ignore]`-gated); mate-in-one verification through the full search; transposition-table reuse test across consecutive iterative-deepening runs +- **Evaluation test coverage** — colour-mirror symmetry (starting position and asymmetric material imbalance), tapered-evaluation phase verification, and bishop-pair bonus delta +- **REST archive documentation** — Documented `GET /api/archive`, `GET /api/archive/stats`, `GET /api/archive/{game_id}`, and `GET /api/archive/{game_id}/replay` in `docs/api/rest.md` with request/response shapes and error codes +- **Desktop packaging smoke test in CI** — The desktop CI job now runs `bun run pack` on Ubuntu to validate the full electron-builder pipeline end-to-end on every push + +### Changed + +- **Version metadata** — Bumped Rust crate, WASM crate, npm package, web UI, desktop app, and VitePress version label to 0.7.0 + +## [0.6.0] — 2026-03-09 + +### Added + +- **Electron desktop app** — Added a dedicated Svelte-based Electron renderer alongside the web UI + - Includes persistent desktop sessions, native file/folder pickers, local backend launch controls, inline logs, and a multi-view workspace shell + - Packaged desktop builds can check GitHub Releases for updates, download them, and install on restart +- **Native desktop installers** — Release automation now publishes platform-native Electron installers in addition to updater-compatible artifacts + - Linux releases include `.deb` alongside AppImage + - macOS releases include `.dmg` alongside updater-compatible `.zip` + - Windows releases include `.msi` alongside NSIS for in-app update compatibility +- **Desktop CI and release automation** — GitHub Actions now validate the Electron app on Ubuntu, macOS, and Windows and publish desktop release assets with dependency review coverage + +### Changed + +- **Version metadata** — Updated project/package version references, install snippets, OpenAPI metadata, and documentation to align with the 0.6.0 desktop release + ## [0.5.2] — 2026-03-07 ### Fixed diff --git a/docs/guide/desktop-ui.md b/docs/guide/desktop-ui.md new file mode 100644 index 0000000..9d0dfa1 --- /dev/null +++ b/docs/guide/desktop-ui.md @@ -0,0 +1,56 @@ +# Desktop UI + +CheckAI now ships with a dedicated Electron desktop application in `desktop/` in addition to the existing browser UI. + +## Goals + +The desktop app keeps the full engine workspace available while adding desktop-native workflows: + +- **Persistent sessions** for saved backend URLs, launch arguments, and the last active view +- **Local backend launch controls** so the app can start and stop `checkai serve` +- **Inline log inspection** for backend stdout/stderr +- **Dedicated multi-panel layout** with dashboard, game, board, analysis, archive, engine, log, and settings views +- **Keyboard shortcuts** including a quick-action palette (`⌘/Ctrl + K`) + +## Technology Stack + +| Layer | Technology | +| --- | --- | +| Main process | Electron | +| Renderer | Svelte + TypeScript | +| Styling | SCSS + Tailwind CSS v4 | +| Bundler | Vite | +| Packaging | electron-builder | + +## Build and Run + +```bash +cd desktop +bun install --frozen-lockfile +bun run build +bun run start +``` + +For local packaging: + +```bash +cd desktop +bun run pack # unpacked app bundle +bun run dist # installable artifacts +``` + +## Workflow + +1. Open the **Engine** view and set the backend URL you want to target. +2. Optionally configure a local `checkai` executable, launch arguments, working directory, opening book, and tablebase paths in **Engine**. +3. Use **Start backend** to launch the saved local profile. +4. Use **Dashboard**, **Games**, **Board**, **Analysis**, and **Archive** to move through the desktop workspace views. +5. Use **Logs** to inspect stdout/stderr from the local backend process. +6. Open **Settings** to adjust theme, compact mode, notifications, developer mode, and board orientation. + +## Notes + +- The desktop app complements the existing web UI; it does not replace it. +- Saved desktop state is stored in Electron's user data directory. +- Release automation publishes updater-compatible desktop artifacts alongside native installers for each platform (`.deb` on Linux, `.dmg` on macOS, `.msi` on Windows). +- Windows release builds keep the updater-compatible NSIS installer in addition to the MSI package so in-app desktop updates can still be applied consistently. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index f8830d1..13a87dc 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -15,59 +15,83 @@ ### Pre-built Binaries (Recommended) -Download the install script for a **pinned release**, verify its checksum, then run it: +Pin the release you want and verify the downloaded binary against the published +SHA-256 checksums before installing it. The examples below use `v0.7.0`; check +the [Releases](https://github.com/JosunLP/checkai/releases) page and replace it +with the current or desired release tag. ::: code-group ```bash [Linux / macOS] -# 1. Set the version you want to install -VERSION="0.5.2" +CHECKAI_VERSION=v0.7.0 +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +[ "$OS" = "darwin" ] || OS="linux" +ARCH="$(uname -m)" +case "$ARCH" in x86_64|amd64) ARCH=x86_64 ;; arm64|aarch64) ARCH=aarch64 ;; esac +ASSET="checkai-${OS}-${ARCH}" +BASE_URL="https://github.com/JosunLP/checkai/releases/download/${CHECKAI_VERSION}" + +curl -fSLO "${BASE_URL}/${ASSET}" +curl -fSLO "${BASE_URL}/checksums-sha256.txt" +CHECKSUM_LINE="$(grep " ${ASSET}$" checksums-sha256.txt)" || { + echo "Error: Asset ${ASSET} not found in checksums-sha256.txt" >&2 + exit 1 +} +if command -v sha256sum >/dev/null 2>&1; then + echo "${CHECKSUM_LINE}" | sha256sum -c - +elif command -v shasum >/dev/null 2>&1; then + echo "${CHECKSUM_LINE}" | shasum -a 256 -c - +else + echo "Error: Neither sha256sum nor shasum found. On Linux, install coreutils; on macOS, shasum should be pre-installed." >&2 + exit 1 +fi +chmod +x "${ASSET}" +sudo install -m 0755 "${ASSET}" /usr/local/bin/checkai +``` -# 2. Download the install script from the pinned release tag -curl -fsSL -o install.sh \ - "https://raw.githubusercontent.com/JosunLP/checkai/v${VERSION}/scripts/install.sh" +```powershell [Windows (PowerShell)] +$Version = "v0.7.0" +$Asset = "checkai-windows-x86_64.exe" +$BaseUrl = "https://github.com/JosunLP/checkai/releases/download/$Version" +Invoke-WebRequest "$BaseUrl/$Asset" -OutFile $Asset +Invoke-WebRequest "$BaseUrl/checksums-sha256.txt" -OutFile checksums-sha256.txt +$Expected = ((Select-String .\checksums-sha256.txt -Pattern " $([regex]::Escape($Asset))$").Line -split "\s+")[0].ToLowerInvariant() +$Actual = (Get-FileHash ".\$Asset" -Algorithm SHA256).Hash.ToLowerInvariant() +if ($Actual -ne $Expected) { throw "Checksum verification failed for $Asset" } +New-Item -ItemType Directory "$env:LOCALAPPDATA\checkai" -Force | Out-Null +Move-Item -Force ".\$Asset" "$env:LOCALAPPDATA\checkai\checkai.exe" +``` -# 3. Download the checksum file and verify -curl -fsSL -o install.sh.sha256 \ - "https://github.com/JosunLP/checkai/releases/download/v${VERSION}/install.sh.sha256" -sha256sum -c install.sh.sha256 +::: -# 4. Inspect the script before running it -less install.sh +Windows ARM64 users should use the `checkai-windows-x86_64.exe` asset under +Windows' x86-64 emulation until a native ARM64 CLI binary is published. -# 5. Execute -sh install.sh -``` +#### Installer shortcut -```powershell [Windows] -# 1. Set the version you want to install -$Version = "0.5.2" +The installer can still detect your operating system, architecture, and latest +release automatically: -# 2. Download the install script from the pinned release tag -Invoke-WebRequest ` - -Uri "https://raw.githubusercontent.com/JosunLP/checkai/v$Version/scripts/install.ps1" ` - -OutFile install.ps1 - -# 3. Download the checksum file and verify -Invoke-WebRequest ` - -Uri "https://github.com/JosunLP/checkai/releases/download/v$Version/install.ps1.sha256" ` - -OutFile install.ps1.sha256 -$expected = (Get-Content install.ps1.sha256).Split(' ')[0] -$actual = (Get-FileHash install.ps1 -Algorithm SHA256).Hash.ToLower() -if ($actual -ne $expected) { throw "Checksum mismatch! Aborting." } +::: code-group -# 4. Inspect the script before running it -Get-Content install.ps1 | Out-Host -Paging +```bash [Linux / macOS] +curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | sh +``` -# 5. Execute -.\install.ps1 +```powershell [Windows (PowerShell)] +irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | iex ``` ::: -> [!WARNING] -> **Never** pipe remote scripts directly into a shell (`curl | sh`, `irm | iex`). -> Always download, verify, and inspect scripts before executing them. +::: warning +The installer shortcut executes the current `main` branch script immediately and +is less secure than verifying a pinned release asset first. Only use it if you +trust the source and accept that trade-off. If you want a manual review step, +open or download the same URL first, read through the script carefully, and only +then run it yourself. You can also inspect the matching script in the +repository's `scripts/` directory before executing it. +::: ### Build from Source @@ -142,47 +166,20 @@ This starts an interactive two-player game with a colored board display. Type `h ::: code-group ```bash [Linux / macOS] -# 1. Set the version that was installed -VERSION="0.5.2" - -# 2. Download the uninstall script from the pinned release tag -curl -fsSL -o uninstall.sh \ - "https://raw.githubusercontent.com/JosunLP/checkai/v${VERSION}/scripts/uninstall.sh" - -# 3. Download the checksum file and verify -curl -fsSL -o uninstall.sh.sha256 \ - "https://github.com/JosunLP/checkai/releases/download/v${VERSION}/uninstall.sh.sha256" -sha256sum -c uninstall.sh.sha256 - -# 4. Inspect, then execute -less uninstall.sh -sh uninstall.sh +curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.sh | sh ``` -```powershell [Windows] -# 1. Set the version that was installed -$Version = "0.5.2" - -# 2. Download the uninstall script from the pinned release tag -Invoke-WebRequest ` - -Uri "https://raw.githubusercontent.com/JosunLP/checkai/v$Version/scripts/uninstall.ps1" ` - -OutFile uninstall.ps1 - -# 3. Download the checksum file and verify -Invoke-WebRequest ` - -Uri "https://github.com/JosunLP/checkai/releases/download/v$Version/uninstall.ps1.sha256" ` - -OutFile uninstall.ps1.sha256 -$expected = (Get-Content uninstall.ps1.sha256).Split(' ')[0] -$actual = (Get-FileHash uninstall.ps1 -Algorithm SHA256).Hash.ToLower() -if ($actual -ne $expected) { throw "Checksum mismatch! Aborting." } - -# 4. Inspect, then execute -Get-Content uninstall.ps1 | Out-Host -Paging -.\uninstall.ps1 +```powershell [Windows (PowerShell)] +irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.sh | iex ``` ::: +::: warning +The one-line commands in the Uninstall section execute a remote script immediately. +Only use them if you trust the source. If you want a manual review step, open or download the same URL first, read through the script carefully, and only then run it yourself. You can also inspect the matching script in the repository's `scripts/` directory before executing it. +::: + ## Next Steps - [CLI Commands](./cli) — All available commands and flags diff --git a/npm/README.md b/npm/README.md index 249c7ed..00c4974 100644 --- a/npm/README.md +++ b/npm/README.md @@ -2,7 +2,7 @@ **CheckAI chess engine compiled to WebAssembly** — use as a Node.js CLI tool or JavaScript library. -Current package release: **0.5.2**. +Current package release: **0.7.0**. Implements complete FIDE 2023 chess rules with move generation, position evaluation, deep search, full game management, and export (PGN/JSON/text). diff --git a/npm/bin/checkai.mjs b/npm/bin/checkai.mjs index 53cf601..4ae32db 100644 --- a/npm/bin/checkai.mjs +++ b/npm/bin/checkai.mjs @@ -175,7 +175,7 @@ try { } case 'version': { - console.log('checkai 0.5.2 (wasm)'); + console.log('checkai 0.6.0 (wasm)'); break; } diff --git a/npm/package.json b/npm/package.json index ea7e240..b30a496 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@josunlp/checkai", - "version": "0.5.2", + "version": "0.7.0", "packageManager": "bun@1.3.10", "description": "CheckAI chess engine — FIDE 2023 rules, move generation, evaluation, search, game management, and export via WebAssembly", "license": "MIT", diff --git a/scripts/install.ps1 b/scripts/install.ps1 deleted file mode 100644 index bdb1c88..0000000 --- a/scripts/install.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -# CheckAI Installer — Windows (PowerShell) -# Usage: irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.ps1 | iex -$ErrorActionPreference = "Stop" - -$repo = "JosunLP/checkai" -$binaryName = "checkai.exe" -$installDir = "$env:LOCALAPPDATA\checkai" - -Write-Host "" -Write-Host "+===========================================+" -ForegroundColor Cyan -Write-Host "| CheckAI Installer (Windows) |" -ForegroundColor Cyan -Write-Host "+===========================================+" -ForegroundColor Cyan -Write-Host "" - -# --- Detect Architecture --- -$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - "aarch64" -} else { - "x86_64" -} - -$assetName = "checkai-windows-${arch}.exe" -Write-Host "Detected platform: windows/${arch}" -Write-Host "Asset: ${assetName}" -Write-Host "" - -# --- Get latest release info --- -Write-Host "Fetching latest release..." -$latestUrl = "https://api.github.com/repos/$repo/releases/latest" - -try { - $release = Invoke-RestMethod -Uri $latestUrl -Headers @{ - "User-Agent" = "checkai-installer" - } -} catch { - Write-Error "Failed to fetch latest release: $_" - exit 1 -} - -$version = $release.tag_name -$asset = $release.assets | Where-Object { $_.name -eq $assetName } - -if (-not $asset) { - Write-Error "Could not find release asset '$assetName'." - Write-Host "Available assets:" - $release.assets | ForEach-Object { Write-Host " - $($_.name)" } - exit 1 -} - -Write-Host "Latest version: $version" -Write-Host "" - -# --- Create install directory --- -if (!(Test-Path $installDir)) { - Write-Host "Creating install directory: $installDir" - New-Item -ItemType Directory -Path $installDir -Force | Out-Null -} - - -# --- Download binary --- -$downloadUrl = $asset.browser_download_url -$targetPath = Join-Path $installDir $binaryName - -Write-Host "Downloading $assetName..." -try { - Invoke-WebRequest -Uri $downloadUrl -OutFile $targetPath -UseBasicParsing -} catch { - Write-Error "Failed to download: $_" - exit 1 -} - -# --- Add to PATH --- -$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") -if ($currentPath -notlike "*$installDir*") { - Write-Host "Adding $installDir to user PATH..." - [Environment]::SetEnvironmentVariable("PATH", "$currentPath;$installDir", "User") - $env:PATH = "$env:PATH;$installDir" - Write-Host " PATH updated. You may need to restart your terminal." -ForegroundColor Yellow -} - -Write-Host "" -Write-Host "+===========================================+" -ForegroundColor Green -Write-Host "| CheckAI installed successfully! |" -ForegroundColor Green -Write-Host "+===========================================+" -ForegroundColor Green -Write-Host "" -Write-Host "Version: $version" -Write-Host "Location: $targetPath" -Write-Host "" -Write-Host "Get started:" -Write-Host " checkai --help Show help" -Write-Host " checkai serve Start the API server" -Write-Host " checkai play Play in the terminal" -Write-Host "" diff --git a/scripts/install.sh b/scripts/install.sh old mode 100644 new mode 100755 index 7b89317..4cc603d --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,15 +1,31 @@ #!/bin/sh -# CheckAI Installer — Linux & macOS -# Usage: curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | sh +# CheckAI Installer — Linux, macOS & Windows +# Cross-platform polyglot: valid in sh/bash and PowerShell. +# +# Linux / macOS: curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | sh +# Windows (PS): irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | iex +# +# The script automatically detects the operating system and CPU architecture. +# No manual version entry is required — the latest GitHub release is fetched. +# +# Polyglot boundary — in sh, the backticks run `# | Out-Null <#` as a +# command substitution, where `#` starts a shell comment so the rest of +# the line is ignored and the substitution expands to an empty string. +# In PowerShell, `# becomes a literal #, the output is piped to +# Out-Null, and <# starts a block comment that hides the shell section. +echo `# | Out-Null <#` + +# ====================== POSIX Shell Section (Linux / macOS) ====================== set -e REPO="JosunLP/checkai" INSTALL_DIR="/usr/local/bin" BINARY_NAME="checkai" -echo "╔═══════════════════════════════════════╗" -echo "║ CheckAI Installer ║" -echo "╚═══════════════════════════════════════╝" +echo "" +echo "=====================================" +echo " CheckAI Installer" +echo "=====================================" echo "" # --- Detect OS --- @@ -18,9 +34,10 @@ case "$OS" in Linux) OS="linux" ;; Darwin) OS="darwin" ;; *) - echo "Error: Unsupported operating system: $OS" - echo "This installer supports Linux and macOS." - echo "For Windows, use install.ps1 instead." + echo "Error: Unsupported operating system for the POSIX shell installer: $OS" + echo "This shell path supports Linux and macOS." + echo "On Windows, run the installer in PowerShell instead:" + echo " irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/install.sh | iex" exit 1 ;; esac @@ -87,9 +104,9 @@ else fi echo "" -echo "╔═══════════════════════════════════════╗" -echo "║ CheckAI installed successfully! ║" -echo "╚═══════════════════════════════════════╝" +echo "=====================================" +echo " CheckAI installed successfully!" +echo "=====================================" echo "" echo "Version: ${VERSION}" echo "Location: ${INSTALL_DIR}/${BINARY_NAME}" @@ -99,3 +116,191 @@ echo " checkai --help Show help" echo " checkai serve Start the API server" echo " checkai play Play in the terminal" echo "" + +exit 0 +#> + +# ====================== PowerShell Section (Windows / Linux / macOS) ====================== + +$ErrorActionPreference = "Stop" + +$repo = "JosunLP/checkai" + +function Invoke-CheckAIDownload { + param( + [string]$Uri, + [string]$OutFile + ) + + $requestParams = @{ + Uri = $Uri + OutFile = $OutFile + } + + if ($PSVersionTable.PSVersion.Major -lt 6) { + $requestParams.UseBasicParsing = $true + } + + Invoke-WebRequest @requestParams +} + +function Assert-NativeCommandSucceeded { + param( + [string]$ErrorMessage + ) + + if ($LASTEXITCODE -ne 0) { + Write-Error $ErrorMessage + exit $LASTEXITCODE + } +} + +Write-Host "" +Write-Host "=====================================" -ForegroundColor Cyan +Write-Host " CheckAI Installer" -ForegroundColor Cyan +Write-Host "=====================================" -ForegroundColor Cyan +Write-Host "" + +# --- Detect OS --- +if ($IsLinux) { + $os = "linux" + $binaryName = "checkai" + $installDir = "/usr/local/bin" +} elseif ($IsMacOS) { + $os = "darwin" + $binaryName = "checkai" + $installDir = "/usr/local/bin" +} else { + $os = "windows" + $binaryName = "checkai.exe" + $installDir = "$env:LOCALAPPDATA\checkai" +} + +# --- Detect Architecture --- +$osArchitecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture +$arch = if ($osArchitecture -eq [System.Runtime.InteropServices.Architecture]::X64) { + "x86_64" +} elseif ($osArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + "aarch64" +} else { + Write-Error "Unsupported architecture: $osArchitecture. Supported architectures are X64 and Arm64." + exit 1 +} + +$assetArch = $arch +if ($os -eq "windows" -and $arch -eq "aarch64") { + Write-Host "Windows ARM64 release asset is not published yet; using the x86_64 CLI binary under emulation." + $assetArch = "x86_64" +} + +if ($os -eq "windows") { + $assetName = "checkai-windows-${assetArch}.exe" +} else { + $assetName = "checkai-${os}-${arch}" +} + +Write-Host "Detected platform: ${os}/${arch}" +Write-Host "Asset: ${assetName}" +Write-Host "" + +# --- Get latest release info --- +Write-Host "Fetching latest release..." +$latestUrl = "https://api.github.com/repos/$repo/releases/latest" + +try { + $release = Invoke-RestMethod -Uri $latestUrl -Headers @{ + "User-Agent" = "checkai-installer" + } +} catch { + Write-Error "Failed to fetch latest release: $_" + exit 1 +} + +$version = $release.tag_name +$asset = $release.assets | Where-Object { $_.name -eq $assetName } + +if (-not $asset) { + Write-Error "Could not find release asset '$assetName'." + Write-Host "Available assets:" + $release.assets | ForEach-Object { Write-Host " - $($_.name)" } + exit 1 +} + +Write-Host "Latest version: $version" +Write-Host "" + +$downloadUrl = $asset.browser_download_url + +if ($os -eq "windows") { + # --- Windows: install to LOCALAPPDATA --- + if (!(Test-Path $installDir)) { + Write-Host "Creating install directory: $installDir" + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + } + + $targetPath = Join-Path $installDir $binaryName + Write-Host "Downloading $assetName..." + try { + Invoke-CheckAIDownload -Uri $downloadUrl -OutFile $targetPath + } catch { + Write-Error "Failed to download: $_" + exit 1 + } + + # --- Add to PATH --- + $currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($currentPath -notlike "*$installDir*") { + Write-Host "Adding $installDir to user PATH..." + [Environment]::SetEnvironmentVariable("PATH", "$currentPath;$installDir", "User") + $env:PATH = "$env:PATH;$installDir" + Write-Host " PATH updated. You may need to restart your terminal." -ForegroundColor Yellow + } +} else { + # --- Linux / macOS: install to /usr/local/bin --- + $tempFile = [System.IO.Path]::GetTempFileName() + Write-Host "Downloading $assetName..." + try { + Invoke-CheckAIDownload -Uri $downloadUrl -OutFile $tempFile + } catch { + Write-Error "Failed to download: $_" + exit 1 + } + + chmod +x $tempFile + Assert-NativeCommandSucceeded "Failed to mark $tempFile as executable." + + $targetPath = Join-Path $installDir $binaryName + if (Test-Path $installDir -PathType Container) { + try { + Move-Item -Force $tempFile $targetPath + } catch { + Write-Host "Requires elevated permissions. Using sudo..." + sudo mv $tempFile $targetPath + Assert-NativeCommandSucceeded "Failed to move $tempFile to $targetPath with sudo." + sudo chmod +x $targetPath + Assert-NativeCommandSucceeded "Failed to mark $targetPath as executable with sudo." + } + } else { + Write-Host "Creating install directory with sudo: $installDir" + sudo mkdir -p $installDir + Assert-NativeCommandSucceeded "Failed to create install directory $installDir with sudo." + sudo mv $tempFile $targetPath + Assert-NativeCommandSucceeded "Failed to move $tempFile to $targetPath with sudo." + sudo chmod +x $targetPath + Assert-NativeCommandSucceeded "Failed to mark $targetPath as executable with sudo." + } +} + +Write-Host "" +Write-Host "=====================================" -ForegroundColor Green +Write-Host " CheckAI installed successfully!" -ForegroundColor Green +Write-Host "=====================================" -ForegroundColor Green +Write-Host "" +Write-Host "Version: $version" +Write-Host "Location: $targetPath" +Write-Host "" +Write-Host "Get started:" +Write-Host " checkai --help Show help" +Write-Host " checkai serve Start the API server" +Write-Host " checkai play Play in the terminal" +Write-Host "" diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 deleted file mode 100644 index 9a9bb51..0000000 --- a/scripts/uninstall.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -# CheckAI Uninstaller — Windows (PowerShell) -# Usage: irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.ps1 | iex -$ErrorActionPreference = "Stop" - -$binaryName = "checkai.exe" -$installDir = "$env:LOCALAPPDATA\checkai" -$binaryPath = Join-Path $installDir $binaryName - -Write-Host "" -Write-Host "+===========================================+" -ForegroundColor Cyan -Write-Host "| CheckAI Uninstaller (Windows) |" -ForegroundColor Cyan -Write-Host "+===========================================+" -ForegroundColor Cyan -Write-Host "" - -# --- Check if installed --- -if (!(Test-Path $binaryPath)) { - Write-Host "CheckAI is not installed at $binaryPath." - Write-Host "" - - # Try to find it elsewhere - $found = Get-Command $binaryName -ErrorAction SilentlyContinue - if ($found) { - Write-Host "Found checkai at: $($found.Source)" - Write-Host "Remove it manually." - } - exit 0 -} - -Write-Host "Found CheckAI at: $binaryPath" - -# --- Confirm removal --- -$confirm = Read-Host "Do you want to uninstall CheckAI? [y/N]" -if ($confirm -notmatch "^[yY]") { - Write-Host "Aborted." - exit 0 -} - -# --- Remove binary --- -Write-Host "Removing $binaryPath..." -Remove-Item -Path $binaryPath -Force - -# Also remove old version file if it exists -$oldBinary = Join-Path $installDir "checkai.old.exe" -if (Test-Path $oldBinary) { - Remove-Item -Path $oldBinary -Force -} - -# --- Remove install directory if empty --- -$remaining = Get-ChildItem -Path $installDir -ErrorAction SilentlyContinue -if (-not $remaining) { - Remove-Item -Path $installDir -Force - Write-Host "Removed empty install directory." -} - -# --- Remove from PATH --- -$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") -if ($currentPath -like "*$installDir*") { - Write-Host "Removing $installDir from user PATH..." - $newPath = ($currentPath -split ";" | Where-Object { $_ -ne $installDir }) -join ";" - [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") - Write-Host " PATH updated. You may need to restart your terminal." -ForegroundColor Yellow -} - -# --- Clean up data directory (optional) --- -$dataDir = "$env:LOCALAPPDATA\checkai-data" -if (Test-Path $dataDir) { - $confirmData = Read-Host "Remove data directory ($dataDir)? [y/N]" - if ($confirmData -match "^[yY]") { - Remove-Item -Path $dataDir -Recurse -Force - Write-Host "Data directory removed." - } else { - Write-Host "Data directory kept." - } -} - -Write-Host "" -Write-Host "+===========================================+" -ForegroundColor Green -Write-Host "| CheckAI uninstalled successfully. |" -ForegroundColor Green -Write-Host "+===========================================+" -ForegroundColor Green -Write-Host "" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh old mode 100644 new mode 100755 index 4a8cd1c..61bcb93 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -1,16 +1,71 @@ #!/bin/sh -# CheckAI Uninstaller — Linux & macOS -# Usage: curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.sh | sh +# CheckAI Uninstaller — Linux, macOS & Windows +# Cross-platform polyglot: valid in sh/bash and PowerShell. +# +# Linux / macOS: curl -fsSL https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.sh | sh +# Windows (PS): irm https://raw.githubusercontent.com/JosunLP/checkai/main/scripts/uninstall.sh | iex +# +# The script automatically detects the operating system. +# +# Polyglot boundary — in sh, the backticks run `# | Out-Null <#` as a +# command substitution, where `#` starts a shell comment so the rest of +# the line is ignored and the substitution expands to an empty string. +# In PowerShell, `# becomes a literal #, the output is piped to +# Out-Null, and <# starts a block comment that hides the shell section. +echo `# | Out-Null <#` + +# ====================== POSIX Shell Section (Linux / macOS) ====================== set -e INSTALL_DIR="/usr/local/bin" BINARY_NAME="checkai" BINARY_PATH="${INSTALL_DIR}/${BINARY_NAME}" -echo "╔═══════════════════════════════════════╗" -echo "║ CheckAI Uninstaller ║" -echo "╚═══════════════════════════════════════╝" echo "" +echo "=====================================" +echo " CheckAI Uninstaller" +echo "=====================================" +echo "" + +prompt_tty_unavailable() { + echo "No readable and writable /dev/tty is available for confirmation prompts. Aborting." >&2 + echo "Re-run the uninstall command from a terminal session that can provide interactive input." >&2 + return 2 +} + +prompt_yes_no() { + if ! printf "%s" "$1" >/dev/tty 2>/dev/null; then + prompt_tty_unavailable + return $? + fi + + if ! read -r REPLY /dev/null; then + prompt_tty_unavailable + return $? + fi + + case "$REPLY" in + [yY]|[yY][eE][sS]) return 0 ;; + *) return 1 ;; + esac +} + +handle_prompt_result() { + PROMPT_STATUS=0 + prompt_yes_no "$1" || PROMPT_STATUS=$? + + case "$PROMPT_STATUS" in + 0) + return 0 + ;; + 1) + return 1 + ;; + 2) + return 2 + ;; + esac +} # --- Check if installed --- if [ ! -f "$BINARY_PATH" ]; then @@ -29,17 +84,18 @@ fi # --- Confirm removal --- echo "Found CheckAI at: ${BINARY_PATH}" -if [ -t 0 ]; then - printf "Do you want to uninstall CheckAI? [y/N] " - read -r CONFIRM - case "$CONFIRM" in - [yY]|[yY][eE][sS]) ;; - *) - echo "Aborted." - exit 0 - ;; - esac -fi +PROMPT_STATUS=0 +handle_prompt_result "Do you want to uninstall CheckAI? [y/N] " || PROMPT_STATUS=$? + +case "$PROMPT_STATUS" in + 1) + echo "Aborted." + exit 0 + ;; + 2) + exit 1 + ;; +esac # --- Remove binary --- echo "Removing ${BINARY_PATH}..." @@ -54,23 +110,143 @@ fi # --- Clean up data directory (optional) --- DATA_DIR="${HOME}/.local/share/checkai" if [ -d "$DATA_DIR" ]; then - if [ -t 0 ]; then - printf "Remove data directory (%s)? [y/N] " "$DATA_DIR" - read -r CONFIRM_DATA - case "$CONFIRM_DATA" in - [yY]|[yY][eE][sS]) - rm -rf "$DATA_DIR" - echo "Data directory removed." - ;; - *) - echo "Data directory kept." - ;; - esac - fi + PROMPT_STATUS=0 + handle_prompt_result "Remove data directory (${DATA_DIR})? [y/N] " || PROMPT_STATUS=$? + + case "$PROMPT_STATUS" in + 0) + rm -rf "$DATA_DIR" + echo "Data directory removed." + ;; + 1) + echo "Data directory kept." + ;; + 2) + exit 1 + ;; + esac fi echo "" -echo "╔═══════════════════════════════════════╗" -echo "║ CheckAI uninstalled successfully. ║" -echo "╚═══════════════════════════════════════╝" +echo "=====================================" +echo " CheckAI uninstalled successfully." +echo "=====================================" echo "" + +exit 0 +#> + +# ====================== PowerShell Section (Windows / Linux / macOS) ====================== + +$ErrorActionPreference = "Stop" + +function Assert-NativeCommandSucceeded { + param( + [string]$ErrorMessage + ) + + if ($LASTEXITCODE -ne 0) { + Write-Error $ErrorMessage + exit $LASTEXITCODE + } +} + +Write-Host "" +Write-Host "=====================================" -ForegroundColor Cyan +Write-Host " CheckAI Uninstaller" -ForegroundColor Cyan +Write-Host "=====================================" -ForegroundColor Cyan +Write-Host "" + +# --- Detect OS --- +if ($IsLinux) { + $binaryName = "checkai" + $installDir = "/usr/local/bin" + $dataDir = "$env:HOME/.local/share/checkai" +} elseif ($IsMacOS) { + $binaryName = "checkai" + $installDir = "/usr/local/bin" + $dataDir = "$env:HOME/.local/share/checkai" +} else { + $binaryName = "checkai.exe" + $installDir = "$env:LOCALAPPDATA\checkai" + $dataDir = "$env:LOCALAPPDATA\checkai-data" +} + +$binaryPath = Join-Path $installDir $binaryName + +# --- Check if installed --- +if (!(Test-Path $binaryPath)) { + Write-Host "CheckAI is not installed at $binaryPath." + Write-Host "" + + # Try to find it elsewhere + $found = Get-Command $binaryName -ErrorAction SilentlyContinue + if ($found) { + Write-Host "Found checkai at: $($found.Source)" + Write-Host "Remove it manually." + } + exit 0 +} + +Write-Host "Found CheckAI at: $binaryPath" + +# --- Confirm removal --- +$confirm = Read-Host "Do you want to uninstall CheckAI? [y/N]" +if ($confirm -notmatch "^[yY]") { + Write-Host "Aborted." + exit 0 +} + +# --- Remove binary --- +Write-Host "Removing $binaryPath..." + +if ($IsLinux -or $IsMacOS) { + try { + Remove-Item -Path $binaryPath -Force + } catch { + Write-Host "Requires elevated permissions. Using sudo..." + sudo rm -f $binaryPath + Assert-NativeCommandSucceeded "Failed to remove $binaryPath with sudo rm." + } +} else { + Remove-Item -Path $binaryPath -Force + + # Also remove old version file if it exists + $oldBinary = Join-Path $installDir "checkai.old.exe" + if (Test-Path $oldBinary) { + Remove-Item -Path $oldBinary -Force + } + + # --- Remove install directory if empty --- + $remaining = Get-ChildItem -Path $installDir -Force -ErrorAction SilentlyContinue + if (-not $remaining) { + Remove-Item -Path $installDir -Force + Write-Host "Removed empty install directory." + } + + # --- Remove from PATH --- + $currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($currentPath -like "*$installDir*") { + Write-Host "Removing $installDir from user PATH..." + $newPath = ($currentPath -split ";" | Where-Object { $_ -ne $installDir }) -join ";" + [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") + Write-Host " PATH updated. You may need to restart your terminal." -ForegroundColor Yellow + } +} + +# --- Clean up data directory (optional) --- +if (Test-Path $dataDir) { + $confirmData = Read-Host "Remove data directory ($dataDir)? [y/N]" + if ($confirmData -match "^[yY]") { + Remove-Item -Path $dataDir -Recurse -Force + Write-Host "Data directory removed." + } else { + Write-Host "Data directory kept." + } +} + +Write-Host "" +Write-Host "=====================================" -ForegroundColor Green +Write-Host " CheckAI uninstalled successfully." -ForegroundColor Green +Write-Host "=====================================" -ForegroundColor Green +Write-Host "" diff --git a/src/api.rs b/src/api.rs index 179ca7c..f22ff50 100644 --- a/src/api.rs +++ b/src/api.rs @@ -41,7 +41,7 @@ pub struct AppState { #[openapi( info( title = "CheckAI — Chess API for AI Agents", - version = "0.5.2", + version = "0.7.0", description = "A REST API that allows AI agents to play chess against each other. \ Follows FIDE 2023 Laws of Chess. Agents communicate using JSON \ game states and move objects as defined in the AGENT.md protocol.", diff --git a/src/eval.rs b/src/eval.rs index 2d7ec7e..8083b78 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -391,6 +391,13 @@ pub fn evaluate(board: &Board, turn: Color) -> i32 { /// /// Returns `(mg_white, eg_white, mg_black, eg_black, phase)`. fn accumulate(board: &Board) -> (i32, i32, i32, i32, i32) { + accumulate_internal(board, true) +} + +fn accumulate_internal( + board: &Board, + include_bishop_pair_bonus: bool, +) -> (i32, i32, i32, i32, i32) { let mut mg_white = 0i32; let mut eg_white = 0i32; let mut mg_black = 0i32; @@ -463,11 +470,11 @@ fn accumulate(board: &Board) -> (i32, i32, i32, i32, i32) { } // Bishop pair bonus - if white_bishops >= 2 { + if include_bishop_pair_bonus && white_bishops >= 2 { mg_white += BISHOP_PAIR_BONUS_MG; eg_white += BISHOP_PAIR_BONUS_EG; } - if black_bishops >= 2 { + if include_bishop_pair_bonus && black_bishops >= 2 { mg_black += BISHOP_PAIR_BONUS_MG; eg_black += BISHOP_PAIR_BONUS_EG; } @@ -1007,4 +1014,179 @@ mod tests { let board = Board::starting_position(); assert_eq!(piece_count(&board), 32); } + + // ----------------------------------------------------------------------- + // Vertical-flip symmetry, phase tapering, and bishop-pair tests + // ----------------------------------------------------------------------- + + /// Vertically mirrors the board and swaps piece colors, producing a + /// position that is strategically identical from the opposite side's + /// perspective. Used to check colour-symmetry of the evaluation. + fn mirror_board(board: &Board) -> Board { + let mut out = Board::default(); + for rank in 0..8 { + for file in 0..8 { + let src = Square::new(file, rank); + if let Some(p) = board.get(src) { + let dst = Square::new(file, 7 - rank); + out.set( + dst, + Some(Piece::new( + p.kind, + match p.color { + Color::White => Color::Black, + Color::Black => Color::White, + }, + )), + ); + } + } + } + out + } + + #[test] + fn test_eval_colour_symmetry_starting_position() { + let board = Board::starting_position(); + let mirrored = mirror_board(&board); + // Starting position is its own colour mirror, so eval must match + // regardless of which side is to move. + assert_eq!( + evaluate(&board, Color::White), + evaluate(&mirrored, Color::Black) + ); + assert_eq!( + evaluate(&board, Color::Black), + evaluate(&mirrored, Color::White) + ); + } + + #[test] + fn test_eval_colour_symmetry_asymmetric_position() { + // White is up a bishop on c4; mirror gives Black up a bishop on c5. + let mut board = Board::default(); + // Kings on the back rank (FIDE-legal positioning). + board.set( + Square::new(4, 0), + Some(Piece::new(PieceKind::King, Color::White)), + ); + board.set( + Square::new(4, 7), + Some(Piece::new(PieceKind::King, Color::Black)), + ); + // Extra white bishop on c4. + board.set( + Square::new(2, 3), + Some(Piece::new(PieceKind::Bishop, Color::White)), + ); + + let w_score = evaluate(&board, Color::White); + let mirrored = mirror_board(&board); + let b_score = evaluate(&mirrored, Color::Black); + assert_eq!( + w_score, b_score, + "Mirroring + swapping colors must give the same eval (W={}, B-mirror={})", + w_score, b_score + ); + // Sanity: the side with the extra bishop should be ahead. + assert!( + w_score > 0, + "White with extra bishop should evaluate positive, got {}", + w_score + ); + } + + #[test] + fn test_phase_tapers_to_endgame_in_pawn_only_position() { + // Pure K+P vs K endgame: phase contribution is 0, so eval should + // use the endgame piece-square tables exclusively. + let mut board = Board::default(); + board.set( + Square::new(4, 0), + Some(Piece::new(PieceKind::King, Color::White)), + ); + board.set( + Square::new(4, 7), + Some(Piece::new(PieceKind::King, Color::Black)), + ); + board.set( + Square::new(4, 4), + Some(Piece::new(PieceKind::Pawn, Color::White)), + ); + + let (_, _, _, _, phase) = accumulate(&board); + assert_eq!(phase, 0, "K+P vs K must have phase 0 (pure endgame)"); + + let score = evaluate(&board, Color::White); + // White is up a pawn in the endgame — should clearly favour White. + assert!( + score > EG_VALUE[0] - 50, + "Endgame pawn advantage should yield score near pawn EG value, got {}", + score + ); + } + + #[test] + fn test_phase_full_in_starting_position() { + let (_, _, _, _, phase) = accumulate(&Board::starting_position()); + assert_eq!( + phase, PHASE_MAX, + "Starting position must have maximum midgame phase" + ); + } + + #[test] + fn test_bishop_pair_bonus_is_applied() { + // Position A: White has the bishop pair (c1 and f1). + let mut with_pair = Board::default(); + with_pair.set( + Square::new(4, 0), + Some(Piece::new(PieceKind::King, Color::White)), + ); + with_pair.set( + Square::new(4, 7), + Some(Piece::new(PieceKind::King, Color::Black)), + ); + with_pair.set( + Square::new(2, 0), + Some(Piece::new(PieceKind::Bishop, Color::White)), + ); + with_pair.set( + Square::new(5, 0), + Some(Piece::new(PieceKind::Bishop, Color::White)), + ); + + // Position B: same material slot on f1, but bishop pair replaced by bishop + knight. + let mut without_pair = with_pair.clone(); + without_pair.set( + Square::new(5, 0), + Some(Piece::new(PieceKind::Knight, Color::White)), + ); + + let (mg_pair, eg_pair, _, _, _) = accumulate_internal(&with_pair, true); + let (mg_pair_without_bonus, eg_pair_without_bonus, _, _, _) = + accumulate_internal(&with_pair, false); + let (mg_mixed, eg_mixed, _, _, _) = accumulate_internal(&without_pair, true); + let (mg_mixed_without_bonus, eg_mixed_without_bonus, _, _, _) = + accumulate_internal(&without_pair, false); + + assert_eq!( + mg_pair - mg_pair_without_bonus, + BISHOP_PAIR_BONUS_MG, + "Midgame bishop-pair board should receive the exact pair bonus" + ); + assert_eq!( + eg_pair - eg_pair_without_bonus, + BISHOP_PAIR_BONUS_EG, + "Endgame bishop-pair board should receive the exact pair bonus" + ); + assert_eq!( + mg_mixed, mg_mixed_without_bonus, + "Mixed bishop+knight board should not receive a bishop-pair bonus" + ); + assert_eq!( + eg_mixed, eg_mixed_without_bonus, + "Mixed bishop+knight board should not receive a bishop-pair bonus" + ); + } } diff --git a/src/search.rs b/src/search.rs index 7b5ccd3..0e46e7f 100644 --- a/src/search.rs +++ b/src/search.rs @@ -485,7 +485,7 @@ fn score_moves( /// Sort scored moves in descending order. fn sort_moves(scored: &mut [(ChessMove, i32)]) { - scored.sort_unstable_by(|a, b| b.1.cmp(&a.1)); + scored.sort_unstable_by_key(|m| std::cmp::Reverse(m.1)); } // --------------------------------------------------------------------------- @@ -993,7 +993,7 @@ impl SearchEngine { .iter() .map(|mv| (*mv, mvv_lva_score(&pos.board, mv))) .collect(); - scored.sort_unstable_by(|a, b| b.1.cmp(&a.1)); + scored.sort_unstable_by_key(|m| std::cmp::Reverse(m.1)); for (mv, _) in scored { let child = pos.make_move(&mv); @@ -1454,4 +1454,190 @@ mod tests { zobrist::hash_position(&null.board, null.turn, &null.castling, null.en_passant); assert_eq!(null.hash, expected, "Null move incremental hash mismatch"); } + + // ----------------------------------------------------------------------- + // Perft, mate-in-N, and TT-reuse tests + // ----------------------------------------------------------------------- + + /// Minimal FEN parser for tests only. Accepts the four mandatory fields + /// (placement, side, castling, en-passant) and the optional halfmove + /// clock. Panics on invalid input — intentional, tests should use + /// well-formed FENs. + fn position_from_fen(fen: &str) -> SearchPosition { + let parts: Vec<&str> = fen.split_whitespace().collect(); + assert!(parts.len() >= 4, "FEN must have at least 4 fields"); + + let mut board = Board::default(); + let ranks: Vec<&str> = parts[0].split('/').collect(); + assert_eq!(ranks.len(), 8, "FEN placement must have 8 ranks"); + for (row_idx, row) in ranks.iter().enumerate() { + let rank = 7 - row_idx as u8; + let mut file: u8 = 0; + for ch in row.chars() { + if let Some(d) = ch.to_digit(10) { + file += d as u8; + } else { + let piece = + Piece::from_fen_char(ch).unwrap_or_else(|| panic!("bad FEN piece {ch}")); + board.set(Square::new(file, rank), Some(piece)); + file += 1; + } + } + assert_eq!(file, 8, "FEN rank must cover 8 files"); + } + + let turn = match parts[1] { + "w" => Color::White, + "b" => Color::Black, + other => panic!("bad FEN side {other}"), + }; + + let mut castling = CastlingRights::default(); + // CastlingRights::default() may grant all rights; reset and parse. + castling.white.kingside = false; + castling.white.queenside = false; + castling.black.kingside = false; + castling.black.queenside = false; + if parts[2] != "-" { + for ch in parts[2].chars() { + match ch { + 'K' => castling.white.kingside = true, + 'Q' => castling.white.queenside = true, + 'k' => castling.black.kingside = true, + 'q' => castling.black.queenside = true, + other => panic!("bad castling char {other}"), + } + } + } + + let en_passant = if parts[3] == "-" { + None + } else { + Some(Square::from_algebraic(parts[3]).expect("bad EP square")) + }; + + let halfmove = parts + .get(4) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + SearchPosition::new(board, turn, castling, en_passant, halfmove) + } + + /// Counts leaf nodes at the given depth (standard perft). + fn perft(pos: &SearchPosition, depth: u32) -> u64 { + if depth == 0 { + return 1; + } + let moves = pos.legal_moves(); + if depth == 1 { + return moves.len() as u64; + } + let mut total = 0u64; + for mv in moves { + total += perft(&pos.make_move(&mv), depth - 1); + } + total + } + + #[test] + fn perft_startpos_depth_1() { + let pos = starting_pos(); + assert_eq!(perft(&pos, 1), 20); + } + + #[test] + fn perft_startpos_depth_2() { + let pos = starting_pos(); + assert_eq!(perft(&pos, 2), 400); + } + + #[test] + fn perft_startpos_depth_3() { + let pos = starting_pos(); + assert_eq!(perft(&pos, 3), 8_902); + } + + /// Depth-4 startpos perft. Slower (~200k nodes); opt-in via + /// `cargo test -- --ignored`. + #[test] + #[ignore] + fn perft_startpos_depth_4() { + let pos = starting_pos(); + assert_eq!(perft(&pos, 4), 197_281); + } + + #[test] + fn perft_kiwipete_depth_1() { + let pos = position_from_fen( + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + ); + assert_eq!(perft(&pos, 1), 48); + } + + #[test] + fn perft_kiwipete_depth_2() { + let pos = position_from_fen( + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + ); + assert_eq!(perft(&pos, 2), 2_039); + } + + /// Kiwipete depth-3 perft (~98k nodes). Opt-in via + /// `cargo test -- --ignored`. + #[test] + #[ignore] + fn perft_kiwipete_depth_3() { + let pos = position_from_fen( + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + ); + assert_eq!(perft(&pos, 3), 97_862); + } + + /// Verify search prefers an immediate back-rank mate and returns a + /// mate-range score for the position `6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1`. + #[test] + fn test_search_finds_back_rank_mate_in_one() { + // Back-rank mate: White rook on a1, Black king trapped on g8 by own pawns. + // FEN: `6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1` + // 1.Ra8# is mate (Black king has no escape — f7, g7, h7 blocked). + let pos = position_from_fen("6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1"); + let mut engine = SearchEngine::with_defaults(); + let result = engine.search(&pos, 4); + assert!(result.best_move.is_some()); + let mv = result.best_move.unwrap(); + // Best move should land the rook on the 8th rank to deliver mate. + assert_eq!( + mv.to.rank, 7, + "Mating move should be a rook lift to the 8th rank, got {:?}", + mv + ); + assert!( + result.score > MATE_THRESHOLD, + "Score must be in mate range, got {}", + result.score + ); + } + + /// Verify the transposition table is actually reused across iterative + /// deepening iterations and across two consecutive searches. + #[test] + fn test_tt_reuse_across_searches() { + let pos = starting_pos(); + let mut engine = SearchEngine::with_defaults(); + + // First search at depth 4 populates the TT. + let r1 = engine.search(&pos, 4); + assert!(r1.stats.tt_hits > 0, "depth-4 ID must accumulate TT hits"); + + // Second search at the same depth should benefit even more: every + // root child has its TT entry available immediately. + let r2 = engine.search(&pos, 4); + assert!( + r2.stats.tt_hits >= r1.stats.tt_hits / 2, + "second search should retain meaningful TT reuse (r1={}, r2={})", + r1.stats.tt_hits, + r2.stats.tt_hits, + ); + } } diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 727a387..c6afae7 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -16,7 +16,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "checkai-wasm" -version = "0.5.2" +version = "0.6.0" dependencies = [ "console_log", "js-sys", diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index c5cec86..bfa6898 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "checkai-wasm" -version = "0.5.2" +version = "0.7.0" edition = "2024" description = "CheckAI chess engine compiled to WebAssembly — use as a Node.js CLI or library" license = "MIT" diff --git a/web/package.json b/web/package.json index a2c4748..6a53146 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@checkai/web", - "version": "0.5.2", + "version": "0.7.0", "packageManager": "bun@1.3.10", "private": true, "type": "module",