diff --git a/.agent/PROJECT_STATE.md b/.agent/PROJECT_STATE.md index aaffa18..aa54c00 100644 --- a/.agent/PROJECT_STATE.md +++ b/.agent/PROJECT_STATE.md @@ -1,10 +1,10 @@ # Project state -Last updated: 2026-08-13 +Last updated: 2026-08-26 ## Current phase -The public foundation, Registry startup vertical slice, human-readable Diff, and portable Windows x64 Developer Preview are on `main`. PR #12 on `feat/windows-services` implements Issue #11's second real Collector while preserving the read-only and no-false-removal boundaries. Implementation, independent review, and local/real-Windows validation are complete; merge remains a maintainer decision. v0.1 remains incomplete. +The public foundation, Registry startup and Windows Services Collectors, human-readable Diff, and portable Windows x64 CLI Developer Preview are on `main`. The current `feat/desktop-capture-workflow` work implements Issue #13's first guided desktop vertical slice while reusing the existing Rust capture, Diff, and report semantics. It remains a source-built development app rather than a distributed desktop preview. v0.1 remains incomplete. ## Implemented components @@ -13,24 +13,27 @@ The public foundation, Registry startup vertical slice, human-readable Diff, and - Draft v1 Snapshot and Diff documents with bounded/header-first Snapshot input, strict UTC timestamps, deterministic serialization, and deliberate Collector-version compatibility checks. - Coverage-aware comparison: incomplete, unavailable, unsupported, or permission-denied scope coverage cannot silently become a Removed finding. - `windows.registry.startup` v1 using query-only Win32 Registry APIs, explicit Registry views, scoped diagnostics, bounded mutation/resource handling, strict native-data decoding, lossless UTF-16 value names, and complete-value SHA-256. -- On the current Issue #11 branch, `windows.services` v1 uses query-only SCM APIs, atomic selected-field observations, strict UTF-16 handling, bounded resources/mutation reads, and permanently conservative current-token partial coverage. +- `windows.services` v1 uses query-only SCM APIs, atomic selected-field observations, strict UTF-16 handling, bounded resources/mutation reads, and permanently conservative current-token partial coverage. - `systemdiff snapshot -o ` with canonical UTC metadata, bounded serialization, and create-new output semantics. - Default human-readable Registry and Services Diff output, explicit `--technical` evidence output, and unchanged `--json` machine output. - A release-mode Windows x64 Developer Preview pipeline with packaging-only static MSVC CRT, an explicit `asInvoker` manifest, exact package/checksum verification, and a later artifact-download smoke job. - Registry-only synthetic before/after fixtures and a dual-gated test-only real HKCU E2E. The real E2E observed exactly one expected Added startup value, zero Removed changes, and verified exact-data cleanup; production Rust has no Registry write path. - A truthful Registry-only README demo whose transcript is regression-tested and whose static visual is derived from that exact output. +- On the current Issue #13 branch, a Tauri 2 + React/TypeScript desktop development app provides a single-session Ready → Capturing → Results workflow in `en-US` and `zh-CN`. Rust owns capture, Diff semantics, coverage, technical evidence, session state, bounded backend-only temporary storage, and cleanup; React owns localization and layout. +- The desktop ordinary presentation uses a versioned locale-neutral DTO, stable semantic IDs, fixed Startup/Windows Services groups, calm coverage notices, and on-demand exact technical text without exposing Snapshot paths or raw documents to the web frontend. - Project-scoped Codex agents, three repeated-workflow skills, living ExecPlans, architecture/format/Collector/threat-model documentation, and synthetic cross-platform tests. ## Known limitations -- Registry Run/RunOnce is implemented on `main`; Services is implemented on the current feature branch. Scheduled Tasks, rules/explanations, sanitization, an installer, and the desktop app are unavailable. +- Registry Run/RunOnce and Windows Services are implemented on `main`. Scheduled Tasks, rules/explanations, sanitization, and an installer remain unavailable. - There is no official binary Release or Authenticode signing. The Developer Preview is an expiring GitHub Actions artifact that requires GitHub sign-in, and clean-machine validation remains a gate for an official alpha. - The current minimum is Windows 10 version 1709 or Windows Server 2016 version 1709. ARM64 v1 collects HKCU Shared scopes but reports HKLM alternate-view coverage as unsupported until those views are represented and tested. - Snapshot files are unredacted and can contain usernames, service accounts, paths/arguments, descriptions, software details, and other sensitive host evidence. They must be reviewed before sharing. - Draft v0.1 diffs assume the same Windows installation and the same user/principal context. Cross-host and cross-user identity are intentionally out of scope. - Registry lookup is case-insensitive, but Collector v1 identity uses exact UTF-16 units because no documented durable cross-platform canonical token is available. A returned casing change can appear as a visible Removed + Added pair. - SCM service-name comparison is also case-insensitive. Services v1 preserves exact returned UTF-16 evidence and accepts the same conservative casing-only false-split limitation rather than applying unverified Unicode/NLS normalization. -- The desktop app is a proposed future boundary, not a generated Tauri application. +- The desktop app is currently development-only: there is no signed/bundled installer, desktop CI artifact, updater, history, import/export, or clean-machine WebView2 bootstrap. Windows 10 1709 is not yet a validated desktop distribution baseline; this slice requires an installed WebView2 Runtime. +- Desktop session Snapshots are unredacted sensitive local evidence. Normal finish/cancel/new-capture/stable-exit paths remove verified files and Results surface cleanup failure, while in-flight exit or crash recovery is conservative and may leave local evidence for the next startup. - No dedicated private Code of Conduct reporting channel is published. GitHub Private Vulnerability Reporting is only for product security reports. - No CODEOWNERS file is committed during the solo-maintainer stage. @@ -47,10 +50,12 @@ The public foundation, Registry startup vertical slice, human-readable Diff, and - Normal changes to `main` go through pull requests and the two required checks: `Rust (windows-latest)` and `Rust (ubuntu-latest)`. - Developer Preview packaging runs only after those gates on trusted upstream `push` events, uploads an exact ZIP/checksum pair for 14 days, and verifies the downloaded artifact in a fresh Windows job. Normal artifacts come from `main`; the exact active feature branch may temporarily produce a clearly named candidate for pre-merge validation. Fork pull requests cannot enter this upload path. - The portable build alone uses static MSVC CRT and remains at version `0.0.0`. The package is commit-linked and hashed but is not claimed to be reproducible, signed, released, or permanently downloadable. +- ADR 0003 accepts Tauri 2 + React/TypeScript for the desktop. The app has a separate Cargo workspace/lockfile so Tauri dependencies cannot pollute the CLI package graph, and CI validates frontend and desktop Rust work explicitly. +- Desktop IPC is local-only and capability-scoped: no shell, filesystem, HTTP, dialog, opener, updater, remote-content, arbitrary-path, or Registry/service write command is exposed. Synchronous capture runs on blocking workers behind a Rust-owned single-session state machine. ## Next milestone -Evaluate and merge PR #12 for Issue #11 when its authoritative GitHub checks remain green. Scheduled Tasks remains intentionally unstarted; a signed, permanent public download remains future release work. +Open and validate the PR for Issue #13's independently reviewed guided desktop development workflow, then decide whether it is ready to merge. Scheduled Tasks remains intentionally unstarted; desktop packaging, clean-machine WebView2 handling, signing, and a permanent public download remain future release work. ## Major unresolved questions @@ -59,4 +64,4 @@ Evaluate and merge PR #12 for Issue #11 when its authoritative GitHub checks rem - What explicitly versioned identity upgrade should eventually address Registry value-name casing without hiding raw evidence or coupling Diff to mutable Windows NLS behavior? - What bounded/archive policy should apply to Scheduled Task raw XML before that Collector is implemented? - What minimum supported Rust version will be tested and documented? -- Should the first desktop spike confirm React/Vite or compare one smaller frontend alternative before accepting ADR 0003? +- What desktop packaging and WebView2 Runtime strategy should be validated before offering a public desktop artifact? diff --git a/.agent/plans/desktop-ui-spike.md b/.agent/plans/desktop-ui-spike.md new file mode 100644 index 0000000..8c26deb --- /dev/null +++ b/.agent/plans/desktop-ui-spike.md @@ -0,0 +1,299 @@ +# First guided desktop capture workflow + +Status: In progress +Owner: Primary agent +Last updated: 2026-08-26 + +## Goal + +Turn the already validated SystemDiff capture-and-diff core into its first ordinary-user desktop workflow. The change answers one question—"What changed on my Windows PC?"—without requiring PowerShell, JSON files, file pickers, Snapshot terminology, or manual before/after bookkeeping. + +This plan implements GitHub Issue #13. It deliberately pauses collector expansion and proves a narrow Windows desktop vertical slice before the project invests in history, dashboards, installers, or additional platform coverage. + +## User-visible outcome + +A user can open a real Windows desktop window and complete this guided flow: + +1. Select **Start capture** on a calm Ready screen. +2. Install, run, or change the software they want to inspect. +3. Select **Finish & Compare**. +4. Read grouped Startup and Windows Services results, with plain-language change descriptions and an optional Technical details disclosure. +5. Start a new capture or cancel safely. + +The workflow is available in English and Simplified Chinese based on the user's browser/Windows language. It uses local, read-only SystemDiff collectors, never asks for elevation, and performs no network or telemetry activity. + +## Current architecture and context + +- `systemdiff-windows::capture_snapshot` is the in-process synchronous orchestration boundary for the implemented Registry startup and Windows Services collectors. Desktop must call it directly and must not shell out to `systemdiff.exe`. +- `systemdiff-diff::diff_snapshots` owns compatibility, deterministic identity, coverage-aware Added/Removed/Modified/Inconclusive classification, and the invariant that incomplete coverage cannot create a false removal. +- `systemdiff-report` already owns terminal-facing human and technical evidence mapping. A narrow locale-neutral desktop presentation module belongs there so Rust, not TypeScript, continues to own meaning. +- The CLI's timestamp, bounded serialization, input loading, and explicit output-path workflow are private to `systemdiff-cli`. The desktop needs its own backend-only ephemeral session/storage boundary; the CLI workflow remains unchanged. +- Snapshot and Diff wire schema v1 are not changed. The desktop presentation DTO is a separate, versioned IPC contract. +- Real Windows Services capture is intentionally `Partial` because SCM enumeration can omit protected services. The desktop presents that as a calm visibility notice while technical details retain exact coverage and diagnostics. +- The broad `before-v1.json` fixture contains planned Scheduled Task evidence and is not suitable for capability claims or screenshots. Desktop tests use focused Registry/Services data. +- The existing portable artifact is CLI-only. A desktop development build must not silently replace or contaminate its packaging, dependency notice, or verification pipeline. + +### Phase 0 OSS reference study + +The study was limited to six public projects and extracted interaction/architecture principles only. No artwork, icons, layouts, or assets will be copied. + +1. **LocalSend** — its Receive/Send workflow hides discovery, networking, and transfer protocol details behind one task and a small number of durable states. Adopt the low cognitive load and truthful waiting/progress surfaces; reject its persistent navigation and transfer-specific percentages. +2. **Files** — its Windows 11 typography, spacing, subtle content surfaces, local banners, and operation feedback feel familiar without explaining a new data model. Adopt surface hierarchy and page-local notices; reject tabs, dual panes, command palettes, and an extensible file-manager shell. +3. **DevToys** — its constrained UI components and Smart Detection keep tool internals behind consistent input/output presentation. Adopt stable typed presentation and restrained Fluent patterns; reject the tool catalog, marketplace, split editor, and developer-dense layout. +4. **UniGetUI** — it successfully normalizes multiple CLI package managers into recognizable objects and actions. Adopt the principle that a GUI can rename and progressively disclose existing core capability without reimplementing it; reject raw tables, bulk selection, filters, and many toolbar commands. +5. **Spacedrive** — its Rust core to typed TypeScript boundary keeps frontend code from redefining backend semantics. Adopt a checked narrow DTO; reject its daemon, persistent library/location model, CQRS scale, job history, and onboarding tax. +6. **WinUtil** — it demonstrates discoverable Windows terminology and standard utility affordances. Adopt only recognizable Windows language/help cues; explicitly reject its admin-first, write-heavy, high-density toolbox and preset aesthetic. + +Public sources reviewed include each project's current repository README/screenshots and relevant public UI/architecture documentation: LocalSend `home_page.dart` and transfer progress, Files v4 status-center documentation, DevToys GUI/Smart Detection guidance, UniGetUI pages and CLI reference, Spacedrive architecture guide, and WinUtil user guide/architecture. + +### Adopted principles + +1. **One job, three durable states.** Ready → Capturing → Results; busy and recoverable error are transient substates, not new product areas. +2. **Five-second primary action.** The Ready screen has one dominant call to action and one sentence explaining the product. +3. **Linear guidance, not domain training.** Say “start recording, make a change, finish and compare,” not Snapshot, Collector, schema, scope, or canonical identity. +4. **Progressive evidence disclosure.** Ordinary view shows the recognizable object, what changed, useful value, and location. Raw UTC, collector status/diagnostics, hashes, and exact evidence live in Technical details. +5. **Truthful activity feedback.** Capture/compare use an indeterminate busy state; no fabricated percentage or instant-cancel claim. +6. **Coverage is a notice, not an error.** Account visibility limits remain visible and factual but do not become a red error page or a confirmed removal. +7. **Rust owns meaning; React owns language and layout.** Change classification, changed service fields, coverage mapping, and session transitions are decided in Rust and represented by stable semantic IDs. +8. **Real workflow as presentation.** README imagery comes only from the working implementation and shows the core task, not future mock functionality. + +### Explicit anti-patterns + +1. Empty app shells: unused sidebar entries, dashboards, settings centers, metrics, or charts. +2. Raw evidence as the default interface: tables of collectors, schemas, canonical IDs, JSON, or raw UTC timestamps. +3. Scareware semantics: fake security scores, malicious/safe claims, warning walls, hacker styling, or AI gradients. +4. Dishonest state: fake percentages, silent long-running work, treating Partial as failure, or treating Inconclusive as Removed. +5. Premature platform architecture: daemon/CQRS/history, plugin UI framework, toolbox navigation, large state libraries, or generic filesystem/shell IPC. + +### SystemDiff information hierarchy + +```text +SystemDiff +└── Promise: See what changed on your Windows PC. + ├── Ready + │ ├── Start capture + │ ├── Checks: Startup entries; Windows services; Scheduled tasks — Coming soon + │ └── Trust: Local only · Read-only · No telemetry + ├── Capturing + │ ├── Before capture completed + │ ├── Install, run, or change the app + │ ├── Finish & Compare + │ └── Cancel + ├── Busy / recoverable error + │ ├── Honest indeterminate activity or short explanation + │ ├── Retry / Return + │ └── Collapsed technical details + └── Results + ├── Confirmed change summary + ├── Startup group + ├── Windows Services group + ├── Calm coverage notice + ├── Optional exact Technical details + └── New capture +``` + +### Tauri spike assessment and decision + +Adopt Tauri 2.11.5 with React 19, TypeScript, and Vite. As of 2026-08-20 it is an actively maintained MIT/Apache-2.0 project, supports the current Rust toolchain, uses the system WebView, and provides the narrow Rust command boundary this repository needs. React is reasonable for future localization and contributor access, but this slice adds no router, global state library, design-system runtime, or large i18n framework. + +Microsoft documents WebView2 support for Windows 10 SAC 1709 and later, so there is no known API-level blocker. Tauri notes that WebView2 is normally preinstalled only on Windows 10 1803 and later; a clean 1709 machine therefore cannot be assumed to have the runtime. This development slice targets machines with the Evergreen Runtime installed and does not claim installer/bootstrap coverage or fully validate the 1709 baseline. Formal distribution must later test missing-Runtime behavior and choose a Runtime installation strategy. + +The local validated prerequisites are Rust 1.97.1 stable MSVC, Node 22.23.1/npm 12.0.1, VS 2022 MSVC tools 14.41, Windows SDK 22621, and WebView2 Runtime 151.0.4129.93. The project will use a locked local `@tauri-apps/cli` dev dependency rather than a global `cargo-tauri` installation. + +Security decision: + +- bundle only local Vite assets and set an explicit production CSP; +- one `main` window, no remote URLs, remote fonts/scripts/iframes, asset protocol, or global Tauri injection; +- no shell, filesystem, HTTP, dialog, opener, updater, or network plugin; +- expose only typed session commands with no path, URL, shell command, raw Snapshot JSON, or generic read/write input; +- use an explicit Tauri capability for the main window and restrict registered application commands at build time; +- run synchronous capture/diff work through `tauri::async_runtime::spawn_blocking` and enforce concurrency in Rust state, not only disabled buttons; +- render observed strings as React text nodes, never `dangerouslySetInnerHTML`; +- keep `asInvoker`; release builds use the Windows GUI subsystem so double-click does not flash a console. + +## Constraints + +- Production remains read-only, offline-first, non-elevated, and without telemetry, accounts, cloud, or network clients. +- Never execute or expand observed Registry/service command data. +- Frontend cannot supply an arbitrary filesystem path or receive a temporary evidence path. +- Temporary Snapshots are unredacted sensitive evidence. Use an application-local owned root, collision-safe create-new operations, exact known filenames, path-containment/reparse checks, and explicit cleanup. Do not recursively delete broad or unverified paths. +- Session state is single-active-session and backend-authoritative. Duplicate or invalid transitions return stable typed conflicts. +- `spawn_blocking` work cannot be promised to cancel immediately. Cancel discards a completed before capture; a currently executing native capture is allowed to finish safely before cleanup/state recovery. +- Preserve raw UTC in technical evidence. React may render the already validated timestamps in local time for ordinary view. +- Do not change Snapshot/Diff v1, collector behavior, CLI commands, Registry/service coverage, or existing CLI package contents. +- Keep the desktop Rust package and lockfile separate from the root Cargo workspace so Tauri dependencies do not enter CLI portable packaging metadata or `THIRD_PARTY_LICENSES.txt`. Validate it explicitly in CI. Path dependencies still reuse the shared Rust crates. +- Default CI must not write Registry data or require administrator privileges. +- Scheduled Tasks, history/baselines, settings, dashboard, charts, risk/signature/rules/AI, redaction/export, updater, installer/signing, and official Release are non-goals. + +## Implementation steps + +1. Create Issue #13, record this plan, create `feat/desktop-capture-workflow`, and accept ADR 0003 with the documented WebView2/runtime and security limits. +2. Add a locale-neutral, serializable desktop presentation model to `systemdiff-report`. It includes a contract version, raw UTC bounds, Rust-owned counts/change kinds, stable group/message/field IDs, display evidence, coverage notices, and on-demand technical text. Add deterministic semantic tests for Registry, Services, coverage, empty, fallback, and hostile evidence cases. +3. Scaffold `apps/desktop` with locked Tauri 2/React/TypeScript/Vite dependencies. Keep the Rust package outside the root workspace with its own lockfile and validation. Audit direct and resolved dependency licenses/security before claiming completion. +4. Implement a pure Rust desktop session state machine and storage layer. Use backend-created app-local session roots, bounded Snapshot serialization/loading, create-new exact filenames, cleanup on finish/cancel/new capture, and conservative startup recovery of only verified owned entries. +5. Expose narrow async Tauri commands for state, start, finish, cancel, and technical details. Perform capture/diff in a blocking worker, prohibit concurrent operations in Rust, and return a small typed recoverable error model. +6. Implement the three-state React UI with typed localization dictionaries for `en-US` and `zh-CN`, navigator-language selection, local timestamp display, an honest indeterminate busy state, duplicate-action prevention, calm coverage notices, and expandable technical evidence. Use restrained Fluent-inspired CSS with responsive light/dark support and no unused navigation. +7. Add a guarded mutation-only HKCU Run dogfood harness. It refuses an existing `SystemDiffDogfood` value, writes only the exact synthetic value under explicit test gates, removes only exact matching data, and is not linked into production Rust/Tauri code. +8. Update CI to validate npm lock install, TypeScript checks, frontend build, desktop Rust fmt/clippy/tests, and a Windows Tauri no-bundle build while preserving existing root Rust and CLI portable jobs. Do not publish a desktop artifact or official bundle in this PR. +9. Run the real desktop app on Windows. Verify Ready → Start → mutation → Finish → Results, no UI freeze, correct Registry Added evidence, exact cleanup, English and Chinese UI, release double-click/no-console behavior, and technical coverage. Capture a real screenshot only after this passes. +10. Update both READMEs, `apps/desktop/README.md`, architecture/roadmap/threat model, ADR 0003, and `PROJECT_STATE.md` with factual implemented/dev-only status and the real screenshot. +11. Run full local validation, an independent reviewer focused on product/security/lifecycle/semantics, and a stranger-first manual test. Fix High/Medium findings and only actionable Low findings within scope. +12. Inspect the final diff, use the `systemdiff-pr` workflow, commit, push the feature branch, create a PR closing #13, and wait for all required and desktop CI checks to pass. Do not merge. + +## Affected files and modules + +- `.agent/plans/desktop-ui-spike.md` and `.agent/PROJECT_STATE.md` +- `docs/adr/0003-desktop-stack.md`, `docs/architecture.md`, `docs/roadmap.md`, `docs/threat-model.md` +- `crates/systemdiff-report/src/lib.rs` and a focused presentation module/tests +- `crates/systemdiff-core/src/lib.rs` and `crates/systemdiff-windows/src/registry.rs` only for equivalent current-stable Clippy compatibility fixes discovered by remote CI +- `apps/desktop` frontend, Tauri backend, separate Cargo/npm lockfiles, and developer documentation +- `.github/workflows/ci.yml` +- a guarded test-only desktop dogfood script under `scripts/` +- `README.md`, `README.zh-CN.md`, and a real screenshot under `docs/assets/` + +No Collector, Snapshot/Diff schema, rule, risk, CLI workflow, or portable CLI package content is expected to change. + +## Test strategy + +### Rust presentation + +- Registry Added maps to the stable Startup group, Rust-owned Added semantics, decoded command, and exact location. +- Service Modified lists only fields that actually changed. +- One-sided evidence under partial coverage remains Inconclusive, confirmed count remains zero, and a calm Services visibility notice is emitted. +- Empty results still contain stable Startup and Windows Services groups/empty-state IDs. +- Unknown artifact fallback is visible and never panics or disappears. +- Control and bidi characters in evidence are display-hardened; exact technical evidence remains available. +- DTO semantic/message IDs and JSON field names are deterministic and localization-neutral. + +### Session and storage + +- Ready → busy → Capturing → busy → Results and New capture. +- Duplicate/illegal Start, Finish, Cancel, and technical-detail transitions. +- Capture, diff, validation, serialization, load, and cleanup failure recovery. +- Create-new collisions and bounded evidence input/output. +- Cancel, successful finish, new capture, startup recovery, and window-close cleanup. +- Owned-root containment, exact allowlist, unexpected entry, symlink/reparse-point, and cleanup-error behavior. +- Raw UTC is preserved and no frontend-supplied path enters storage. + +### Frontend + +- Typecheck and production Vite build are mandatory. Add only a lightweight lint/test tool if it does not materially expand dependencies. +- Manually verify Ready, scanning, Capturing, comparing, Results, empty results, Registry Added, Service Modified, Inconclusive/coverage, and recoverable error. +- Verify `en-US`, `zh-CN`, locale fallback, local-time rendering, responsive width, light/dark preference, keyboard focus, and busy-button disabling. + +### Windows integration and dogfood + +- Real window launch through Tauri and a release `--no-bundle` build. +- GUI Start → guarded HKCU `SystemDiffDogfood` mutation → GUI Finish → exact Added startup result → exact-data cleanup. +- No administrator privileges, production write API, CLI workflow, JSON, file picker, or console is required for the user path. +- Inspect PE subsystem/no-console and run the generated executable with installed WebView2. + +### Commands + +```powershell +cargo fmt --all --check +cargo clippy --locked --workspace --all-targets -- -D warnings +cargo test --locked --workspace --all-targets + +npm ci --prefix apps/desktop +npm run check --prefix apps/desktop +npm run build --prefix apps/desktop + +cargo fmt --manifest-path apps/desktop/src-tauri/Cargo.toml --all --check +cargo clippy --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets -- -D warnings +cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets +npm run tauri:build --prefix apps/desktop +``` + +The exact CI and manual dogfood commands will be recorded with observed output in Final validation. No success will be inferred. + +## Risks + +- **UI freeze or duplicate capture:** synchronous Win32 collectors can block. Use async commands plus `spawn_blocking` and an atomic Rust session transition before work begins. +- **Sensitive temp evidence left behind:** create only under an owned root, use create-new exact names, clean explicitly, and conservatively recover verified stale directories. Do not claim LocalAppData is encryption. +- **False removal in the UI:** presentation must consume `ChangeKind` and coverage from Rust and never reclassify in TypeScript. +- **WebView2 unavailable on old Windows:** Win10 1709 may require Evergreen Runtime. Keep this PR dev-only and do not claim clean-machine distribution support. +- **IPC/content escalation:** remote content, wide capabilities, generic paths, shell/fs/http plugins, or unescaped HTML would expand trust. Reject them and review the generated capability/CSP. +- **CLI artifact regression:** a Tauri workspace dependency graph can pollute existing CLI license/package checks. Use a separate desktop Cargo lock/workspace boundary and explicitly rerun existing package/download verification. +- **Dependency size/advisories:** audit actual Cargo/npm lockfiles; do not hide High/Medium issues behind a generic Tauri allowlist. +- **Crash during native capture:** blocking native work is not instantly abortable. Do not promise it; serialize session operations and finish cleanup when the task returns. +- **Misleading screenshot:** take it only from the validated real app with current Registry/Services capability and Scheduled Tasks visibly marked Coming soon. + +## Rollback and compatibility + +The desktop app is additive and development-only. Reverting its app directory, presentation DTO, documentation, and desktop CI leaves the CLI, Collectors, Snapshot/Diff schema, fixtures, and existing portable artifact unchanged. There is no user database or migration. Ephemeral session files are not a supported interchange format and are deleted after use. + +The new presentation IPC model begins at contract version 1 but is not the public Snapshot/Diff wire schema. Future desktop compatibility changes must be deliberate and frontend/backend versions ship together. ADR 0003 records the durable stack choice and may be superseded rather than rewritten if the desktop architecture later changes. + +## Progress + +- [x] 2026-08-20: Synced clean `main` at `ff51d9433bd605406543f59ec8a10054fb4e442a` with `origin/main`. +- [x] 2026-08-20: Completed six-project stranger-first UI/architecture research and recorded adopted/rejected patterns above. +- [x] 2026-08-20: Verified Tauri 2.11.5/WebView2 compatibility, security constraints, current dependency state, and local Windows prerequisites. +- [x] 2026-08-20: Mapped capture, diff, report, session, CI, fixtures, packaging, and documentation boundaries. +- [x] 2026-08-20: Created GitHub Issue #13 for the guided desktop workflow. +- [x] 2026-08-20: Created `feat/desktop-capture-workflow` and implemented the locale-neutral presentation contract in `systemdiff-report`. +- [x] 2026-08-21: Implemented and tested the backend-only session storage/state machine, root lock, conservative recovery, and five no-argument Tauri commands. +- [x] 2026-08-21: Implemented and validated the bilingual React UI, responsive Fluent-inspired styling, local-time display, honest busy/error states, and on-demand technical evidence. +- [x] 2026-08-21: Completed three real Windows guarded dogfood runs, exact Registry cleanup, GUI subsystem verification, and a real English Results screenshot. +- [x] 2026-08-26: Completed final local validation and independent review with no remaining High/Medium findings. +- [ ] Commit, push, create the PR, and confirm authoritative remote CI. + +## Discoveries + +- The current CLI package license verifier considers all non-workspace packages returned by root Cargo metadata. Adding Tauri to the root workspace would contaminate CLI-only dependency notices even if the CLI does not link Tauri. A separate desktop Cargo workspace/lockfile is the smallest non-regressive boundary for this slice. +- Windows Services capture is intentionally Partial because successful SCM enumeration can omit inaccessible services. A calm coverage notice is expected during normal desktop use, not an exceptional error state. +- Tauri's `spawn_blocking` work generally cannot be aborted after it starts. UI Cancel semantics must not claim immediate native cancellation. +- WebView2 supports Windows 10 1709 at the platform level, but only 1803+ is normally preinstalled. A naked development executable is not a clean-machine distribution strategy. +- `localhost` and a Vite server explicitly bound to `127.0.0.1` are not necessarily the same endpoint on an IPv6-preferring host; keep the Tauri development URL and Vite bind address exactly aligned. +- A permanent zero-byte root lock file is intentionally not session evidence. The process holds an advisory file lock for its lifetime so a second instance cannot run stale-session recovery against an active capture. +- GitHub's `stable` channel advanced to Rust/Clippy 1.98.0 during final validation and enabled `chunks_exact_to_as_chunks` under `-D warnings`. Replacing the two existing fixed-width decode iterators with Clippy's equivalent `as_chunks` form keeps stable CI green without pinning an obsolete toolchain or changing decoding semantics. +- The desktop crate intentionally links the Tauri runtime only on Windows while keeping pure session/storage tests cross-platform. Its build script must mirror that boundary: invoking Tauri manifest generation on Ubuntu expects runtime build metadata that cannot exist there. The non-Windows build script is therefore a no-op; Ubuntu still compiles, lints, and tests the backend code. +- npm 10 and npm 12 forward nested `--` arguments differently. Keeping `--no-bundle` inside a locked `tauri:build` package script makes the local and GitHub Windows build command deterministic instead of relying on version-specific CLI argument forwarding. + +## Decisions + +- Accept ADR 0003: Tauri 2 + React + TypeScript is the desktop stack, subject to the local-only, narrow-IPC, installed-WebView2 boundary in this plan. +- Keep desktop session orchestration inside the desktop Rust backend until another consumer exists; do not add a general session crate. +- Put locale-neutral presentation semantics in `systemdiff-report`, the existing report/presentation owner. +- Keep the desktop Rust package outside the root Cargo workspace and validate it explicitly so the existing CLI artifact remains unchanged. +- Keep frontend localization as small typed dictionaries. Do not add a router, state library, design system, or general i18n framework. +- Persist ephemeral before/after evidence only in a backend-owned app-local directory because the requested acceptance explicitly requires collision-safe local storage and cleanup. Do not expose paths to React or add history. +- Use a real screenshot rather than a GIF if animation tooling would enlarge scope. + +## Final validation + +Local toolchain used on 2026-08-21: + +- `rustc 1.97.1 (8bab26f4f 2026-07-14)`, Cargo 1.97.1, rustfmt 1.9.0-stable, Clippy 0.1.97; +- Node.js 22.23.1 and npm 12.0.1; +- WebView2 Runtime 151.0.4129.93 in the validated development environment. + +Completed local checks: + +- `npm ci --prefix apps/desktop`: passed; 152 packages installed and 153 audited by the install command. +- `npm run check/lint/build --prefix apps/desktop`: all passed after review fixes; the final Vite build emitted a 216.95 kB JavaScript asset (67.07 kB gzip) and 9.41 kB CSS asset (2.99 kB gzip). +- `npm audit --prefix apps/desktop --audit-level=low --json`: zero info/low/moderate/high/critical vulnerabilities in the locked npm graph. +- root `cargo fmt --all --check` and `cargo clippy --locked --workspace --all-targets -- -D warnings`: passed. +- root `cargo test --locked --workspace --all-targets`: 119 passed, zero failed. +- after remote Clippy 1.98 exposed the new lint in two pre-existing fixed-width decoders, the equivalent iterator updates passed root fmt, Clippy, and all 119 tests locally; authoritative 1.98 confirmation is delegated to the replacement GitHub run. +- after Ubuntu exposed the over-broad Tauri build-script invocation, gating manifest/command generation to Windows passed desktop fmt, Clippy, tests, and a Windows no-bundle release build locally; authoritative Ubuntu confirmation is delegated to the replacement GitHub run. +- desktop Cargo fmt/Clippy checks: passed; desktop `cargo test --locked ... --all-targets`: 16 passed, zero failed, including managed bootstrap errors, storage-initialization routing, visible cleanup state, and normal/deferred shutdown semantics. +- `npm run tauri:build --prefix apps/desktop`: passed; produced `apps/desktop/src-tauri/target/release/systemdiff-desktop.exe`, 8,930,304 bytes. Direct PE parsing reported PE32+ magic `0x020B` and subsystem `2` (Windows GUI, no console subsystem). +- CLI human Diff, JSON Diff, and Collector-list smoke commands: passed. +- existing CLI package and downloaded-artifact-style verifier: passed. It retained the exact two-file outer artifact and five-file portable ZIP, AMD64/asInvoker/uiAccess=false/static-CRT import boundary, all CLI modes, and a real read-only Snapshot smoke. +- desktop Cargo metadata contained 425 registry packages across its all-platform locked graph, with zero missing license metadata and zero Git dependencies. `cargo-audit` was not installed, so no RustSec success is claimed; nested Cargo and npm Dependabot entries were added. +- guarded desktop dogfood script parsed successfully; Markdown relative-link check passed across 33 files; secret/local-path pattern scans found no hits; `git diff --check` passed after final review fixes. + +Real Windows dogfood (run three times): + +1. launched the release GUI directly without a console and confirmed Ready was understandable in both Chinese and English; +2. used the GUI **Start capture** action and reached the responsive Capturing state; +3. separately used the dual-gated test-only harness to add exact `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` value `SystemDiffDogfood` with data `cmd.exe /d /c exit 0`; +4. used the GUI **Finish & Compare** action and saw exactly `1 change detected`, `SystemDiffDogfood`, `Added to your startup`, the expected location and command, no observed Service configuration changes, and a calm current-account coverage notice; +5. expanded Technical details and confirmed raw UTC, one Added/zero Removed/zero Modified/zero Inconclusive, exact Registry evidence, all Registry scope coverage, conservative Services coverage, diagnostics, and completed desktop-session evidence cleanup; +6. the harness removed only the exact matching value and verified cleanup. A subsequent Registry read confirmed the value absent. After application exit the session root contained no session directory, only its zero-byte lock file. + +The real screenshot is `docs/assets/systemdiff-desktop-results.jpg` (982×752, 54,990 bytes) and contains only the guarded synthetic evidence. Both READMEs use it while clearly labeling the desktop as source-built development software, not a distributed Release. + +The independent reviewer initially found four Medium lifecycle/UX issues and one residual Medium bootstrap-error routing issue. The implementation now surfaces cleanup-pending state in the ordinary UI, avoids an incorrect no-change claim for inconclusive-only results, synchronously guards duplicate commands, cleans stable sessions on normal exit, and treats cached bootstrap failures as restart-required rather than offering a false retry. Regression tests cover the fixes. The final independent re-review reported no High or Medium findings; the only remaining Low is a visible but non-obscuring pointer halo in the real screenshot. Authoritative PR checks remain pending and will be appended before the plan is complete. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8505c8f..aec0b3d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,24 @@ updates: timezone: Asia/Shanghai open-pull-requests-limit: 5 + - package-ecosystem: cargo + directory: /apps/desktop/src-tauri + schedule: + interval: weekly + day: monday + time: "09:10" + timezone: Asia/Shanghai + open-pull-requests-limit: 5 + + - package-ecosystem: npm + directory: /apps/desktop + schedule: + interval: weekly + day: monday + time: "09:20" + timezone: Asia/Shanghai + open-pull-requests-limit: 5 + - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c920fa9..735bf53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,8 +4,8 @@ on: push: branches: - main - # Narrow pre-merge candidate validation for Issue #11. - - feat/windows-services + # Narrow pre-merge candidate validation for Issue #13. + - feat/desktop-capture-workflow pull_request: permissions: @@ -56,10 +56,65 @@ jobs: - name: List Collector status run: cargo run --locked -p systemdiff-cli -- collectors + desktop: + name: Desktop (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: apps/desktop/package-lock.json + + - name: Install locked frontend dependencies + run: npm ci --prefix apps/desktop + + - name: Type-check frontend + run: npm run check --prefix apps/desktop + + - name: Lint frontend + run: npm run lint --prefix apps/desktop + + - name: Build frontend + run: npm run build --prefix apps/desktop + + - name: Install stable Rust + run: rustup toolchain install stable --profile minimal --component rustfmt,clippy + + - name: Select stable Rust + run: rustup default stable + + - name: Check desktop Rust formatting + run: cargo fmt --manifest-path apps/desktop/src-tauri/Cargo.toml --all --check + + - name: Lint desktop Rust + run: cargo clippy --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets -- -D warnings + + - name: Test desktop Rust + run: cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets + + - name: Build Windows desktop executable + if: runner.os == 'Windows' + run: npm run tauri:build --prefix apps/desktop + package-windows-preview: name: Package Windows developer preview if: github.event_name == 'push' - needs: rust + needs: + - rust + - desktop runs-on: windows-latest timeout-minutes: 30 diff --git a/.gitignore b/.gitignore index d25889a..d323dfc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ pnpm-debug.log* # Tauri local output **/.tauri/ +apps/desktop/src-tauri/gen/schemas/ # Editors and IDEs .idea/ diff --git a/README.md b/README.md index aa2b224..08c1e6d 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ SystemDiff takes a before Snapshot and an after Snapshot, then explains the evidence that changed. It is for questions like: “I installed this program—what startup entries or Windows services did it change?” > [!IMPORTANT] -> SystemDiff is pre-release software. Today it captures and compares the documented Windows Registry Run/RunOnce startup locations and Windows service configuration visible to the current token. An unsigned, short-lived Windows x64 Developer Preview is available from eligible CI runs, but there is no official binary Release. Scheduled Tasks, rules, redaction, releases, and the desktop app are not implemented. +> SystemDiff is pre-release software. Today it captures and compares the documented Windows Registry Run/RunOnce startup locations and Windows service configuration visible to the current token. The first guided desktop workflow can be built from source for development, and an unsigned, short-lived Windows x64 CLI Developer Preview is available from eligible CI runs. There is no official binary Release. Scheduled Tasks, rules, redaction, and desktop distribution are not implemented. -[Try the sample](#try-the-registry-demo) · [Developer Preview builds](#developer-preview-builds) · [Build from source](#build-from-source) · [Inspect the data format](docs/data-format.md) +[See the desktop workflow](#desktop-development-build) · [Try the sample](#try-the-registry-demo) · [CLI Developer Preview](#cli-developer-preview-builds) · [Build from source](#build-from-source) -![SystemDiff showing one synthetic Registry startup entry added](docs/assets/registry-startup-demo.svg) +![SystemDiff Desktop showing one real synthetic Registry startup change](docs/assets/systemdiff-desktop-results.jpg) -_Verified output from the committed synthetic Registry-only fixtures. No real host data is shown._ +_A real Windows desktop dogfood run: SystemDiff captured before and after a guarded synthetic `HKCU` Run change, classified exactly one Added startup entry, and verified exact-data cleanup. No personal host evidence is shown._ ## Available today @@ -27,12 +27,26 @@ _Verified output from the committed synthetic Registry-only fixtures. No real ho | Human-readable, technical, and deterministic JSON Diff output | Implemented | | Coverage-aware comparison that does not turn missing evidence into a false removal | Implemented | | Capture current-token-visible Windows service configuration (drivers excluded) | Implemented with conservative partial coverage | +| Guided Start capture → Finish & compare desktop workflow | Implemented as a source-built development app | | Scheduled Tasks Collector | Planned; not implemented | | Rules, signatures, risk classification, and redacted sharing | Planned; not implemented | SystemDiff reports facts such as “Added to current-user startup.” It does not currently decide whether an entry is malicious, safe, signed, or worthy of removal. -## Developer Preview builds +## Desktop development build + +The first desktop vertical slice reuses the same Rust Collectors, Diff engine, and report semantics as the CLI. It guides a user through one local session, shows calm grouped results for Startup and Windows Services, preserves incomplete coverage as Inconclusive, and offers exact technical details on demand. English (`en-US`) and Simplified Chinese (`zh-CN`) are included. + +It is intentionally a development app, not a distributed product build: there is no installer, signed executable, desktop Actions artifact, update mechanism, history, file import, or clean-machine WebView2 bootstrap yet. It requires an installed WebView2 Runtime and the source-build prerequisites in [apps/desktop/README.md](apps/desktop/README.md). + +```powershell +npm ci --prefix apps/desktop +npm --prefix apps/desktop run tauri -- dev +``` + +The app stores temporary unredacted before/after evidence under its backend-owned local session directory and removes verified session files after finish, cancel, a new capture, or a stable normal exit. It does not expose paths to the web frontend or add Registry/service write capability. Exiting during an in-flight native capture, a cleanup failure, or a crash can leave sensitive local evidence for conservative startup recovery; Results then surface a cleanup warning when the process is still running. This is not yet a shareable-report workflow. + +## CLI Developer Preview builds Successful `main` CI runs attach `systemdiff-windows-x86_64-developer-preview` for 14 days. This is an ephemeral GitHub Actions artifact, not a GitHub Release or a supported version. To get it: @@ -53,7 +67,11 @@ With a stable Rust MSVC toolchain installed: cargo run --locked --quiet -p systemdiff-cli -- diff fixtures/snapshots/registry-before-v1.json fixtures/snapshots/registry-after-v1.json ``` -The sample contains one synthetic `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` addition. It is the same output shown above and is fixed by a regression test. +The sample contains one synthetic `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` addition and is fixed by a regression test. + +![SystemDiff CLI showing one synthetic Registry startup entry added](docs/assets/registry-startup-demo.svg) + +_Verified output from the committed Registry-only fixtures. No real host data is shown._ The three report modes serve different needs: @@ -111,11 +129,11 @@ cargo test --locked --workspace --all-targets cargo run --locked -p systemdiff-cli -- collectors ``` -There is no official binary Release yet. The CI Developer Preview above is unsigned and temporary. The existing synthetic HKCU write-based E2E harness is test-only, requires two explicit gates, refuses to overwrite an existing value, performs exact-data guarded cleanup, and is not run by default CI. +There is no official binary Release yet. The CI CLI Developer Preview above is unsigned and temporary. The existing synthetic HKCU write-based E2E harnesses are test-only, require explicit gates, refuse to overwrite an existing value, perform exact-data guarded cleanup, and are not run by default CI. ## Architecture and roadmap -The Rust workspace separates versioned domain data, Windows API access, deterministic Diff, rules, reporting, and CLI composition. The future desktop client is proposed to reuse the same core; no Tauri application has been generated. +The Rust workspace separates versioned domain data, Windows API access, deterministic Diff, rules, reporting, and CLI composition. The Tauri 2 desktop development app reuses those crates through a narrow Rust-owned session and presentation boundary; React handles localization and layout without reclassifying evidence. Registry startup and Windows Services are the first two completed vertical slices, not the finished v0.1. See the [Collector notes](docs/collectors.md) and [roadmap](docs/roadmap.md) for current boundaries. diff --git a/README.zh-CN.md b/README.zh-CN.md index 496cc3c..29e93be 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,13 +11,13 @@ SystemDiff 会分别创建变更前后的 Snapshot,再说明两者之间有哪些证据发生了变化。它要回答的是这类问题:“我刚安装了这个程序,它改了哪些启动项或 Windows 服务?” > [!IMPORTANT] -> SystemDiff 仍处于预发布阶段。现阶段真正支持的是采集并比较 Windows Registry 中有官方文档的 Run/RunOnce 启动项,以及当前 token 可见的 Windows 服务配置。符合条件的 CI run 会提供短期有效、未签名的 Windows x64 Developer Preview,但目前没有正式二进制 Release。计划任务、规则、脱敏、正式发布包和桌面应用均尚未实现。 +> SystemDiff 仍处于预发布阶段。现阶段真正支持的是采集并比较 Windows Registry 中有官方文档的 Run/RunOnce 启动项,以及当前 token 可见的 Windows 服务配置。首个引导式桌面流程已可从源码构建用于开发;符合条件的 CI run 也会提供短期有效、未签名的 Windows x64 CLI Developer Preview。目前没有正式二进制 Release,计划任务、规则、脱敏和桌面分发均尚未实现。 -[试用示例](#试用-registry-示例) · [Developer Preview 构建](#developer-preview-构建) · [从源码构建](#从源码构建) · [查看数据格式](docs/data-format.md) +[查看桌面流程](#桌面开发构建) · [试用示例](#试用-registry-示例) · [CLI Developer Preview](#cli-developer-preview-构建) · [从源码构建](#从源码构建) -![SystemDiff 显示新增的一条 synthetic Registry 启动项](docs/assets/registry-startup-demo.svg) +![SystemDiff Desktop 显示一条真实的 synthetic Registry 启动项变化](docs/assets/systemdiff-desktop-results.jpg) -_图中是仓库内 Registry-only synthetic fixtures 生成并经过验证的真实输出,不含真实主机数据。_ +_这是一次真实的 Windows 桌面 dogfood:SystemDiff 在受保护的 synthetic `HKCU` Run 变更前后完成采集,准确识别出一条 Added 启动项,并验证了 exact-data cleanup。图中不含个人主机证据。_ ## 当前可用能力 @@ -27,12 +27,26 @@ _图中是仓库内 Registry-only synthetic fixtures 生成并经过验证的真 | 易读文本、technical 文本和确定性 JSON 三种 Diff 输出 | 已实现 | | 感知采集覆盖情况,不把缺失证据误报为删除 | 已实现 | | 采集当前 token 可见的 Windows 服务配置(不含驱动) | 已实现,并采用保守的 partial coverage | +| 引导式“开始记录 → 完成并比较”桌面流程 | 已实现为可从源码构建的开发应用 | | Scheduled Tasks Collector | 计划中;尚未实现 | | 规则、数字签名、风险判断和脱敏分享 | 计划中;尚未实现 | SystemDiff 目前只陈述“已加入当前用户启动项”这类事实,不会判断某个条目是否恶意、安全、已签名或应该删除。 -## Developer Preview 构建 +## 桌面开发构建 + +首个 Desktop vertical slice 复用与 CLI 相同的 Rust Collectors、Diff engine 和报告语义。它引导用户完成一次本地记录,把 Startup 和 Windows Services 结果分组显示,保留因覆盖不完整产生的 Inconclusive,并按需展示完整 technical details。界面包含英文(`en-US`)和简体中文(`zh-CN`)。 + +它目前只是开发应用,并非可分发的正式产品:尚无安装程序、签名 executable、Desktop Actions artifact、自动更新、历史记录、文件导入或 clean-machine WebView2 bootstrap。运行前需要已安装 WebView2 Runtime,并满足 [apps/desktop/README.md](apps/desktop/README.md) 中的源码构建条件。 + +```powershell +npm ci --prefix apps/desktop +npm --prefix apps/desktop run tauri -- dev +``` + +应用会把未脱敏的临时 before/after evidence 存在后端自有的本地 session 目录中,并在完成、取消、开始新记录或稳定的正常退出时删除经过验证的 session 文件。它不会向 Web frontend 暴露路径,也没有增加 Registry 或服务写入能力。在原生采集尚未结束时退出、清理失败或发生崩溃,仍可能留下敏感的本地 evidence,等待保守的启动恢复;如果进程仍在运行,Results 会明确显示清理警告。因此这还不是可直接分享报告的流程。 + +## CLI Developer Preview 构建 `main` 的 CI 成功运行后,会附带保存 14 天的 `systemdiff-windows-x86_64-developer-preview`。这是会过期的 GitHub Actions artifact,不是 GitHub Release,也不代表受支持的正式版本。获取步骤如下: @@ -53,7 +67,11 @@ x64 executable 使用 Cargo `release` profile 构建。CI 会检查 PE 架构和 cargo run --locked --quiet -p systemdiff-cli -- diff fixtures/snapshots/registry-before-v1.json fixtures/snapshots/registry-after-v1.json ``` -示例只包含一条 synthetic `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` 新增项。它与上方图片中的输出完全一致,并由回归测试固定。 +示例只包含一条 synthetic `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` 新增项,并由回归测试固定。 + +![SystemDiff CLI 显示新增的一条 synthetic Registry 启动项](docs/assets/registry-startup-demo.svg) + +_图中是仓库内 Registry-only fixtures 生成并经过验证的输出,不含真实主机数据。_ 三种输出模式分别面向不同需求: @@ -111,11 +129,11 @@ cargo test --locked --workspace --all-targets cargo run --locked -p systemdiff-cli -- collectors ``` -目前还没有官方二进制 Release。上文的 CI Developer Preview 未签名且会过期。现有的 synthetic HKCU 写入型 E2E harness 只用于测试,需要两个显式 gate,会拒绝覆盖已有 value,并使用 exact-data guarded cleanup;默认 CI 不会运行它。 +目前还没有官方二进制 Release。上文的 CI CLI Developer Preview 未签名且会过期。现有的 synthetic HKCU 写入型 E2E harness 都只用于测试,需要显式 gate,会拒绝覆盖已有 value,并使用 exact-data guarded cleanup;默认 CI 不会运行它们。 ## 架构与路线图 -Rust workspace 把带版本的领域数据、Windows API 访问、确定性 Diff、规则、报告和 CLI 组合彼此分离。未来的桌面客户端计划复用同一套 core;目前尚未生成 Tauri 应用。 +Rust workspace 把带版本的领域数据、Windows API 访问、确定性 Diff、规则、报告和 CLI 组合彼此分离。Tauri 2 桌面开发应用通过窄小、由 Rust 决定语义的 session/presentation 边界复用这些 crates;React 只负责本地化和布局,不会重新判断 evidence。 Registry startup 和 Windows Services 是前两个完成的 vertical slice,并不代表 v0.1 已经完成。当前边界和后续计划见 [Collector 说明](docs/collectors.md)与[路线图](docs/roadmap.md)。 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 9358242..c88a482 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -1,7 +1,53 @@ -# SystemDiff desktop application +# SystemDiff desktop development -The desktop application is intentionally not scaffolded during the CLI bootstrap. +The desktop app is the first guided, ordinary-user interface for SystemDiff. It provides a local three-stage workflow—start capture, make a Windows change, finish and compare—without exposing Snapshot JSON or asking for file paths. -[ADR 0003](../../docs/adr/0003-desktop-stack.md) proposes Tauri 2 with React and TypeScript for v0.2, subject to a narrow post-v0.1 spike covering least-privilege IPC, bundled content/CSP, `en-US` and `zh-CN` localization, WebView2 behavior, packaging, tests, and dependency audit. +This is currently a **developer preview built from source**, not an official release, signed installer, or replacement for the existing CLI artifact. It has been designed for Windows 10 version 1709 and later, but a clean Windows 10 1709 machine may not already have the required Microsoft WebView2 Evergreen Runtime. The first validated development environment and remaining baseline work are tracked in the repository project state. -Do not generate a frontend package or add Node/Tauri CI until that ADR is accepted. The desktop client must reuse the Rust core and must not call Windows APIs directly or hide underlying evidence. +## Boundaries + +- Rust calls the existing Registry startup and Windows Services collectors, coverage-aware Diff, and report presentation APIs in-process. The app never shells out to the CLI. +- React handles layout, interaction, local time, and `en-US` / `zh-CN` text. It does not classify evidence. +- Production behavior is read-only, local, non-elevated, and without telemetry or network access. +- Tauri exposes only the guided session commands. The frontend cannot provide arbitrary filesystem paths, URLs, shell commands, or Snapshot JSON. +- Unredacted before/after evidence is stored only in a collision-safe application-local session directory and removed after compare, cancel, or a stable normal exit. Cleanup failures are visible in ordinary Results, while in-flight exit/crash recovery considers only verified SystemDiff-owned stale sessions. This local storage is not encryption. + +## Prerequisites + +- Rust stable with the MSVC toolchain, `rustfmt`, and Clippy +- Node.js 22 and npm +- Microsoft Visual Studio C++ Build Tools and a Windows SDK +- Microsoft WebView2 Evergreen Runtime + +Use the project-local Tauri CLI; a global `cargo-tauri` install is not required. + +## Run the desktop app + +```powershell +npm ci --prefix apps/desktop +npm --prefix apps/desktop run tauri -- dev +``` + +The hot-reload development process is launched from a terminal for developer diagnostics, but the product workflow itself does not require a console, PowerShell, JSON, or file picker. + +## Validate + +```powershell +npm run check --prefix apps/desktop +npm run lint --prefix apps/desktop +npm run build --prefix apps/desktop + +cargo fmt --manifest-path apps/desktop/src-tauri/Cargo.toml --all --check +cargo clippy --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets -- -D warnings +cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets + +npm run tauri:build --prefix apps/desktop +``` + +The last command produces an unsigned development executable under `apps/desktop/src-tauri/target/release/`. It depends on the installed WebView2 Runtime and is not an installer or an official downloadable release. + +## Test-only Registry dogfood + +The production desktop binary contains no Registry write command. Maintainers can separately run the explicitly gated mutation-only harness in `scripts/set-desktop-dogfood-registry-value.ps1` between **Start capture** and **Finish & Compare**. The harness refuses an existing value and only removes the exact known synthetic type and data. + +Do not run write-enabled dogfood on a machine or account where you are not authorized to make the test change. diff --git a/apps/desktop/eslint.config.js b/apps/desktop/eslint.config.js new file mode 100644 index 0000000..5fa0ceb --- /dev/null +++ b/apps/desktop/eslint.config.js @@ -0,0 +1,20 @@ +import eslint from "@eslint/js"; +import hooks from "eslint-plugin-react-hooks"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist", "src-tauri/target"] }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.{ts,tsx}"], + languageOptions: { + globals: globals.browser, + }, + plugins: { + "react-hooks": hooks, + }, + rules: hooks.configs.flat.recommended.rules, + }, +); diff --git a/apps/desktop/icon.svg b/apps/desktop/icon.svg new file mode 100644 index 0000000..e088901 --- /dev/null +++ b/apps/desktop/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 0000000..5b754ab --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,14 @@ + + + + + + + + SystemDiff + + +
+ + + diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json new file mode 100644 index 0000000..0bd8c7c --- /dev/null +++ b/apps/desktop/package-lock.json @@ -0,0 +1,2916 @@ +{ + "name": "systemdiff-desktop", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "systemdiff-desktop", + "version": "0.0.0", + "dependencies": { + "@tauri-apps/api": "2.11.1", + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@tauri-apps/cli": "2.11.4", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "@vitejs/plugin-react": "6.1.0", + "eslint": "10.8.1", + "eslint-plugin-react-hooks": "7.1.1", + "globals": "17.11.0", + "typescript": "6.0.3", + "typescript-eslint": "8.67.0", + "vite": "8.2.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz", + "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz", + "integrity": "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..c965af9 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,32 @@ +{ + "name": "systemdiff-desktop", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "check": "tsc --noEmit", + "lint": "eslint . --max-warnings 0", + "tauri": "tauri", + "tauri:build": "tauri build --no-bundle" + }, + "dependencies": { + "@tauri-apps/api": "2.11.1", + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@tauri-apps/cli": "2.11.4", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "@vitejs/plugin-react": "6.1.0", + "eslint": "10.8.1", + "eslint-plugin-react-hooks": "7.1.1", + "globals": "17.11.0", + "typescript": "6.0.3", + "typescript-eslint": "8.67.0", + "vite": "8.2.2" + } +} diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..8bef4b3 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4477 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "systemdiff-core" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", + "sha2 0.11.0", + "time", +] + +[[package]] +name = "systemdiff-desktop" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", + "systemdiff-core", + "systemdiff-diff", + "systemdiff-report", + "systemdiff-windows", + "tauri", + "tauri-build", + "time", +] + +[[package]] +name = "systemdiff-diff" +version = "0.0.0" +dependencies = [ + "serde", + "systemdiff-core", +] + +[[package]] +name = "systemdiff-report" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", + "systemdiff-core", + "systemdiff-diff", +] + +[[package]] +name = "systemdiff-windows" +version = "0.0.0" +dependencies = [ + "sha2 0.11.0", + "systemdiff-core", + "windows 0.62.2", + "windows-version", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "tokio", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.20", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.20", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..5caef52 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "systemdiff-desktop" +version = "0.0.0" +edition = "2024" +license = "Apache-2.0" +authors = ["SystemDiff contributors"] +publish = false +build = "build.rs" + +[lib] +name = "systemdiff_desktop" +path = "src/lib.rs" + +[[bin]] +name = "systemdiff-desktop" +path = "src/main.rs" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +systemdiff-core = { path = "../../../crates/systemdiff-core" } +systemdiff-diff = { path = "../../../crates/systemdiff-diff" } +systemdiff-report = { path = "../../../crates/systemdiff-report" } +systemdiff-windows = { path = "../../../crates/systemdiff-windows" } +time = { version = "0.3.55", default-features = false, features = ["formatting", "std"] } + +[target.'cfg(windows)'.dependencies] +tauri = { version = "=2.11.5", default-features = false, features = ["wry"] } + +[build-dependencies] +tauri-build = { version = "2.6.3", features = [] } + +# Keep the desktop dependency graph and lockfile independent from the portable +# CLI workspace until a desktop artifact is part of the release process. +[workspace] diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000..5c2f46b --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,17 @@ +#[cfg(windows)] +const COMMANDS: &[&str] = &[ + "get_session_state", + "start_capture", + "finish_capture", + "cancel_capture", + "get_technical_details", +]; + +fn main() { + #[cfg(windows)] + { + let attributes = tauri_build::Attributes::new() + .app_manifest(tauri_build::AppManifest::new().commands(COMMANDS)); + tauri_build::try_build(attributes).expect("failed to prepare the Tauri application"); + } +} diff --git a/apps/desktop/src-tauri/capabilities/desktop.json b/apps/desktop/src-tauri/capabilities/desktop.json new file mode 100644 index 0000000..8a73a67 --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/desktop.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "desktop", + "description": "The single local SystemDiff desktop window can invoke only the guided capture workflow.", + "windows": ["main"], + "permissions": [ + "allow-get-session-state", + "allow-start-capture", + "allow-finish-capture", + "allow-cancel-capture", + "allow-get-technical-details" + ] +} diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000..04a40b4 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.ico differ diff --git a/apps/desktop/src-tauri/permissions/autogenerated/cancel_capture.toml b/apps/desktop/src-tauri/permissions/autogenerated/cancel_capture.toml new file mode 100644 index 0000000..7641668 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/cancel_capture.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-cancel-capture" +description = "Enables the cancel_capture command without any pre-configured scope." +commands.allow = ["cancel_capture"] + +[[permission]] +identifier = "deny-cancel-capture" +description = "Denies the cancel_capture command without any pre-configured scope." +commands.deny = ["cancel_capture"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/finish_capture.toml b/apps/desktop/src-tauri/permissions/autogenerated/finish_capture.toml new file mode 100644 index 0000000..4ffe56c --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/finish_capture.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-finish-capture" +description = "Enables the finish_capture command without any pre-configured scope." +commands.allow = ["finish_capture"] + +[[permission]] +identifier = "deny-finish-capture" +description = "Denies the finish_capture command without any pre-configured scope." +commands.deny = ["finish_capture"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/get_session_state.toml b/apps/desktop/src-tauri/permissions/autogenerated/get_session_state.toml new file mode 100644 index 0000000..7e82924 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/get_session_state.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-get-session-state" +description = "Enables the get_session_state command without any pre-configured scope." +commands.allow = ["get_session_state"] + +[[permission]] +identifier = "deny-get-session-state" +description = "Denies the get_session_state command without any pre-configured scope." +commands.deny = ["get_session_state"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/get_technical_details.toml b/apps/desktop/src-tauri/permissions/autogenerated/get_technical_details.toml new file mode 100644 index 0000000..02b2dee --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/get_technical_details.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-get-technical-details" +description = "Enables the get_technical_details command without any pre-configured scope." +commands.allow = ["get_technical_details"] + +[[permission]] +identifier = "deny-get-technical-details" +description = "Denies the get_technical_details command without any pre-configured scope." +commands.deny = ["get_technical_details"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/start_capture.toml b/apps/desktop/src-tauri/permissions/autogenerated/start_capture.toml new file mode 100644 index 0000000..b6e1fd6 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/start_capture.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-start-capture" +description = "Enables the start_capture command without any pre-configured scope." +commands.allow = ["start_capture"] + +[[permission]] +identifier = "deny-start-capture" +description = "Denies the start_capture command without any pre-configured scope." +commands.deny = ["start_capture"] diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..6d12e77 --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,94 @@ +#![forbid(unsafe_code)] + +mod session; +mod storage; + +pub use session::{ + AppError, AppViewState, DesktopBackendState, SessionService, SessionStage, ShutdownCleanup, + SystemSnapshotSource, +}; +pub use storage::{RecoverySummary, SessionStorage}; + +#[cfg(windows)] +mod tauri_app { + use super::{AppError, AppViewState, DesktopBackendState, SessionService, SessionStorage}; + use tauri::{Manager, State}; + + #[tauri::command] + fn get_session_state(state: State<'_, DesktopBackendState>) -> Result { + state.session_state() + } + + #[tauri::command] + async fn start_capture( + state: State<'_, DesktopBackendState>, + ) -> Result { + let service = state.service()?; + tauri::async_runtime::spawn_blocking(move || service.start_capture()) + .await + .map_err(|error| AppError::background_task(error.to_string()))? + } + + #[tauri::command] + async fn finish_capture( + state: State<'_, DesktopBackendState>, + ) -> Result { + let service = state.service()?; + tauri::async_runtime::spawn_blocking(move || service.finish_capture()) + .await + .map_err(|error| AppError::background_task(error.to_string()))? + } + + #[tauri::command] + async fn cancel_capture( + state: State<'_, DesktopBackendState>, + ) -> Result { + let service = state.service()?; + tauri::async_runtime::spawn_blocking(move || service.cancel_capture()) + .await + .map_err(|error| AppError::background_task(error.to_string()))? + } + + #[tauri::command] + fn get_technical_details(state: State<'_, DesktopBackendState>) -> Result { + state.service()?.technical_details() + } + + pub(super) fn run() { + tauri::Builder::default() + .setup(|app| { + let backend = match app.path().app_local_data_dir() { + Ok(app_local_data) => { + DesktopBackendState::from_result(SessionService::production( + SessionStorage::new(app_local_data.join("capture-sessions-v1")), + )) + } + Err(error) => DesktopBackendState::failed(AppError::bootstrap_storage( + format!("could not resolve app-local data directory: {error}"), + )), + }; + app.manage(backend); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + get_session_state, + start_capture, + finish_capture, + cancel_capture, + get_technical_details + ]) + .build(tauri::generate_context!()) + .expect("failed to build the SystemDiff desktop application") + .run(|app_handle, event| { + if matches!(event, tauri::RunEvent::ExitRequested { .. }) { + let backend = app_handle.state::(); + let _ = backend.shutdown_cleanup(); + } + }); + } +} + +#[cfg(windows)] +pub fn run() { + tauri_app::run(); +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..17cd78c --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,11 @@ +#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] + +#[cfg(windows)] +fn main() { + systemdiff_desktop::run(); +} + +#[cfg(not(windows))] +fn main() { + eprintln!("The SystemDiff desktop application requires Windows."); +} diff --git a/apps/desktop/src-tauri/src/session.rs b/apps/desktop/src-tauri/src/session.rs new file mode 100644 index 0000000..b667ea8 --- /dev/null +++ b/apps/desktop/src-tauri/src/session.rs @@ -0,0 +1,921 @@ +use crate::storage::{ + RecoverySummary, SessionFiles, SessionRootLock, SessionStorage, StorageError, +}; +use serde::Serialize; +use std::error::Error; +use std::fmt; +use std::sync::{Arc, Mutex, MutexGuard}; +use systemdiff_core::Snapshot; +use systemdiff_diff::{DiffOptions, diff_snapshots}; +use systemdiff_report::presentation::{DesktopPresentation, build_desktop_presentation}; +use systemdiff_report::render_technical; +use time::OffsetDateTime; +use time::format_description::well_known::Rfc3339; + +pub trait SnapshotSource: Send + Sync { + fn capture(&self) -> Result; +} + +#[derive(Debug, Default)] +pub struct SystemSnapshotSource; + +impl SnapshotSource for SystemSnapshotSource { + fn capture(&self) -> Result { + let captured_at = OffsetDateTime::now_utc() + .format(&Rfc3339) + .map_err(|error| CaptureFailure::new(error.to_string()))?; + systemdiff_windows::capture_snapshot(captured_at, env!("CARGO_PKG_VERSION").to_owned()) + .map_err(|error| match error { + systemdiff_windows::CaptureError::UnsupportedPlatform => { + CaptureFailure::unsupported_windows(error.to_string()) + } + systemdiff_windows::CaptureError::InvalidSnapshot(_) => { + CaptureFailure::invalid_snapshot(error.to_string()) + } + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SessionStage { + Ready, + Starting, + Capturing, + Finishing, + Results, +} + +#[derive(Debug, Clone, Serialize)] +pub struct AppViewState { + pub stage: SessionStage, + pub presentation: Option, + pub cleanup_pending: bool, +} + +#[derive(Debug, Clone)] +struct CaptureResult { + presentation: DesktopPresentation, + technical_details: String, + cleanup_pending: Option, +} + +#[derive(Debug)] +enum InternalState { + Ready, + Starting, + Capturing(SessionFiles), + Finishing, + Results(CaptureResult), +} + +pub struct SessionService { + storage: SessionStorage, + _root_lock: SessionRootLock, + source: Arc, + state: Mutex, + startup_recovery: RecoverySummary, +} + +#[derive(Clone)] +pub struct DesktopBackendState { + service: Result, AppError>, +} + +impl DesktopBackendState { + pub fn from_result(service: Result) -> Self { + Self { + service: service.map(Arc::new), + } + } + + pub fn failed(error: AppError) -> Self { + Self { + service: Err(error), + } + } + + pub fn service(&self) -> Result, AppError> { + self.service.clone() + } + + pub fn session_state(&self) -> Result { + self.service().map(|service| service.session_state()) + } + + pub fn shutdown_cleanup(&self) -> Result { + match &self.service { + Ok(service) => service.shutdown_cleanup(), + Err(_) => Ok(ShutdownCleanup::Complete), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShutdownCleanup { + Complete, + DeferredBusy, +} + +impl SessionService { + pub fn production(storage: SessionStorage) -> Result { + Self::new(storage, Arc::new(SystemSnapshotSource)) + } + + pub fn new(storage: SessionStorage, source: Arc) -> Result { + let root_lock = storage.acquire_root_lock().map_err(|error| match error { + StorageError::AlreadyRunning => AppError::another_instance(), + error => AppError::bootstrap_storage(error.to_string()), + })?; + let startup_recovery = storage + .recover_stale_sessions() + .map_err(|error| AppError::bootstrap_storage(error.to_string()))?; + Ok(Self { + storage, + _root_lock: root_lock, + source, + state: Mutex::new(InternalState::Ready), + startup_recovery, + }) + } + + pub fn session_state(&self) -> AppViewState { + view_state(&self.lock_state()) + } + + pub fn technical_details(&self) -> Result { + let state = self.lock_state(); + match &*state { + InternalState::Results(result) => Ok(result.technical_details.clone()), + _ => Err(AppError::invalid_state( + "get_technical_details", + stage_of(&state), + )), + } + } + + pub fn start_capture(&self) -> Result { + { + let mut state = self.lock_state(); + match &*state { + InternalState::Ready => {} + _ => return Err(AppError::busy("start_capture")), + } + *state = InternalState::Starting; + } + + let result = self.start_capture_work(); + let mut state = self.lock_state(); + match result { + Ok(session) => { + *state = InternalState::Capturing(session); + Ok(view_state(&state)) + } + Err(error) => { + *state = InternalState::Ready; + Err(error) + } + } + } + + pub fn finish_capture(&self) -> Result { + let session = { + let mut state = self.lock_state(); + let InternalState::Capturing(session) = &*state else { + return Err(AppError::invalid_state("finish_capture", stage_of(&state))); + }; + let session = session.clone(); + *state = InternalState::Finishing; + session + }; + + let result = self.finish_capture_work(&session); + let mut state = self.lock_state(); + match result { + Ok(result) => { + *state = InternalState::Results(result); + Ok(view_state(&state)) + } + Err(error) => { + *state = InternalState::Capturing(session); + Err(error) + } + } + } + + pub fn cancel_capture(&self) -> Result { + enum CancelTarget { + Capture(SessionFiles), + Results { + session: SessionFiles, + result: Box, + }, + } + + let target = { + let mut state = self.lock_state(); + match &*state { + InternalState::Results(result) if result.cleanup_pending.is_none() => { + *state = InternalState::Ready; + return Ok(view_state(&state)); + } + InternalState::Results(result) => { + let result = result.clone(); + let session = result + .cleanup_pending + .clone() + .expect("guarded by the preceding cleanup-pending check"); + *state = InternalState::Finishing; + CancelTarget::Results { + session, + result: Box::new(result), + } + } + InternalState::Capturing(session) => { + let session = session.clone(); + *state = InternalState::Finishing; + CancelTarget::Capture(session) + } + _ => { + return Err(AppError::invalid_state("cancel_capture", stage_of(&state))); + } + } + }; + + let session = match &target { + CancelTarget::Capture(session) | CancelTarget::Results { session, .. } => session, + }; + match self.storage.cleanup_session(session) { + Ok(()) => { + let mut state = self.lock_state(); + *state = InternalState::Ready; + Ok(view_state(&state)) + } + Err(error) => { + let mut state = self.lock_state(); + *state = match target { + CancelTarget::Capture(session) => InternalState::Capturing(session), + CancelTarget::Results { result, .. } => InternalState::Results(*result), + }; + Err(AppError::session_cleanup(error)) + } + } + } + + /// Best-effort cleanup used by the normal desktop application exit path. + /// + /// A stable Capturing session or a Results session with pending cleanup is + /// removed through the same exact allowlist as explicit cancellation. An + /// in-flight native capture/diff cannot be interrupted safely, so it is + /// deliberately left for conservative startup recovery. + pub fn shutdown_cleanup(&self) -> Result { + match self.session_state().stage { + SessionStage::Ready => Ok(ShutdownCleanup::Complete), + SessionStage::Starting | SessionStage::Finishing => Ok(ShutdownCleanup::DeferredBusy), + SessionStage::Capturing | SessionStage::Results => { + self.cancel_capture()?; + Ok(ShutdownCleanup::Complete) + } + } + } + + fn start_capture_work(&self) -> Result { + let session = self + .storage + .create_session() + .map_err(AppError::session_storage)?; + let result = self + .source + .capture() + .map_err(AppError::capture) + .and_then(|snapshot| { + self.storage + .write_before(&session, &snapshot) + .map_err(AppError::session_storage) + }); + if let Err(error) = result { + return match self.storage.cleanup_session(&session) { + Ok(()) => Err(error), + Err(cleanup) => Err(error.with_cleanup_failure(cleanup)), + }; + } + Ok(session) + } + + fn finish_capture_work(&self, session: &SessionFiles) -> Result { + let result = (|| { + let before = self + .storage + .read_before(session) + .map_err(AppError::session_storage)?; + let after = self.source.capture().map_err(AppError::capture)?; + self.storage + .write_after(session, &after) + .map_err(AppError::session_storage)?; + let after = self + .storage + .read_after(session) + .map_err(AppError::session_storage)?; + let diff = diff_snapshots(&before, &after, DiffOptions::default()) + .map_err(|error| AppError::diff(error.to_string()))?; + let presentation = build_desktop_presentation(&diff); + let technical = render_technical(&diff, &before, &after); + let technical_details = format!( + concat!( + "Desktop session\n", + " startup sessions cleaned: {}\n", + " startup entries refused: {}\n", + " Snapshot size limit: {} bytes\n", + " session evidence cleanup: required before Results\n\n", + "{}" + ), + self.startup_recovery.cleaned_sessions, + self.startup_recovery.refused_entries, + crate::storage::MAX_SNAPSHOT_BYTES, + technical + ); + Ok(CaptureResult { + presentation, + technical_details, + cleanup_pending: None, + }) + })(); + + match result { + Ok(mut result) => { + match self.storage.cleanup_session(session) { + Ok(()) => result + .technical_details + .push_str("\nDesktop session evidence cleanup: complete\n"), + Err(error) => { + result.technical_details.push_str(&format!( + concat!( + "\nWARNING: desktop session evidence cleanup did not complete.\n", + "The owned app-local session will be considered for conservative cleanup during the next startup; suspicious entries are never deleted.\n", + "Cleanup error: {}\n" + ), + error + )); + result.cleanup_pending = Some(session.clone()); + } + } + Ok(result) + } + Err(error) => match self.storage.remove_after_if_present(session) { + Ok(()) => Err(error), + Err(cleanup) => Err(error.with_cleanup_failure(cleanup)), + }, + } + } + + fn lock_state(&self) -> MutexGuard<'_, InternalState> { + self.state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } +} + +fn view_state(state: &InternalState) -> AppViewState { + match state { + InternalState::Ready => AppViewState { + stage: SessionStage::Ready, + presentation: None, + cleanup_pending: false, + }, + InternalState::Starting => AppViewState { + stage: SessionStage::Starting, + presentation: None, + cleanup_pending: false, + }, + InternalState::Capturing(_) => AppViewState { + stage: SessionStage::Capturing, + presentation: None, + cleanup_pending: false, + }, + InternalState::Finishing => AppViewState { + stage: SessionStage::Finishing, + presentation: None, + cleanup_pending: false, + }, + InternalState::Results(result) => AppViewState { + stage: SessionStage::Results, + presentation: Some(result.presentation.clone()), + cleanup_pending: result.cleanup_pending.is_some(), + }, + } +} + +fn stage_of(state: &InternalState) -> SessionStage { + view_state(state).stage +} + +#[derive(Debug, Clone)] +pub struct CaptureFailure { + kind: CaptureFailureKind, + details: String, +} + +#[derive(Debug, Clone, Copy)] +enum CaptureFailureKind { + Generic, + UnsupportedWindows, + InvalidSnapshot, +} + +impl CaptureFailure { + pub fn new(details: String) -> Self { + Self { + kind: CaptureFailureKind::Generic, + details, + } + } + + fn unsupported_windows(details: String) -> Self { + Self { + kind: CaptureFailureKind::UnsupportedWindows, + details, + } + } + + fn invalid_snapshot(details: String) -> Self { + Self { + kind: CaptureFailureKind::InvalidSnapshot, + details, + } + } +} + +impl fmt::Display for CaptureFailure { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.details) + } +} + +impl Error for CaptureFailure {} + +#[derive(Debug, Clone, Serialize)] +pub struct AppError { + pub code: String, + pub message_id: String, + pub technical_details: String, +} + +impl AppError { + fn new(code: &'static str, message_id: &'static str, technical_details: String) -> Self { + Self { + code: code.to_owned(), + message_id: message_id.to_owned(), + technical_details, + } + } + + fn busy(command: &'static str) -> Self { + Self::new( + "session_busy", + "error.session_busy", + format!("{command} was refused because another session operation is active"), + ) + } + + fn invalid_state(command: &'static str, stage: SessionStage) -> Self { + Self::new( + "invalid_session_state", + "error.invalid_session_state", + format!("{command} is not valid while the session stage is {stage:?}"), + ) + } + + fn capture(error: CaptureFailure) -> Self { + let (code, message_id) = match error.kind { + CaptureFailureKind::Generic => ("capture_failed", "error.capture_failed"), + CaptureFailureKind::UnsupportedWindows => { + ("unsupported_windows", "error.unsupported_windows") + } + CaptureFailureKind::InvalidSnapshot => ("invalid_snapshot", "error.invalid_snapshot"), + }; + Self::new(code, message_id, error.to_string()) + } + + fn session_storage(error: StorageError) -> Self { + match error { + StorageError::InvalidSnapshot { details } => Self::new( + "invalid_snapshot", + "error.invalid_snapshot", + format!("stored Snapshot is invalid: {details}"), + ), + error => Self::new( + "session_storage_failed", + "error.session_storage_failed", + error.to_string(), + ), + } + } + + fn session_cleanup(error: StorageError) -> Self { + Self::new( + "session_cleanup_failed", + "error.session_cleanup_failed", + error.to_string(), + ) + } + + fn another_instance() -> Self { + Self::new( + "another_instance_running", + "error.another_instance_running", + "another SystemDiff desktop instance owns the app-local session root".to_owned(), + ) + } + + fn diff(details: String) -> Self { + Self::new("diff_failed", "error.diff_failed", details) + } + + pub fn background_task(details: String) -> Self { + Self::new( + "background_task_failed", + "error.background_task_failed", + details, + ) + } + + pub fn bootstrap_storage(details: String) -> Self { + Self::new( + "bootstrap_storage_failed", + "error.bootstrap_storage_failed", + details, + ) + } + + fn with_cleanup_failure(mut self, cleanup: StorageError) -> Self { + self.technical_details = format!( + "{}; cleanup also failed: {}", + self.technical_details, cleanup + ); + self + } +} + +impl fmt::Display for AppError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{} ({}): {}", + self.code, self.message_id, self.technical_details + ) + } +} + +impl Error for AppError {} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::VecDeque; + use std::fs; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + use systemdiff_core::{ + CollectorRun, CollectorStatus, HostMetadata, PrivilegeState, RedactionMetadata, + RedactionStatus, ScopeCoverage, Snapshot, + }; + + static TEST_COUNTER: AtomicU64 = AtomicU64::new(0); + + struct TestDirectory(PathBuf); + + impl TestDirectory { + fn new() -> Self { + let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "systemdiff-desktop-session-test-{}-{id}", + std::process::id() + )); + fs::create_dir(&path).expect("test directory must be unique"); + Self(path) + } + } + + impl Drop for TestDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + struct FakeSource { + results: Mutex>>, + } + + impl FakeSource { + fn new(results: Vec>) -> Self { + Self { + results: Mutex::new(results.into()), + } + } + } + + impl SnapshotSource for FakeSource { + fn capture(&self) -> Result { + self.results + .lock() + .unwrap() + .pop_front() + .expect("fake capture result must exist") + } + } + + fn snapshot(captured_at: &str) -> Snapshot { + Snapshot { + document_type: "systemdiff.snapshot".to_owned(), + schema_version: 1, + systemdiff_version: "0.0.0-test".to_owned(), + captured_at: captured_at.to_owned(), + host: HostMetadata { + windows_version: None, + windows_build: None, + architecture: Some("x86_64".to_owned()), + }, + privilege: PrivilegeState::StandardUser, + enabled_collectors: vec!["test.collector".to_owned()], + collectors: vec![CollectorRun { + id: "test.collector".to_owned(), + version: 1, + status: CollectorStatus::Complete, + coverage: vec![ScopeCoverage { + scope_id: "test.scope".to_owned(), + status: CollectorStatus::Complete, + }], + diagnostics: Vec::new(), + }], + redaction: RedactionMetadata { + status: RedactionStatus::Unredacted, + policy: None, + }, + observations: Vec::new(), + } + } + + fn service_with( + temporary: &TestDirectory, + results: Vec>, + ) -> SessionService { + SessionService::new( + SessionStorage::new(temporary.0.join("sessions")), + Arc::new(FakeSource::new(results)), + ) + .unwrap() + } + + fn session_directory_count(temporary: &TestDirectory) -> usize { + fs::read_dir(temporary.0.join("sessions")) + .unwrap() + .filter_map(Result::ok) + .filter(|entry| { + entry + .file_name() + .to_str() + .is_some_and(|name| name.starts_with("session-v1-")) + }) + .count() + } + + #[test] + fn successful_start_finish_and_cleanup_reach_results() { + let temporary = TestDirectory::new(); + let service = service_with( + &temporary, + vec![ + Ok(snapshot("2026-08-20T00:00:00Z")), + Ok(snapshot("2026-08-20T00:05:00Z")), + ], + ); + + assert_eq!( + service.start_capture().unwrap().stage, + SessionStage::Capturing + ); + let state = service.finish_capture().unwrap(); + + assert_eq!(state.stage, SessionStage::Results); + assert!(state.presentation.is_some()); + assert!(!state.cleanup_pending); + assert_eq!(session_directory_count(&temporary), 0); + let technical = service.technical_details().unwrap(); + assert!(technical.contains("SystemDiff technical diff")); + assert!(technical.contains("Desktop session evidence cleanup: complete")); + assert_eq!(service.cancel_capture().unwrap().stage, SessionStage::Ready); + assert!(service.technical_details().is_err()); + } + + #[test] + fn cancel_discards_the_owned_session_and_returns_ready() { + let temporary = TestDirectory::new(); + let service = service_with(&temporary, vec![Ok(snapshot("2026-08-20T00:00:00Z"))]); + service.start_capture().unwrap(); + + let state = service.cancel_capture().unwrap(); + + assert_eq!(state.stage, SessionStage::Ready); + assert_eq!(session_directory_count(&temporary), 0); + } + + #[test] + fn start_failure_restores_ready_and_cleans_the_session() { + let temporary = TestDirectory::new(); + let service = service_with( + &temporary, + vec![Err(CaptureFailure::new("synthetic failure".to_owned()))], + ); + + let error = service.start_capture().unwrap_err(); + + assert_eq!(error.code, "capture_failed"); + assert_eq!(service.session_state().stage, SessionStage::Ready); + assert_eq!(session_directory_count(&temporary), 0); + } + + #[test] + fn finish_failure_keeps_before_evidence_available_for_retry_or_cancel() { + let temporary = TestDirectory::new(); + let service = service_with( + &temporary, + vec![ + Ok(snapshot("2026-08-20T00:00:00Z")), + Err(CaptureFailure::new("synthetic failure".to_owned())), + ], + ); + service.start_capture().unwrap(); + + let error = service.finish_capture().unwrap_err(); + + assert_eq!(error.code, "capture_failed"); + assert_eq!(service.session_state().stage, SessionStage::Capturing); + assert_eq!(session_directory_count(&temporary), 1); + assert_eq!(service.cancel_capture().unwrap().stage, SessionStage::Ready); + } + + #[test] + fn duplicate_and_out_of_order_commands_are_rejected() { + let temporary = TestDirectory::new(); + let service = service_with(&temporary, vec![Ok(snapshot("2026-08-20T00:00:00Z"))]); + + assert_eq!( + service.finish_capture().unwrap_err().code, + "invalid_session_state" + ); + service.start_capture().unwrap(); + assert_eq!(service.start_capture().unwrap_err().code, "session_busy"); + } + + #[test] + fn completed_results_survive_a_conservative_cleanup_refusal() { + let temporary = TestDirectory::new(); + let service = service_with( + &temporary, + vec![ + Ok(snapshot("2026-08-20T00:00:00Z")), + Ok(snapshot("2026-08-20T00:05:00Z")), + ], + ); + service.start_capture().unwrap(); + let session = { + let state = service.lock_state(); + let InternalState::Capturing(session) = &*state else { + panic!("session must be capturing"); + }; + session.clone() + }; + fs::write( + session.directory().join("unexpected.txt"), + b"refuse cleanup", + ) + .unwrap(); + + let state = service.finish_capture().unwrap(); + + assert_eq!(state.stage, SessionStage::Results); + assert!(state.cleanup_pending); + assert!( + service + .technical_details() + .unwrap() + .contains("WARNING: desktop session evidence cleanup did not complete") + ); + assert!(session.directory().exists()); + assert_eq!( + service.cancel_capture().unwrap_err().code, + "session_cleanup_failed" + ); + assert_eq!(service.session_state().stage, SessionStage::Results); + fs::remove_file(session.directory().join("unexpected.txt")).unwrap(); + assert_eq!( + service.shutdown_cleanup().unwrap(), + ShutdownCleanup::Complete + ); + assert_eq!(service.session_state().stage, SessionStage::Ready); + assert!(!session.directory().exists()); + } + + #[test] + fn typed_capture_and_stored_snapshot_failures_keep_stable_message_ids() { + let generic = AppError::capture(CaptureFailure::new("generic".to_owned())); + let unsupported = AppError::capture(CaptureFailure::unsupported_windows( + "unsupported".to_owned(), + )); + let captured_invalid = AppError::capture(CaptureFailure::invalid_snapshot( + "invalid capture".to_owned(), + )); + let stored_invalid = AppError::session_storage(StorageError::InvalidSnapshot { + details: "invalid stored evidence".to_owned(), + }); + + assert_eq!( + (generic.code.as_str(), generic.message_id.as_str()), + ("capture_failed", "error.capture_failed") + ); + assert_eq!( + (unsupported.code.as_str(), unsupported.message_id.as_str()), + ("unsupported_windows", "error.unsupported_windows") + ); + assert_eq!( + ( + captured_invalid.code.as_str(), + captured_invalid.message_id.as_str() + ), + ("invalid_snapshot", "error.invalid_snapshot") + ); + assert_eq!( + ( + stored_invalid.code.as_str(), + stored_invalid.message_id.as_str() + ), + ("invalid_snapshot", "error.invalid_snapshot") + ); + } + + #[test] + fn managed_bootstrap_failure_is_returned_by_commands_without_a_service() { + let backend = DesktopBackendState::failed(AppError::another_instance()); + + let state_error = backend.session_state().unwrap_err(); + let Err(service_error) = backend.service() else { + panic!("bootstrap failure must not expose a SessionService"); + }; + + assert_eq!(state_error.code, "another_instance_running"); + assert_eq!(state_error.message_id, "error.another_instance_running"); + assert_eq!(service_error.code, "another_instance_running"); + assert_eq!( + backend.shutdown_cleanup().unwrap(), + ShutdownCleanup::Complete + ); + } + + #[test] + fn storage_initialization_failure_is_cached_as_restart_required() { + let temporary = TestDirectory::new(); + let non_directory = temporary.0.join("not-a-directory"); + fs::write(&non_directory, b"blocks session root creation").unwrap(); + let initialization = SessionService::new( + SessionStorage::new(non_directory.join("sessions")), + Arc::new(FakeSource::new(Vec::new())), + ); + let backend = DesktopBackendState::from_result(initialization); + + let state_error = backend.session_state().unwrap_err(); + let Err(service_error) = backend.service() else { + panic!("bootstrap storage failure must not expose a SessionService"); + }; + + assert_eq!(state_error.code, "bootstrap_storage_failed"); + assert_eq!(state_error.message_id, "error.bootstrap_storage_failed"); + assert_eq!(service_error.code, "bootstrap_storage_failed"); + assert_eq!(service_error.message_id, "error.bootstrap_storage_failed"); + } + + #[test] + fn normal_shutdown_cleans_a_stable_capturing_session() { + let temporary = TestDirectory::new(); + let service = service_with(&temporary, vec![Ok(snapshot("2026-08-20T00:00:00Z"))]); + service.start_capture().unwrap(); + + let outcome = service.shutdown_cleanup().unwrap(); + + assert_eq!(outcome, ShutdownCleanup::Complete); + assert_eq!(service.session_state().stage, SessionStage::Ready); + assert_eq!(session_directory_count(&temporary), 0); + } + + #[test] + fn busy_shutdown_is_deferred_for_startup_recovery_without_claiming_cleanup() { + let temporary = TestDirectory::new(); + let service = service_with(&temporary, Vec::new()); + *service.lock_state() = InternalState::Starting; + + assert_eq!( + service.shutdown_cleanup().unwrap(), + ShutdownCleanup::DeferredBusy + ); + assert_eq!(service.session_state().stage, SessionStage::Starting); + } +} diff --git a/apps/desktop/src-tauri/src/storage.rs b/apps/desktop/src-tauri/src/storage.rs new file mode 100644 index 0000000..600b8f1 --- /dev/null +++ b/apps/desktop/src-tauri/src/storage.rs @@ -0,0 +1,712 @@ +use serde::Serialize; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; +use systemdiff_core::{Snapshot, decode_snapshot_document}; + +const SESSION_PREFIX: &str = "session-v1-"; +const OWNERSHIP_MARKER: &str = ".systemdiff-session-v1"; +const OWNERSHIP_CONTENTS: &[u8] = b"SystemDiff desktop capture session v1\n"; +const ROOT_LOCK_FILE: &str = ".systemdiff-desktop.lock"; +const BEFORE_FILE: &str = "before.json"; +const AFTER_FILE: &str = "after.json"; +const MAX_CREATE_ATTEMPTS: usize = 128; +pub(crate) const MAX_SNAPSHOT_BYTES: u64 = 64 * 1024 * 1024; + +static SESSION_COUNTER: AtomicU64 = AtomicU64::new(0); + +#[derive(Debug, Clone)] +pub struct SessionStorage { + root: PathBuf, +} + +#[derive(Debug)] +pub(crate) struct SessionRootLock { + _file: File, +} + +#[derive(Debug, Clone)] +pub(crate) struct SessionFiles { + directory: PathBuf, + before: PathBuf, + after: PathBuf, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct RecoverySummary { + pub cleaned_sessions: u64, + pub refused_entries: u64, +} + +impl SessionStorage { + pub fn new(root: PathBuf) -> Self { + Self { root } + } + + pub(crate) fn create_session(&self) -> Result { + self.ensure_root()?; + for _ in 0..MAX_CREATE_ATTEMPTS { + let token = candidate_token(); + match self.create_session_named(&token) { + Ok(session) => return Ok(session), + Err(StorageError::Collision) => continue, + Err(error) => return Err(error), + } + } + Err(StorageError::CreateAttemptsExhausted) + } + + pub(crate) fn acquire_root_lock(&self) -> Result { + self.ensure_root()?; + let path = self.root.join(ROOT_LOCK_FILE); + if let Ok(metadata) = fs::symlink_metadata(&path) + && (!metadata.is_file() || is_link_or_reparse(&metadata)) + { + return Err(StorageError::UnsafePath( + "single-instance lock is not a plain file", + )); + } + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&path) + .map_err(|source| StorageError::Io { + operation: "open single-instance lock", + source, + })?; + let metadata = file.metadata().map_err(|source| StorageError::Io { + operation: "inspect opened single-instance lock", + source, + })?; + if !metadata.is_file() || is_link_or_reparse(&metadata) { + return Err(StorageError::UnsafePath( + "opened single-instance lock is not a plain file", + )); + } + file.try_lock().map_err(|error| match error { + fs::TryLockError::WouldBlock => StorageError::AlreadyRunning, + fs::TryLockError::Error(source) => StorageError::Io { + operation: "acquire single-instance lock", + source, + }, + })?; + Ok(SessionRootLock { _file: file }) + } + + pub(crate) fn write_before( + &self, + session: &SessionFiles, + snapshot: &Snapshot, + ) -> Result<(), StorageError> { + self.write_snapshot(session, &session.before, BEFORE_FILE, snapshot) + } + + pub(crate) fn write_after( + &self, + session: &SessionFiles, + snapshot: &Snapshot, + ) -> Result<(), StorageError> { + self.write_snapshot(session, &session.after, AFTER_FILE, snapshot) + } + + pub(crate) fn read_before(&self, session: &SessionFiles) -> Result { + self.read_snapshot(session, &session.before, BEFORE_FILE) + } + + pub(crate) fn read_after(&self, session: &SessionFiles) -> Result { + self.read_snapshot(session, &session.after, AFTER_FILE) + } + + pub(crate) fn remove_after_if_present( + &self, + session: &SessionFiles, + ) -> Result<(), StorageError> { + self.validate_session(session)?; + remove_regular_file_if_present(&session.after) + } + + pub(crate) fn cleanup_session(&self, session: &SessionFiles) -> Result<(), StorageError> { + self.validate_session(session)?; + validate_session_entries(&session.directory)?; + + remove_regular_file_if_present(&session.before)?; + remove_regular_file_if_present(&session.after)?; + remove_regular_file_if_present(&session.directory.join(OWNERSHIP_MARKER))?; + fs::remove_dir(&session.directory).map_err(|source| StorageError::Io { + operation: "remove session directory", + source, + }) + } + + pub(crate) fn recover_stale_sessions(&self) -> Result { + self.ensure_root()?; + let mut summary = RecoverySummary { + cleaned_sessions: 0, + refused_entries: 0, + }; + let entries = fs::read_dir(&self.root).map_err(|source| StorageError::Io { + operation: "enumerate session root", + source, + })?; + + for entry in entries { + let entry = entry.map_err(|source| StorageError::Io { + operation: "read session root entry", + source, + })?; + let name = entry.file_name(); + let Some(name) = name.to_str() else { + summary.refused_entries += 1; + continue; + }; + if name == ROOT_LOCK_FILE { + continue; + } + if !valid_session_name(name) { + summary.refused_entries += 1; + continue; + } + + let session = SessionFiles::for_directory(entry.path()); + match self.cleanup_session(&session) { + Ok(()) => summary.cleaned_sessions += 1, + Err(_) => summary.refused_entries += 1, + } + } + Ok(summary) + } + + fn ensure_root(&self) -> Result<(), StorageError> { + fs::create_dir_all(&self.root).map_err(|source| StorageError::Io { + operation: "create session root", + source, + })?; + let metadata = fs::symlink_metadata(&self.root).map_err(|source| StorageError::Io { + operation: "inspect session root", + source, + })?; + if !metadata.is_dir() || is_link_or_reparse(&metadata) { + return Err(StorageError::UnsafePath( + "session root is not a plain directory", + )); + } + Ok(()) + } + + fn create_session_named(&self, token: &str) -> Result { + if !valid_session_token(token) { + return Err(StorageError::UnsafePath("invalid generated session token")); + } + let directory = self.root.join(format!("{SESSION_PREFIX}{token}")); + match fs::create_dir(&directory) { + Ok(()) => {} + Err(source) if source.kind() == io::ErrorKind::AlreadyExists => { + return Err(StorageError::Collision); + } + Err(source) => { + return Err(StorageError::Io { + operation: "create session directory", + source, + }); + } + } + + let marker = directory.join(OWNERSHIP_MARKER); + let result = write_new_file(&marker, OWNERSHIP_CONTENTS, "create ownership marker"); + if let Err(error) = result { + let _ = fs::remove_dir(&directory); + return Err(error); + } + Ok(SessionFiles::for_directory(directory)) + } + + fn write_snapshot( + &self, + session: &SessionFiles, + path: &Path, + expected_name: &str, + snapshot: &Snapshot, + ) -> Result<(), StorageError> { + self.validate_file_path(session, path, expected_name)?; + let bytes = serialize_snapshot(snapshot)?; + write_new_file(path, &bytes, "write Snapshot") + } + + fn read_snapshot( + &self, + session: &SessionFiles, + path: &Path, + expected_name: &str, + ) -> Result { + self.validate_file_path(session, path, expected_name)?; + let bytes = read_bounded_file(path)?; + decode_snapshot_document(&bytes).map_err(|error| StorageError::InvalidSnapshot { + details: error.to_string(), + }) + } + + fn validate_file_path( + &self, + session: &SessionFiles, + path: &Path, + expected_name: &str, + ) -> Result<(), StorageError> { + self.validate_session(session)?; + if path.parent() != Some(session.directory.as_path()) + || path.file_name().and_then(|name| name.to_str()) != Some(expected_name) + { + return Err(StorageError::UnsafePath( + "session file escaped its allowlist", + )); + } + Ok(()) + } + + fn validate_session(&self, session: &SessionFiles) -> Result<(), StorageError> { + self.ensure_root()?; + if session.directory.parent() != Some(self.root.as_path()) { + return Err(StorageError::UnsafePath( + "session directory escaped its root", + )); + } + let Some(name) = session.directory.file_name().and_then(|name| name.to_str()) else { + return Err(StorageError::UnsafePath( + "session directory has no valid name", + )); + }; + if !valid_session_name(name) { + return Err(StorageError::UnsafePath( + "session directory name is not owned", + )); + } + + let root = self + .root + .canonicalize() + .map_err(|source| StorageError::Io { + operation: "resolve session root", + source, + })?; + let metadata = + fs::symlink_metadata(&session.directory).map_err(|source| StorageError::Io { + operation: "inspect session directory", + source, + })?; + if !metadata.is_dir() || is_link_or_reparse(&metadata) { + return Err(StorageError::UnsafePath( + "session directory is not a plain directory", + )); + } + let resolved = session + .directory + .canonicalize() + .map_err(|source| StorageError::Io { + operation: "resolve session directory", + source, + })?; + if resolved.parent() != Some(root.as_path()) { + return Err(StorageError::UnsafePath( + "resolved session directory escaped its root", + )); + } + verify_marker(&session.directory.join(OWNERSHIP_MARKER)) + } +} + +impl SessionFiles { + fn for_directory(directory: PathBuf) -> Self { + Self { + before: directory.join(BEFORE_FILE), + after: directory.join(AFTER_FILE), + directory, + } + } + + #[cfg(test)] + pub(crate) fn directory(&self) -> &Path { + &self.directory + } +} + +fn candidate_token() -> String { + let elapsed = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let counter = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); + format!("{:x}-{elapsed:x}-{counter:x}", std::process::id()) +} + +fn valid_session_name(name: &str) -> bool { + name.strip_prefix(SESSION_PREFIX) + .is_some_and(valid_session_token) +} + +fn valid_session_token(token: &str) -> bool { + !token.is_empty() + && token.len() <= 96 + && token + .bytes() + .all(|byte| byte.is_ascii_hexdigit() || byte == b'-') +} + +fn write_new_file(path: &Path, bytes: &[u8], operation: &'static str) -> Result<(), StorageError> { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .map_err(|source| StorageError::Io { operation, source })?; + file.write_all(bytes) + .and_then(|()| file.flush()) + .map_err(|source| StorageError::Io { operation, source }) +} + +fn read_bounded_file(path: &Path) -> Result, StorageError> { + let metadata = fs::symlink_metadata(path).map_err(|source| StorageError::Io { + operation: "inspect Snapshot", + source, + })?; + if !metadata.is_file() || is_link_or_reparse(&metadata) { + return Err(StorageError::UnsafePath("Snapshot is not a plain file")); + } + if metadata.len() > MAX_SNAPSHOT_BYTES { + return Err(StorageError::SnapshotTooLarge); + } + + let file = File::open(path).map_err(|source| StorageError::Io { + operation: "open Snapshot", + source, + })?; + let mut bytes = Vec::with_capacity(metadata.len() as usize); + file.take(MAX_SNAPSHOT_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|source| StorageError::Io { + operation: "read Snapshot", + source, + })?; + if bytes.len() as u64 > MAX_SNAPSHOT_BYTES { + return Err(StorageError::SnapshotTooLarge); + } + Ok(bytes) +} + +fn serialize_snapshot(snapshot: &Snapshot) -> Result, StorageError> { + let mut writer = CappedWriter::new(MAX_SNAPSHOT_BYTES as usize); + serde_json::to_writer_pretty(&mut writer, snapshot).map_err(|error| { + if writer.exceeded { + StorageError::SnapshotTooLarge + } else { + StorageError::Serialize(error.to_string()) + } + })?; + writer.write_all(b"\n").map_err(|error| { + if writer.exceeded { + StorageError::SnapshotTooLarge + } else { + StorageError::Io { + operation: "serialize Snapshot", + source: error, + } + } + })?; + Ok(writer.bytes) +} + +struct CappedWriter { + bytes: Vec, + maximum: usize, + exceeded: bool, +} + +impl CappedWriter { + fn new(maximum: usize) -> Self { + Self { + bytes: Vec::new(), + maximum, + exceeded: false, + } + } +} + +impl Write for CappedWriter { + fn write(&mut self, buffer: &[u8]) -> io::Result { + if buffer.len() > self.maximum.saturating_sub(self.bytes.len()) { + self.exceeded = true; + return Err(io::Error::other("Snapshot exceeds desktop size limit")); + } + self.bytes.extend_from_slice(buffer); + Ok(buffer.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn verify_marker(path: &Path) -> Result<(), StorageError> { + let metadata = fs::symlink_metadata(path).map_err(|source| StorageError::Io { + operation: "inspect ownership marker", + source, + })?; + if !metadata.is_file() || is_link_or_reparse(&metadata) { + return Err(StorageError::UnsafePath( + "ownership marker is not a plain file", + )); + } + let mut contents = Vec::with_capacity(OWNERSHIP_CONTENTS.len() + 1); + File::open(path) + .and_then(|file| { + file.take((OWNERSHIP_CONTENTS.len() + 1) as u64) + .read_to_end(&mut contents) + }) + .map_err(|source| StorageError::Io { + operation: "read ownership marker", + source, + })?; + if contents != OWNERSHIP_CONTENTS { + return Err(StorageError::UnsafePath( + "ownership marker contents do not match", + )); + } + Ok(()) +} + +fn validate_session_entries(directory: &Path) -> Result<(), StorageError> { + for entry in fs::read_dir(directory).map_err(|source| StorageError::Io { + operation: "enumerate session directory", + source, + })? { + let entry = entry.map_err(|source| StorageError::Io { + operation: "read session entry", + source, + })?; + let Some(name) = entry.file_name().to_str().map(ToOwned::to_owned) else { + return Err(StorageError::UnsafePath( + "session contains a non-Unicode entry", + )); + }; + if !matches!(name.as_str(), OWNERSHIP_MARKER | BEFORE_FILE | AFTER_FILE) { + return Err(StorageError::UnsafePath( + "session contains an unexpected entry", + )); + } + let metadata = fs::symlink_metadata(entry.path()).map_err(|source| StorageError::Io { + operation: "inspect session entry", + source, + })?; + if !metadata.is_file() || is_link_or_reparse(&metadata) { + return Err(StorageError::UnsafePath( + "session entry is not a plain file", + )); + } + } + Ok(()) +} + +fn remove_regular_file_if_present(path: &Path) -> Result<(), StorageError> { + let metadata = match fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(source) => { + return Err(StorageError::Io { + operation: "inspect session file before cleanup", + source, + }); + } + }; + if !metadata.is_file() || is_link_or_reparse(&metadata) { + return Err(StorageError::UnsafePath( + "cleanup target is not a plain file", + )); + } + fs::remove_file(path).map_err(|source| StorageError::Io { + operation: "remove session file", + source, + }) +} + +#[cfg(windows)] +fn is_link_or_reparse(metadata: &fs::Metadata) -> bool { + use std::os::windows::fs::MetadataExt; + + const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400; + metadata.file_type().is_symlink() + || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 +} + +#[cfg(not(windows))] +fn is_link_or_reparse(metadata: &fs::Metadata) -> bool { + metadata.file_type().is_symlink() +} + +#[derive(Debug)] +pub(crate) enum StorageError { + Collision, + AlreadyRunning, + CreateAttemptsExhausted, + SnapshotTooLarge, + InvalidSnapshot { + details: String, + }, + Serialize(String), + UnsafePath(&'static str), + Io { + operation: &'static str, + source: io::Error, + }, +} + +impl std::fmt::Display for StorageError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Collision => formatter.write_str("session path already exists"), + Self::AlreadyRunning => { + formatter.write_str("another SystemDiff desktop instance owns the session root") + } + Self::CreateAttemptsExhausted => { + formatter.write_str("could not allocate a collision-free session directory") + } + Self::SnapshotTooLarge => write!( + formatter, + "Snapshot exceeds the {} byte desktop limit", + MAX_SNAPSHOT_BYTES + ), + Self::InvalidSnapshot { details } => { + write!(formatter, "stored Snapshot is invalid: {details}") + } + Self::Serialize(details) => { + write!(formatter, "could not serialize Snapshot: {details}") + } + Self::UnsafePath(details) => { + write!(formatter, "refused unsafe session path: {details}") + } + Self::Io { operation, source } => write!(formatter, "{operation}: {source}"), + } + } +} + +impl std::error::Error for StorageError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io { source, .. } => Some(source), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + static TEST_COUNTER: AtomicU64 = AtomicU64::new(0); + + struct TestDirectory(PathBuf); + + impl TestDirectory { + fn new() -> Self { + let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "systemdiff-desktop-storage-test-{}-{id}", + std::process::id() + )); + fs::create_dir(&path).expect("test directory must be unique"); + Self(path) + } + } + + impl Drop for TestDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + #[test] + fn create_new_rejects_a_collision_without_touching_existing_data() { + let temporary = TestDirectory::new(); + let storage = SessionStorage::new(temporary.0.join("sessions")); + storage.ensure_root().expect("root must be created"); + let occupied = storage.root.join("session-v1-a-b-c"); + fs::create_dir(&occupied).expect("collision directory must be created"); + fs::write(occupied.join("keep.txt"), b"keep").expect("sentinel must be created"); + + let error = storage + .create_session_named("a-b-c") + .expect_err("existing session name must collide"); + + assert!(matches!(error, StorageError::Collision)); + assert_eq!(fs::read(occupied.join("keep.txt")).unwrap(), b"keep"); + } + + #[test] + fn cleanup_refuses_unexpected_entries_and_preserves_the_whole_session() { + let temporary = TestDirectory::new(); + let storage = SessionStorage::new(temporary.0.join("sessions")); + storage.ensure_root().unwrap(); + let session = storage.create_session_named("1-2-3").unwrap(); + fs::write(session.directory.join("unexpected.txt"), b"do not delete").unwrap(); + + let error = storage.cleanup_session(&session).unwrap_err(); + + assert!(matches!(error, StorageError::UnsafePath(_))); + assert!(session.directory.exists()); + assert!(session.directory.join("unexpected.txt").exists()); + } + + #[test] + fn cleanup_refuses_a_session_outside_the_configured_root() { + let temporary = TestDirectory::new(); + let storage = SessionStorage::new(temporary.0.join("sessions")); + storage.ensure_root().unwrap(); + let outside = temporary.0.join("session-v1-a-b-c"); + fs::create_dir(&outside).unwrap(); + fs::write(outside.join(OWNERSHIP_MARKER), OWNERSHIP_CONTENTS).unwrap(); + let session = SessionFiles::for_directory(outside.clone()); + + let error = storage.cleanup_session(&session).unwrap_err(); + + assert!(matches!(error, StorageError::UnsafePath(_))); + assert!(outside.exists()); + } + + #[test] + fn recovery_deletes_only_valid_owned_sessions() { + let temporary = TestDirectory::new(); + let storage = SessionStorage::new(temporary.0.join("sessions")); + storage.ensure_root().unwrap(); + let owned = storage.create_session_named("a-b-c").unwrap(); + fs::write(&owned.before, b"snapshot").unwrap(); + let refused = storage.root.join("session-v1-d-e-f"); + fs::create_dir(&refused).unwrap(); + fs::write(refused.join("other"), b"unknown").unwrap(); + fs::write(storage.root.join("notes.txt"), b"unrelated").unwrap(); + + let summary = storage.recover_stale_sessions().unwrap(); + + assert_eq!(summary.cleaned_sessions, 1); + assert_eq!(summary.refused_entries, 2); + assert!(!owned.directory.exists()); + assert!(refused.exists()); + assert!(storage.root.join("notes.txt").exists()); + } + + #[test] + fn root_lock_refuses_a_second_concurrent_owner() { + let temporary = TestDirectory::new(); + let storage = SessionStorage::new(temporary.0.join("sessions")); + let first = storage.acquire_root_lock().unwrap(); + + let error = storage.acquire_root_lock().unwrap_err(); + + assert!(matches!(error, StorageError::AlreadyRunning)); + drop(first); + storage + .acquire_root_lock() + .expect("lock must be available after the first owner exits"); + } +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..227cd9b --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "SystemDiff", + "version": "0.0.0", + "identifier": "io.github.xiaojuch.systemdiff", + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devUrl": "http://127.0.0.1:1420", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "SystemDiff", + "width": 980, + "height": 720, + "minWidth": 720, + "minHeight": 560, + "resizable": true, + "fullscreen": false, + "dragDropEnabled": false + } + ], + "security": { + "csp": "default-src 'self'; img-src 'self' data:; style-src 'self'; font-src 'self'; connect-src ipc: http://ipc.localhost; script-src 'self'" + } + }, + "bundle": { + "active": false + } +} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx new file mode 100644 index 0000000..f1aefcd --- /dev/null +++ b/apps/desktop/src/App.tsx @@ -0,0 +1,392 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { detectLocale, translate } from "./i18n"; +import type { + DesktopChangeGroup, + DesktopChangeItem, + DesktopCommandError, + DesktopField, + DesktopPresentation, + DesktopSessionState, + DesktopValue, + Locale, + SessionStage, +} from "./types"; + +const initialState: DesktopSessionState = { + stage: "ready", + presentation: null, + cleanup_pending: false, +}; + +function normalizeError(error: unknown): DesktopCommandError { + if (typeof error === "object" && error !== null) { + const candidate = error as Partial; + if ( + typeof candidate.code === "string" && + typeof candidate.message_id === "string" && + typeof candidate.technical_details === "string" + ) { + return candidate as DesktopCommandError; + } + } + if (typeof error === "string") { + try { + return normalizeError(JSON.parse(error)); + } catch { + return { code: "internal", message_id: "error.internal", technical_details: error }; + } + } + return { + code: "internal", + message_id: "error.internal", + technical_details: "The desktop command returned an unrecognized error.", + }; +} + +function localTime(locale: Locale, utc: string): string { + const value = new Date(utc); + if (Number.isNaN(value.getTime())) return utc; + return new Intl.DateTimeFormat(locale, { + hour: "numeric", + minute: "2-digit", + }).format(value); +} + +function groupHeading(locale: Locale, group: DesktopChangeGroup): string { + return translate(locale, group.heading_message_id); +} + +function displayValue(locale: Locale, value: DesktopValue): string { + switch (value.kind) { + case "evidence": + return value.value; + case "message": + return translate(locale, value.message_id); + case "number": + return String(value.value); + case "boolean": + return translate(locale, value.value ? "value.boolean.true" : "value.boolean.false"); + case "evidence_list": + return value.values.length === 0 ? translate(locale, "value.none") : value.values.join(" · "); + } +} + +function Field({ field, locale }: { field: DesktopField; locale: Locale }) { + const label = translate(locale, field.field_id); + const fieldClass = `field-${field.field_id.replaceAll(".", "-")}`; + if (field.mode === "changed") { + return ( +
+
{label}
+
+ {translate(locale, "field.before")}{displayValue(locale, field.before)} + + {translate(locale, "field.after")}{displayValue(locale, field.after)} +
+
+ ); + } + return ( +
+
{label}
+
{displayValue(locale, field.value)}
+
+ ); +} + +function ChangeCard({ item, locale }: { item: DesktopChangeItem; locale: Locale }) { + const marker = item.change === "added" ? "+" : item.change === "removed" ? "−" : item.change === "inconclusive" ? "?" : "~"; + return ( +
+ +
+

{displayValue(locale, item.headline)}

+

{translate(locale, item.message_id)}

+ {item.fields.length > 0 && ( +
+ {item.fields.map((field, index) => )} +
+ )} +
+
+ ); +} + +function Results({ + presentation, + locale, + onNewCapture, + working, + cleanupPending, +}: { + presentation: DesktopPresentation; + locale: Locale; + onNewCapture: () => Promise; + working: boolean; + cleanupPending: boolean; +}) { + const [technicalOpen, setTechnicalOpen] = useState(false); + const [technical, setTechnical] = useState(null); + const [technicalError, setTechnicalError] = useState(null); + const confirmed = presentation.summary.confirmed_change_count; + const inconclusive = presentation.summary.inconclusive_change_count; + + const toggleTechnical = async () => { + const next = !technicalOpen; + setTechnicalOpen(next); + if (next && technical === null && technicalError === null) { + try { + setTechnical(await invoke("get_technical_details")); + } catch (error) { + setTechnicalError(normalizeError(error).technical_details); + } + } + }; + + const summary = confirmed === 0 + ? translate(locale, inconclusive > 0 ? "results.noConfirmedChanges" : "results.noChanges") + : translate(locale, confirmed === 1 ? "results.change.one" : "results.change.many", { count: confirmed }); + + return ( +
+
+
+

SystemDiff

+

{summary}

+ {inconclusive > 0 && ( +

+ {translate(locale, inconclusive === 1 ? "results.inconclusive.one" : "results.inconclusive.many", { count: inconclusive })} +

+ )} +
+
+ {translate(locale, "results.started", { time: localTime(locale, presentation.started_at_utc) })} + {translate(locale, "results.finished", { time: localTime(locale, presentation.finished_at_utc) })} +
+
+ +
+ {presentation.groups.map((group) => ( +
+

{groupHeading(locale, group)}

+ {group.items.length === 0 ? ( +

{translate(locale, group.empty_message_id)}

+ ) : ( +
+ {group.items.map((item, index) => )} +
+ )} +
+ ))} + + {presentation.coverage_notices.length > 0 && ( +
+ +
+

{translate(locale, "coverage.heading")}

+ {presentation.coverage_notices.map((notice, index) => ( +

+ {translate(locale, notice.scope_message_id)} +

+ ))} +
+
+ )} + + {cleanupPending && ( +
+ +
+

{translate(locale, "cleanup.heading")}

+

{translate(locale, "cleanup.pending")}

+
+
+ )} + +
+ + {technicalOpen && ( +
{technicalError ?? technical ?? translate(locale, "technical.loading")}
+ )} +
+
+ +
+ +
+
+ ); +} + +function Busy({ stage, locale }: { stage: Extract; locale: Locale }) { + return ( +
+ +

SystemDiff

+

{translate(locale, `busy.${stage}.title`)}

+

{translate(locale, `busy.${stage}.body`)}

+
+ +
+
+ ); +} + +function Ready({ locale, onStart, working }: { locale: Locale; onStart: () => Promise; working: boolean }) { + return ( +
+
+ +

SystemDiff

+

{translate(locale, "app.promise")}

+

{translate(locale, "ready.body")}

+ +
+ +
+

{translate(locale, "checks.heading")}

+
    +
  • {translate(locale, "checks.startup")}
  • +
  • {translate(locale, "checks.services")}
  • +
  • {translate(locale, "checks.tasks")}{translate(locale, "checks.comingSoon")}
  • +
+
+ +
+ {translate(locale, "trust.local")}· + {translate(locale, "trust.readOnly")}· + {translate(locale, "trust.telemetry")} +
+
+ ); +} + +function Capturing({ locale, onFinish, onCancel, working }: { locale: Locale; onFinish: () => Promise; onCancel: () => Promise; working: boolean }) { + return ( +
+ +

{translate(locale, "capturing.done")}

+

{translate(locale, "capturing.title")}

+

{translate(locale, "capturing.body")}

+
+ + +
+
+ ); +} + +function ErrorView({ error, locale, onRecover, working }: { error: DesktopCommandError; locale: Locale; onRecover: () => Promise; working: boolean }) { + const restartRequired = error.code === "another_instance_running" + || error.code === "bootstrap_storage_failed"; + return ( +
+ +

SystemDiff

+

{translate(locale, "error.title")}

+

{translate(locale, error.message_id)}

+

+ {translate(locale, restartRequired ? "error.restart" : "error.body")} +

+ {!restartRequired && ( + + )} +
+ {translate(locale, "error.technical")} +
{`${error.code}: ${error.technical_details}`}
+
+
+ ); +} + +export default function App() { + const [locale, setLocale] = useState(() => detectLocale()); + const [session, setSession] = useState(initialState); + const [error, setError] = useState(null); + const [working, setWorking] = useState(true); + const inFlight = useRef(false); + + const invokeState = useCallback(async (command: string): Promise => { + if (inFlight.current) return; + inFlight.current = true; + setWorking(true); + try { + setError(null); + if (command === "start_capture") { + setSession({ stage: "starting", presentation: null, cleanup_pending: false }); + } else if (command === "finish_capture") { + setSession((current) => ({ + stage: "finishing", + presentation: current.presentation, + cleanup_pending: current.cleanup_pending, + })); + } + setSession(await invoke(command)); + } catch (commandError) { + setError(normalizeError(commandError)); + } finally { + inFlight.current = false; + setWorking(false); + } + }, []); + + useEffect(() => { + document.documentElement.lang = locale; + }, [locale]); + + useEffect(() => { + let disposed = false; + inFlight.current = true; + void invoke("get_session_state") + .then((next) => { + if (!disposed) setSession(next); + }) + .catch((commandError: unknown) => { + if (!disposed) setError(normalizeError(commandError)); + }) + .finally(() => { + if (!disposed) { + inFlight.current = false; + setWorking(false); + } + }); + return () => { + disposed = true; + }; + }, []); + + let content; + if (error !== null) { + content = invokeState("get_session_state")} />; + } else if (session.stage === "starting" || session.stage === "finishing") { + content = ; + } else if (session.stage === "capturing") { + content = invokeState("finish_capture")} onCancel={() => invokeState("cancel_capture")} />; + } else if (session.stage === "results" && session.presentation !== null) { + content = invokeState("cancel_capture")} />; + } else { + content = invokeState("start_capture")} />; + } + + return ( + <> + + {content} + + ); +} diff --git a/apps/desktop/src/i18n.ts b/apps/desktop/src/i18n.ts new file mode 100644 index 0000000..1db17f4 --- /dev/null +++ b/apps/desktop/src/i18n.ts @@ -0,0 +1,327 @@ +import type { Locale } from "./types"; + +const enUS = { + "app.promise": "See what changed on your Windows PC.", + "language.switch": "Switch interface language", + "ready.body": "Capture your system before and after you install, run, or change an app.", + "ready.start": "Start capture", + "checks.heading": "Currently checks", + "checks.startup": "Startup entries", + "checks.services": "Windows services", + "checks.tasks": "Scheduled tasks", + "checks.comingSoon": "Coming soon", + "trust.local": "Local only", + "trust.readOnly": "Read-only", + "trust.telemetry": "No telemetry", + "busy.starting.title": "Checking your PC…", + "busy.starting.body": "SystemDiff is recording the starting state. This can take a moment.", + "busy.finishing.title": "Comparing changes…", + "busy.finishing.body": "SystemDiff is checking what changed since you started.", + "capturing.done": "Starting state captured", + "capturing.title": "Now install, run, or change the app you want to inspect.", + "capturing.body": "SystemDiff is ready when you are.", + "capturing.finish": "Finish & Compare", + "capturing.cancel": "Cancel", + "results.change.one": "1 change detected", + "results.change.many": "{count} changes detected", + "results.noChanges": "No observed changes", + "results.noConfirmedChanges": "No confirmed changes", + "results.inconclusive.one": "1 possible change could not be confirmed", + "results.inconclusive.many": "{count} possible changes could not be confirmed", + "results.started": "Started {time}", + "results.finished": "Finished {time}", + "results.captureTimes": "Capture times", + "results.new": "New capture", + "group.startup": "Startup", + "group.windows_services": "Windows Services", + "group.other": "Other evidence", + "group.empty.startup": "No observed startup changes", + "group.empty.services": "No observed configuration changes", + "group.empty.other": "No other observed changes", + "group.startup.empty": "No observed startup changes", + "group.windows_services.empty": "No observed configuration changes", + "technical.show": "Technical details", + "technical.hide": "Hide technical details", + "technical.loading": "Loading technical evidence…", + "coverage.heading": "Coverage notice", + "coverage.services.current_token": + "Some protected Windows services may not be visible to your current account.", + "coverage.incomplete": "Some Windows information could not be fully checked.", + "cleanup.heading": "Local evidence cleanup needed", + "cleanup.pending": "SystemDiff could not remove its temporary local evidence. Choose New capture to retry cleanup, and review Technical details.", + "change.startup.added": "Added to your startup", + "change.startup.removed": "Removed from your startup", + "change.startup.modified": "Startup entry changed", + "change.startup.inconclusive": "This startup change could not be confirmed", + "change.service.added": "Windows service added", + "change.service.removed": "Windows service removed", + "change.service.modified": "Windows service configuration changed", + "change.service.inconclusive": "This service change could not be confirmed", + "change.other.added": "Evidence added", + "change.other.removed": "Evidence removed", + "change.other.modified": "Evidence changed", + "change.other.inconclusive": "This change could not be confirmed", + "change.registry_startup.added": "Added to your startup", + "change.registry_startup.removed": "Removed from your startup", + "change.registry_startup.modified": "Startup entry changed", + "change.registry_startup.unchanged": "Startup entry unchanged", + "change.registry_startup.inconclusive": "This startup change could not be confirmed", + "change.windows_service.added": "Windows service added", + "change.windows_service.removed": "Windows service removed", + "change.windows_service.modified": "Windows service configuration changed", + "change.windows_service.unchanged": "Windows service unchanged", + "change.windows_service.inconclusive": "This service change could not be confirmed", + "field.command": "Command", + "field.location": "Location", + "field.service_name": "Service name", + "field.display_name": "Display name", + "field.binary_path": "Binary path", + "field.start_type": "Start type", + "field.error_control": "Error handling", + "field.account": "Account", + "field.dependencies": "Dependencies", + "field.description": "Description", + "field.delayed_auto_start": "Delayed start", + "field.before": "Before", + "field.after": "After", + "registry.location": "Location", + "registry.command": "Command", + "service.name": "Service name", + "service.display_name": "Display name", + "service.type": "Service type", + "service.start": "Start type", + "service.delayed_auto_start": "Delayed start", + "service.error_control": "Error handling", + "service.binary_path": "Binary path", + "service.account": "Account", + "service.dependencies": "Dependencies", + "service.load_order_group": "Load-order group", + "service.tag_id": "Tag ID", + "service.description": "Description", + "value.registry.default_name": "Default value (unnamed)", + "value.registry.undecodable_name": "Registry value with an undecodable name", + "value.registry.unavailable": "Value could not be decoded", + "value.not_set": "Not set", + "value.none": "None", + "value.boolean.true": "Yes", + "value.boolean.false": "No", + "value.service.start.boot": "Boot", + "value.service.start.system": "System", + "value.service.start.automatic": "Automatic", + "value.service.start.automatic_delayed": "Automatic (delayed)", + "value.service.start.manual": "Manual", + "value.service.start.disabled": "Disabled", + "value.service.start.unknown": "Unknown", + "value.service.error_control.ignore": "Ignore", + "value.service.error_control.normal": "Normal", + "value.service.error_control.severe": "Severe", + "value.service.error_control.critical": "Critical", + "value.service.error_control.unknown": "Unknown", + "coverage.incomplete.before": "The first check had limited coverage.", + "coverage.incomplete.after": "The second check had limited coverage.", + "coverage.incomplete.both": "Some information had limited coverage.", + "coverage.scope.current_user_run": "The current-user Run location could not be fully checked.", + "coverage.scope.current_user_run_once": "The current-user RunOnce location could not be fully checked.", + "coverage.scope.local_machine_native_run": "A system-wide startup location could not be fully checked.", + "coverage.scope.local_machine_native_run_once": "A system-wide startup location could not be fully checked.", + "coverage.scope.local_machine_registry32_run": "The 32-bit system-wide Run location could not be fully checked.", + "coverage.scope.local_machine_registry32_run_once": "The 32-bit system-wide RunOnce location could not be fully checked.", + "coverage.scope.local_machine_registry64_run": "The 64-bit system-wide Run location could not be fully checked.", + "coverage.scope.local_machine_registry64_run_once": "The 64-bit system-wide RunOnce location could not be fully checked.", + "coverage.scope.windows_services_current_token": "Some protected Windows services may not be visible to your current account.", + "coverage.scope.unknown": "Some Windows information could not be fully checked.", + "error.title": "SystemDiff couldn't complete that step", + "error.body": "Your previous capture has been kept when it is safe to retry.", + "error.retry": "Return to the app", + "error.technical": "Technical details", + "error.unsupported_windows": "This Windows version is not supported by SystemDiff.", + "error.capture_failed": "SystemDiff couldn't check this PC right now.", + "error.session_storage_failed": "SystemDiff couldn't create its temporary local session.", + "error.bootstrap_storage_failed": "SystemDiff couldn't open its private local workspace.", + "error.invalid_snapshot": "SystemDiff couldn't validate the captured system state.", + "error.compare_failed": "SystemDiff couldn't compare the two captures.", + "error.busy": "SystemDiff is already working on another step.", + "error.invalid_state": "That action is not available at this stage.", + "error.internal": "An internal desktop error occurred.", + "error.another_instance_running": "SystemDiff is already open in another window.", + "error.background_task_failed": "The background check stopped unexpectedly.", + "error.diff_failed": "SystemDiff couldn't compare the two captures.", + "error.invalid_session_state": "That action is not available at this stage.", + "error.session_busy": "SystemDiff is already working on another step.", + "error.session_cleanup_failed": "SystemDiff couldn't remove its temporary local evidence.", + "error.restart": "Close this window, resolve the local issue or close the other SystemDiff window, then open SystemDiff again.", +} as const; + +type KnownMessageId = keyof typeof enUS; + +const zhCN: Record = { + "app.promise": "看看这台 Windows 电脑发生了什么变化。", + "language.switch": "切换界面语言", + "ready.body": "在安装、运行或修改应用前后各记录一次,SystemDiff 会替你比较变化。", + "ready.start": "开始记录", + "checks.heading": "当前检查", + "checks.startup": "开机启动项", + "checks.services": "Windows 服务", + "checks.tasks": "计划任务", + "checks.comingSoon": "即将支持", + "trust.local": "仅在本机处理", + "trust.readOnly": "只读检查", + "trust.telemetry": "无遥测", + "busy.starting.title": "正在检查电脑…", + "busy.starting.body": "SystemDiff 正在记录初始状态,可能需要一点时间。", + "busy.finishing.title": "正在比较变化…", + "busy.finishing.body": "SystemDiff 正在检查从开始记录到现在发生了什么。", + "capturing.done": "初始状态已记录", + "capturing.title": "现在请安装、运行或修改你想检查的应用。", + "capturing.body": "完成后再回到这里,SystemDiff 已准备好。", + "capturing.finish": "完成并比较", + "capturing.cancel": "取消", + "results.change.one": "检测到 1 项变化", + "results.change.many": "检测到 {count} 项变化", + "results.noChanges": "没有观察到变化", + "results.noConfirmedChanges": "没有确认的变化", + "results.inconclusive.one": "有 1 项可能的变化无法确认", + "results.inconclusive.many": "有 {count} 项可能的变化无法确认", + "results.started": "开始于 {time}", + "results.finished": "完成于 {time}", + "results.captureTimes": "记录时间", + "results.new": "重新记录", + "group.startup": "开机启动", + "group.windows_services": "Windows 服务", + "group.other": "其他证据", + "group.empty.startup": "没有观察到启动项变化", + "group.empty.services": "没有观察到服务配置变化", + "group.empty.other": "没有观察到其他变化", + "group.startup.empty": "没有观察到启动项变化", + "group.windows_services.empty": "没有观察到服务配置变化", + "technical.show": "技术详情", + "technical.hide": "收起技术详情", + "technical.loading": "正在加载技术证据…", + "coverage.heading": "检查范围说明", + "coverage.services.current_token": "当前账户可能无法看到部分受保护的 Windows 服务。", + "coverage.incomplete": "部分 Windows 信息未能完整检查。", + "cleanup.heading": "需要清理本地证据", + "cleanup.pending": "SystemDiff 未能删除本地临时证据。请选择“重新记录”再次尝试清理,并查看技术详情。", + "change.startup.added": "已添加到开机启动", + "change.startup.removed": "已从开机启动中移除", + "change.startup.modified": "启动项发生变化", + "change.startup.inconclusive": "无法确认这项启动变化", + "change.service.added": "新增了 Windows 服务", + "change.service.removed": "移除了 Windows 服务", + "change.service.modified": "Windows 服务配置发生变化", + "change.service.inconclusive": "无法确认这项服务变化", + "change.other.added": "新增了证据", + "change.other.removed": "移除了证据", + "change.other.modified": "证据发生变化", + "change.other.inconclusive": "无法确认这项变化", + "change.registry_startup.added": "已添加到开机启动", + "change.registry_startup.removed": "已从开机启动中移除", + "change.registry_startup.modified": "启动项发生变化", + "change.registry_startup.unchanged": "启动项没有变化", + "change.registry_startup.inconclusive": "无法确认这项启动变化", + "change.windows_service.added": "新增了 Windows 服务", + "change.windows_service.removed": "移除了 Windows 服务", + "change.windows_service.modified": "Windows 服务配置发生变化", + "change.windows_service.unchanged": "Windows 服务没有变化", + "change.windows_service.inconclusive": "无法确认这项服务变化", + "field.command": "命令", + "field.location": "位置", + "field.service_name": "服务名称", + "field.display_name": "显示名称", + "field.binary_path": "程序路径", + "field.start_type": "启动方式", + "field.error_control": "错误处理", + "field.account": "运行账户", + "field.dependencies": "依赖项", + "field.description": "说明", + "field.delayed_auto_start": "延迟启动", + "field.before": "之前", + "field.after": "之后", + "registry.location": "位置", + "registry.command": "命令", + "service.name": "服务名称", + "service.display_name": "显示名称", + "service.type": "服务类型", + "service.start": "启动方式", + "service.delayed_auto_start": "延迟启动", + "service.error_control": "错误处理", + "service.binary_path": "程序路径", + "service.account": "运行账户", + "service.dependencies": "依赖项", + "service.load_order_group": "加载顺序组", + "service.tag_id": "Tag ID", + "service.description": "说明", + "value.registry.default_name": "默认值(未命名)", + "value.registry.undecodable_name": "名称无法解码的 Registry value", + "value.registry.unavailable": "无法解码该值", + "value.not_set": "未设置", + "value.none": "无", + "value.boolean.true": "是", + "value.boolean.false": "否", + "value.service.start.boot": "引导时启动", + "value.service.start.system": "系统启动", + "value.service.start.automatic": "自动", + "value.service.start.automatic_delayed": "自动(延迟)", + "value.service.start.manual": "手动", + "value.service.start.disabled": "已禁用", + "value.service.start.unknown": "未知", + "value.service.error_control.ignore": "忽略", + "value.service.error_control.normal": "正常", + "value.service.error_control.severe": "严重", + "value.service.error_control.critical": "关键", + "value.service.error_control.unknown": "未知", + "coverage.incomplete.before": "第一次检查的覆盖范围受限。", + "coverage.incomplete.after": "第二次检查的覆盖范围受限。", + "coverage.incomplete.both": "部分信息的检查范围受限。", + "coverage.scope.current_user_run": "未能完整检查当前用户的 Run 启动位置。", + "coverage.scope.current_user_run_once": "未能完整检查当前用户的 RunOnce 启动位置。", + "coverage.scope.local_machine_native_run": "未能完整检查系统范围的启动位置。", + "coverage.scope.local_machine_native_run_once": "未能完整检查系统范围的启动位置。", + "coverage.scope.local_machine_registry32_run": "未能完整检查 32 位系统范围的 Run 启动位置。", + "coverage.scope.local_machine_registry32_run_once": "未能完整检查 32 位系统范围的 RunOnce 启动位置。", + "coverage.scope.local_machine_registry64_run": "未能完整检查 64 位系统范围的 Run 启动位置。", + "coverage.scope.local_machine_registry64_run_once": "未能完整检查 64 位系统范围的 RunOnce 启动位置。", + "coverage.scope.windows_services_current_token": "当前账户可能无法看到部分受保护的 Windows 服务。", + "coverage.scope.unknown": "部分 Windows 信息未能完整检查。", + "error.title": "SystemDiff 未能完成这一步", + "error.body": "如果可以安全重试,之前的记录会被保留。", + "error.retry": "返回应用", + "error.technical": "技术详情", + "error.unsupported_windows": "SystemDiff 暂不支持当前 Windows 版本。", + "error.capture_failed": "SystemDiff 目前无法检查这台电脑。", + "error.session_storage_failed": "SystemDiff 无法创建本地临时会话。", + "error.bootstrap_storage_failed": "SystemDiff 无法打开自己的本地工作目录。", + "error.invalid_snapshot": "SystemDiff 无法验证刚刚记录的系统状态。", + "error.compare_failed": "SystemDiff 无法比较前后两次记录。", + "error.busy": "SystemDiff 正在执行另一个步骤。", + "error.invalid_state": "当前阶段不能执行这个操作。", + "error.internal": "桌面应用发生了内部错误。", + "error.another_instance_running": "SystemDiff 已在另一个窗口中运行。", + "error.background_task_failed": "后台检查意外停止。", + "error.diff_failed": "SystemDiff 无法比较前后两次记录。", + "error.invalid_session_state": "当前阶段不能执行这个操作。", + "error.session_busy": "SystemDiff 正在执行另一个步骤。", + "error.session_cleanup_failed": "SystemDiff 无法删除本地临时证据。", + "error.restart": "请关闭此窗口,处理本地问题或关闭另一个 SystemDiff 窗口,然后重新打开 SystemDiff。", +}; + +const messages: Record> = { + "en-US": enUS, + "zh-CN": zhCN, +}; + +export function detectLocale(languages: readonly string[] = navigator.languages): Locale { + const preferred = languages[0]?.toLowerCase() ?? ""; + return preferred.startsWith("zh") + ? "zh-CN" + : "en-US"; +} + +export function translate( + locale: Locale, + messageId: string, + values: Record = {}, +): string { + const template = messages[locale][messageId] ?? messages["en-US"][messageId] ?? messageId; + return template.replace(/\{([a-z_]+)\}/gu, (_, key: string) => String(values[key] ?? `{${key}}`)); +} diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx new file mode 100644 index 0000000..693ac2e --- /dev/null +++ b/apps/desktop/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css new file mode 100644 index 0000000..5204fa5 --- /dev/null +++ b/apps/desktop/src/styles.css @@ -0,0 +1,185 @@ +:root { + font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif; + color: #172033; + background: #eef2f7; + font-synthesis: none; + text-rendering: optimizeLegibility; + --accent: #176fca; + --accent-hover: #0e5da9; + --surface: rgba(255, 255, 255, 0.82); + --surface-solid: #ffffff; + --border: rgba(60, 75, 98, 0.16); + --muted: #5d6879; + --quiet: #7d8796; + --success: #237a4b; + --warning: #8a6320; + --shadow: 0 18px 54px rgba(41, 55, 78, 0.10); +} + +* { box-sizing: border-box; } + +html, body, #root { min-width: 320px; min-height: 100%; margin: 0; } + +body { + min-height: 100vh; + background: + radial-gradient(circle at 18% -10%, rgba(95, 161, 225, 0.16), transparent 34rem), + linear-gradient(145deg, #f6f8fb 0%, #e8edf4 100%); +} + +button, summary { font: inherit; } +button { cursor: pointer; } +button:focus-visible, summary:focus-visible { outline: 3px solid rgba(23, 111, 202, 0.32); outline-offset: 3px; } + +.locale-switch { + position: fixed; + z-index: 5; + top: 34px; + right: 36px; + min-height: 30px; + padding: 4px 10px; + border: 1px solid var(--border); + border-radius: 7px; + color: var(--muted); + background: rgba(255, 255, 255, 0.54); + font-size: 0.76rem; +} +.locale-switch:hover { color: inherit; background: rgba(255, 255, 255, 0.82); } + +.app-shell { + width: min(980px, calc(100% - 40px)); + min-height: calc(100vh - 40px); + margin: 20px auto; + border: 1px solid var(--border); + border-radius: 18px; + background: var(--surface); + box-shadow: var(--shadow); + backdrop-filter: blur(18px); + overflow: hidden; +} + +.ready-shell { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr); grid-template-rows: 1fr auto; padding: clamp(32px, 7vw, 76px); gap: 46px; align-items: center; } +.hero { max-width: 610px; } +.eyebrow { margin: 0 0 10px; color: var(--accent); font-size: 0.82rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; } +h1 { margin: 0; font-family: "Segoe UI Variable Display", "Segoe UI", sans-serif; font-size: clamp(2.15rem, 4.5vw, 3.65rem); line-height: 1.06; letter-spacing: -0.035em; font-weight: 620; } +.lead { max-width: 650px; margin: 22px 0 0; color: var(--muted); font-size: clamp(1.02rem, 2vw, 1.2rem); line-height: 1.65; } +.brand-mark { width: 44px; height: 44px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px; margin-bottom: 24px; transform: rotate(2deg); } +.brand-mark span { border-radius: 4px; background: var(--accent); } +.brand-mark span:first-child { transform: translateY(4px); opacity: 0.68; } +.brand-mark span:last-child { transform: translateY(-4px); } + +.primary-button, .secondary-button { + min-height: 44px; + border-radius: 8px; + padding: 10px 22px; + font-weight: 600; + transition: background 120ms ease, border-color 120ms ease, transform 120ms ease; +} +.primary-button { border: 1px solid rgba(0, 0, 0, 0.08); color: #fff; background: var(--accent); box-shadow: 0 4px 12px rgba(23, 111, 202, 0.18); } +.primary-button:hover { background: var(--accent-hover); } +.primary-button:active, .secondary-button:active { transform: translateY(1px); } +.secondary-button { border: 1px solid var(--border); color: inherit; background: rgba(255, 255, 255, 0.62); } +.secondary-button:hover { background: rgba(255, 255, 255, 0.92); } +.hero-action { margin-top: 34px; min-width: 154px; } + +.checks-card { align-self: center; border: 1px solid var(--border); border-radius: 14px; background: rgba(255, 255, 255, 0.62); padding: 26px; } +.checks-card h2 { margin: 0 0 18px; font-size: 0.82rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; } +.checks-card ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 17px; } +.checks-card li { display: grid; grid-template-columns: 24px 1fr auto; align-items: center; gap: 10px; font-weight: 550; } +.check-icon { color: var(--success); font-weight: 750; } +.coming-soon { color: var(--quiet); } +.coming-soon .check-icon { color: var(--quiet); } +.coming-soon small { padding: 3px 7px; border-radius: 5px; background: rgba(91, 106, 126, 0.09); font-size: 0.68rem; font-weight: 650; letter-spacing: 0.025em; } +.trust-row { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 9px; align-items: center; color: var(--quiet); font-size: 0.82rem; } +.trust-row i { font-style: normal; opacity: 0.55; } + +.centered-shell { min-height: calc(100vh - 40px); padding: clamp(40px, 9vw, 90px); display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } +.centered-shell h1 { max-width: 760px; font-size: clamp(2rem, 4.2vw, 3.45rem); } +.centered-shell .lead { max-width: 610px; } +.indeterminate-track { width: min(420px, 80vw); height: 4px; margin-top: 42px; overflow: hidden; border-radius: 999px; background: rgba(23, 111, 202, 0.12); } +.indeterminate-track span { display: block; width: 34%; height: 100%; border-radius: inherit; background: var(--accent); animation: indeterminate 1.3s ease-in-out infinite; } +@keyframes indeterminate { 0% { transform: translateX(-110%); } 50% { transform: translateX(95%); } 100% { transform: translateX(300%); } } +@media (prefers-reduced-motion: reduce) { .indeterminate-track span { animation-duration: 3s; } } + +.success-ring, .error-symbol { width: 64px; height: 64px; display: grid; place-items: center; margin-bottom: 22px; border-radius: 50%; font-size: 1.65rem; font-weight: 700; } +.success-ring { color: var(--success); background: rgba(35, 122, 75, 0.10); border: 1px solid rgba(35, 122, 75, 0.24); } +.error-symbol { color: #a3483d; background: rgba(163, 72, 61, 0.09); border: 1px solid rgba(163, 72, 61, 0.2); } +.status-label { margin: 0 0 22px; color: var(--success); font-weight: 650; } +.action-row { display: flex; gap: 12px; margin-top: 38px; } +.muted-copy { color: var(--quiet); } +.error-details { width: min(680px, 100%); margin-top: 26px; text-align: left; } +.error-details summary { cursor: pointer; color: var(--muted); } +.error-details pre, .technical-panel pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 16px; border: 1px solid var(--border); border-radius: 9px; background: rgba(10, 20, 34, 0.05); color: var(--muted); font: 0.78rem/1.55 "Cascadia Mono", Consolas, monospace; } + +.results-shell { display: flex; flex-direction: column; } +.results-header { display: flex; justify-content: space-between; gap: 30px; padding: 40px clamp(26px, 6vw, 62px) 32px; border-bottom: 1px solid var(--border); background: rgba(255, 255, 255, 0.34); } +.results-header h1 { font-size: clamp(2rem, 4vw, 3.15rem); } +.capture-times { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; justify-content: end; gap: 5px; color: var(--quiet); font-size: 0.8rem; } +.inconclusive-summary { color: var(--warning); margin: 12px 0 0; } +.results-content { padding: 10px clamp(26px, 6vw, 62px) 30px; } +.result-group { padding: 28px 0 30px; border-bottom: 1px solid var(--border); } +.result-group h2, .coverage-notice h2, .cleanup-notice h2 { margin: 0 0 16px; font-size: 1.05rem; font-weight: 650; } +.empty-state { margin: 0; color: var(--quiet); } +.change-list { display: grid; gap: 12px; } +.change-card { display: grid; grid-template-columns: 34px 1fr; gap: 12px; padding: 19px; border: 1px solid var(--border); border-radius: 11px; background: rgba(255, 255, 255, 0.52); } +.change-marker { width: 26px; height: 26px; display: grid; place-items: center; border-radius: 50%; color: var(--accent); background: rgba(23, 111, 202, 0.10); font-weight: 750; } +.change-removed .change-marker { color: #9f5147; background: rgba(159, 81, 71, 0.10); } +.change-inconclusive .change-marker { color: var(--warning); background: rgba(138, 99, 32, 0.10); } +.change-body h3 { margin: 1px 0 4px; font-size: 1rem; overflow-wrap: anywhere; } +.change-meaning { margin: 0; color: var(--muted); } +.evidence-list { margin: 17px 0 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; } +.evidence-field { min-width: 0; } +.evidence-field dt { margin-bottom: 4px; color: var(--quiet); font-size: 0.72rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.045em; } +.evidence-field dd { margin: 0; overflow-wrap: anywhere; font: 0.82rem/1.5 "Cascadia Mono", Consolas, monospace; } +.evidence-comparison { grid-column: 1 / -1; } +.field-registry-location, .field-registry-command { grid-column: 1 / -1; } +.evidence-comparison dd { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 12px; } +.evidence-comparison span:not(.comparison-arrow) { display: flex; flex-direction: column; gap: 3px; min-width: 0; } +.evidence-comparison small { color: var(--quiet); font: 0.7rem "Segoe UI", sans-serif; } +.comparison-arrow { color: var(--quiet); } +.coverage-notice, .cleanup-notice { display: grid; grid-template-columns: 30px 1fr; gap: 12px; margin: 28px 0 12px; padding: 18px; border: 1px solid rgba(138, 99, 32, 0.2); border-radius: 10px; background: rgba(255, 246, 224, 0.46); } +.coverage-notice h2, .cleanup-notice h2 { margin-bottom: 5px; } +.coverage-notice p, .cleanup-notice p { margin: 0; color: var(--muted); line-height: 1.5; } +.notice-icon { width: 24px; height: 24px; display: grid; place-items: center; border: 1px solid rgba(138, 99, 32, 0.34); border-radius: 50%; color: var(--warning); font-weight: 700; } +.primary-button:disabled, .secondary-button:disabled { cursor: wait; opacity: 0.58; transform: none; box-shadow: none; } +.technical-panel { padding: 14px 0 0; } +.disclosure { display: flex; gap: 8px; padding: 8px 0; border: 0; color: var(--muted); background: transparent; } +.technical-panel pre { max-height: 360px; overflow: auto; } +.results-actions { position: sticky; z-index: 3; bottom: 0; padding: 16px clamp(26px, 6vw, 62px); margin-top: auto; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); background: rgba(255, 255, 255, 0.92); } + +@media (max-width: 720px) { + .locale-switch { top: 12px; right: 14px; } + .app-shell { width: 100%; min-height: 100vh; margin: 0; border: 0; border-radius: 0; } + .ready-shell { grid-template-columns: 1fr; grid-template-rows: auto auto auto; padding: 38px 26px; gap: 32px; } + .trust-row { grid-column: 1; } + .results-header { flex-direction: column; } + .capture-times { align-items: flex-start; } + .evidence-comparison dd { grid-template-columns: 1fr; } + .comparison-arrow { transform: rotate(90deg); justify-self: start; } +} + +@media (prefers-color-scheme: dark) { + :root { + color: #edf2f9; + background: #151a22; + --accent: #66a9ec; + --accent-hover: #82baf0; + --surface: rgba(29, 36, 47, 0.88); + --surface-solid: #1d242f; + --border: rgba(208, 222, 240, 0.13); + --muted: #b5c0cf; + --quiet: #8f9baa; + --success: #6fca98; + --warning: #e4bd6f; + --shadow: 0 18px 54px rgba(0, 0, 0, 0.24); + } + body { background: radial-gradient(circle at 18% -10%, rgba(40, 105, 168, 0.19), transparent 34rem), linear-gradient(145deg, #171d26, #11161d); } + .primary-button { color: #071523; background: #78b8f3; } + .primary-button:hover { color: #071523; background: #93c7f7; } + .secondary-button, .checks-card, .change-card { background: rgba(255, 255, 255, 0.045); } + .locale-switch { background: rgba(255, 255, 255, 0.05); } + .results-header { background: rgba(255, 255, 255, 0.035); } + .results-actions { background: rgba(29, 36, 47, 0.97); } + .secondary-button:hover { background: rgba(255, 255, 255, 0.08); } + .coverage-notice, .cleanup-notice { background: rgba(138, 99, 32, 0.11); } +} diff --git a/apps/desktop/src/types.ts b/apps/desktop/src/types.ts new file mode 100644 index 0000000..af1c87c --- /dev/null +++ b/apps/desktop/src/types.ts @@ -0,0 +1,84 @@ +export type Locale = "en-US" | "zh-CN"; + +export type SessionStage = + | "ready" + | "starting" + | "capturing" + | "finishing" + | "results"; + +export interface DesktopSessionState { + stage: SessionStage; + presentation: DesktopPresentation | null; + cleanup_pending: boolean; +} + +export interface DesktopPresentation { + contract_version: 1; + started_at_utc: string; + finished_at_utc: string; + summary: DesktopSummary; + groups: DesktopChangeGroup[]; + coverage_notices: DesktopCoverageNotice[]; +} + +export interface DesktopSummary { + confirmed_change_count: number; + inconclusive_change_count: number; +} + +export type DesktopGroupId = "startup" | "windows_services" | "other"; +export type DesktopChangeKind = + | "added" + | "removed" + | "modified" + | "unchanged" + | "inconclusive"; + +export interface DesktopChangeGroup { + group_id: DesktopGroupId; + heading_message_id: string; + empty_message_id: string; + items: DesktopChangeItem[]; +} + +export interface DesktopChangeItem { + change: DesktopChangeKind; + message_id: string; + headline: DesktopValue; + fields: DesktopField[]; +} + +export type DesktopField = + | { field_id: string; mode: "current"; value: DesktopValue } + | { field_id: string; mode: "changed"; before: DesktopValue; after: DesktopValue }; + +export type DesktopValue = + | { kind: "evidence"; value: string } + | { kind: "message"; message_id: string } + | { kind: "number"; value: number } + | { kind: "boolean"; value: boolean } + | { kind: "evidence_list"; values: string[] }; + +export interface DesktopCoverageNotice { + group_id: DesktopGroupId | null; + message_id: string; + scope_message_id: string; + before_status: DesktopCoverageStatus; + after_status: DesktopCoverageStatus; +} + +export type DesktopCoverageStatus = + | "complete" + | "partial" + | "permission_denied" + | "unavailable" + | "unsupported" + | "failed" + | "not_present"; + +export interface DesktopCommandError { + code: string; + message_id: string; + technical_details: string; +} diff --git a/apps/desktop/src/vite-env.d.ts b/apps/desktop/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/desktop/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 0000000..a35d693 --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts new file mode 100644 index 0000000..2c4e0cd --- /dev/null +++ b/apps/desktop/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + host: "127.0.0.1", + port: 1420, + strictPort: true, + }, +}); diff --git a/crates/systemdiff-core/src/lib.rs b/crates/systemdiff-core/src/lib.rs index 2315006..b114419 100644 --- a/crates/systemdiff-core/src/lib.rs +++ b/crates/systemdiff-core/src/lib.rs @@ -647,7 +647,9 @@ fn decode_utf16le_hex(encoded: &str) -> Option> { } encoded .as_bytes() - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|chunk| { let low = (hex_nibble(chunk[0])? << 4) | hex_nibble(chunk[1])?; let high = (hex_nibble(chunk[2])? << 4) | hex_nibble(chunk[3])?; diff --git a/crates/systemdiff-report/src/lib.rs b/crates/systemdiff-report/src/lib.rs index 2575e5f..4aeb5d9 100644 --- a/crates/systemdiff-report/src/lib.rs +++ b/crates/systemdiff-report/src/lib.rs @@ -1,5 +1,7 @@ #![forbid(unsafe_code)] +pub mod presentation; + use serde::Serialize; use std::error::Error; use std::fmt; diff --git a/crates/systemdiff-report/src/presentation.rs b/crates/systemdiff-report/src/presentation.rs new file mode 100644 index 0000000..5b7197b --- /dev/null +++ b/crates/systemdiff-report/src/presentation.rs @@ -0,0 +1,1064 @@ +//! Locale-neutral presentation data for the desktop IPC boundary. +//! +//! Rust owns change classification and evidence-to-field mapping. The desktop +//! frontend only translates stable semantic identifiers and renders values. + +use serde::Serialize; +use systemdiff_core::{ + Artifact, CollectorStatus, RegistryDecodedValue, RegistryHive, RegistryStartupEntry, + RegistryStartupKind, RegistryValueDecoding, RegistryValueName, RegistryView, WindowsService, +}; +use systemdiff_diff::{ArtifactChange, ChangeKind, DiffDocument, DiffWarning}; + +use super::terminal_text; + +pub const DESKTOP_PRESENTATION_CONTRACT_VERSION: u32 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct DesktopPresentation { + pub contract_version: u32, + pub started_at_utc: String, + pub finished_at_utc: String, + pub summary: DesktopSummary, + pub groups: Vec, + pub coverage_notices: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct DesktopSummary { + pub confirmed_change_count: u64, + pub inconclusive_change_count: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct DesktopGroup { + pub group_id: DesktopGroupId, + pub heading_message_id: DesktopMessageId, + pub empty_message_id: DesktopMessageId, + pub items: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DesktopGroupId { + Startup, + WindowsServices, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct DesktopChangeItem { + pub change: DesktopChangeKind, + pub message_id: DesktopMessageId, + pub headline: DesktopValue, + pub fields: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DesktopChangeKind { + Added, + Removed, + Modified, + Unchanged, + Inconclusive, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct DesktopField { + pub field_id: DesktopFieldId, + #[serde(flatten)] + pub content: DesktopFieldContent, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "mode", rename_all = "snake_case")] +pub enum DesktopFieldContent { + Current { + value: DesktopValue, + }, + Changed { + before: DesktopValue, + after: DesktopValue, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum DesktopValue { + Evidence { value: String }, + Message { message_id: DesktopMessageId }, + Number { value: u64 }, + Boolean { value: bool }, + EvidenceList { values: Vec }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum DesktopFieldId { + #[serde(rename = "registry.location")] + RegistryLocation, + #[serde(rename = "registry.command")] + RegistryCommand, + #[serde(rename = "service.name")] + ServiceName, + #[serde(rename = "service.display_name")] + ServiceDisplayName, + #[serde(rename = "service.type")] + ServiceType, + #[serde(rename = "service.start")] + ServiceStart, + #[serde(rename = "service.delayed_auto_start")] + ServiceDelayedAutoStart, + #[serde(rename = "service.error_control")] + ServiceErrorControl, + #[serde(rename = "service.binary_path")] + ServiceBinaryPath, + #[serde(rename = "service.account")] + ServiceAccount, + #[serde(rename = "service.dependencies")] + ServiceDependencies, + #[serde(rename = "service.load_order_group")] + ServiceLoadOrderGroup, + #[serde(rename = "service.tag_id")] + ServiceTagId, + #[serde(rename = "service.description")] + ServiceDescription, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct DesktopCoverageNotice { + pub group_id: Option, + pub message_id: DesktopMessageId, + pub scope_message_id: DesktopMessageId, + pub before_status: DesktopCoverageStatus, + pub after_status: DesktopCoverageStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DesktopCoverageStatus { + Complete, + Partial, + PermissionDenied, + Unavailable, + Unsupported, + Failed, + NotPresent, +} + +/// Stable localization keys exposed at the desktop IPC boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum DesktopMessageId { + #[serde(rename = "group.startup")] + GroupStartup, + #[serde(rename = "group.startup.empty")] + GroupStartupEmpty, + #[serde(rename = "group.windows_services")] + GroupWindowsServices, + #[serde(rename = "group.windows_services.empty")] + GroupWindowsServicesEmpty, + #[serde(rename = "change.registry_startup.added")] + RegistryStartupAdded, + #[serde(rename = "change.registry_startup.removed")] + RegistryStartupRemoved, + #[serde(rename = "change.registry_startup.modified")] + RegistryStartupModified, + #[serde(rename = "change.registry_startup.unchanged")] + RegistryStartupUnchanged, + #[serde(rename = "change.registry_startup.inconclusive")] + RegistryStartupInconclusive, + #[serde(rename = "change.windows_service.added")] + WindowsServiceAdded, + #[serde(rename = "change.windows_service.removed")] + WindowsServiceRemoved, + #[serde(rename = "change.windows_service.modified")] + WindowsServiceModified, + #[serde(rename = "change.windows_service.unchanged")] + WindowsServiceUnchanged, + #[serde(rename = "change.windows_service.inconclusive")] + WindowsServiceInconclusive, + #[serde(rename = "value.registry.default_name")] + RegistryDefaultValueName, + #[serde(rename = "value.registry.undecodable_name")] + RegistryUndecodableValueName, + #[serde(rename = "value.registry.unavailable")] + RegistryValueUnavailable, + #[serde(rename = "value.not_set")] + ValueNotSet, + #[serde(rename = "value.none")] + ValueNone, + #[serde(rename = "value.service.start.boot")] + ServiceStartBoot, + #[serde(rename = "value.service.start.system")] + ServiceStartSystem, + #[serde(rename = "value.service.start.automatic")] + ServiceStartAutomatic, + #[serde(rename = "value.service.start.automatic_delayed")] + ServiceStartAutomaticDelayed, + #[serde(rename = "value.service.start.manual")] + ServiceStartManual, + #[serde(rename = "value.service.start.disabled")] + ServiceStartDisabled, + #[serde(rename = "value.service.start.unknown")] + ServiceStartUnknown, + #[serde(rename = "value.service.error_control.ignore")] + ServiceErrorControlIgnore, + #[serde(rename = "value.service.error_control.normal")] + ServiceErrorControlNormal, + #[serde(rename = "value.service.error_control.severe")] + ServiceErrorControlSevere, + #[serde(rename = "value.service.error_control.critical")] + ServiceErrorControlCritical, + #[serde(rename = "value.service.error_control.unknown")] + ServiceErrorControlUnknown, + #[serde(rename = "coverage.incomplete.before")] + CoverageIncompleteBefore, + #[serde(rename = "coverage.incomplete.after")] + CoverageIncompleteAfter, + #[serde(rename = "coverage.incomplete.both")] + CoverageIncompleteBoth, + #[serde(rename = "coverage.scope.current_user_run")] + CoverageScopeCurrentUserRun, + #[serde(rename = "coverage.scope.current_user_run_once")] + CoverageScopeCurrentUserRunOnce, + #[serde(rename = "coverage.scope.local_machine_native_run")] + CoverageScopeLocalMachineNativeRun, + #[serde(rename = "coverage.scope.local_machine_native_run_once")] + CoverageScopeLocalMachineNativeRunOnce, + #[serde(rename = "coverage.scope.local_machine_registry32_run")] + CoverageScopeLocalMachineRegistry32Run, + #[serde(rename = "coverage.scope.local_machine_registry32_run_once")] + CoverageScopeLocalMachineRegistry32RunOnce, + #[serde(rename = "coverage.scope.local_machine_registry64_run")] + CoverageScopeLocalMachineRegistry64Run, + #[serde(rename = "coverage.scope.local_machine_registry64_run_once")] + CoverageScopeLocalMachineRegistry64RunOnce, + #[serde(rename = "coverage.scope.windows_services_current_token")] + CoverageScopeWindowsServicesCurrentToken, + #[serde(rename = "coverage.scope.unknown")] + CoverageScopeUnknown, +} + +pub fn build_desktop_presentation(diff: &DiffDocument) -> DesktopPresentation { + let mut startup_items = Vec::new(); + let mut service_items = Vec::new(); + + for change in &diff.changes { + match change_artifact(change) { + Some(Artifact::RegistryStartup(_)) => { + if let Some(item) = registry_item(change) { + startup_items.push(item); + } + } + Some(Artifact::WindowsService(_)) => { + if let Some(item) = service_item(change) { + service_items.push(item); + } + } + Some(Artifact::ScheduledTask(_)) | None => {} + } + } + + let confirmed_change_count = u64::try_from( + startup_items + .iter() + .chain(&service_items) + .filter(|item| { + matches!( + item.change, + DesktopChangeKind::Added + | DesktopChangeKind::Removed + | DesktopChangeKind::Modified + ) + }) + .count(), + ) + .unwrap_or(u64::MAX); + let inconclusive_change_count = u64::try_from( + startup_items + .iter() + .chain(&service_items) + .filter(|item| item.change == DesktopChangeKind::Inconclusive) + .count(), + ) + .unwrap_or(u64::MAX); + + DesktopPresentation { + contract_version: DESKTOP_PRESENTATION_CONTRACT_VERSION, + started_at_utc: diff.before_captured_at.clone(), + finished_at_utc: diff.after_captured_at.clone(), + summary: DesktopSummary { + confirmed_change_count, + inconclusive_change_count, + }, + groups: vec![ + DesktopGroup { + group_id: DesktopGroupId::Startup, + heading_message_id: DesktopMessageId::GroupStartup, + empty_message_id: DesktopMessageId::GroupStartupEmpty, + items: startup_items, + }, + DesktopGroup { + group_id: DesktopGroupId::WindowsServices, + heading_message_id: DesktopMessageId::GroupWindowsServices, + empty_message_id: DesktopMessageId::GroupWindowsServicesEmpty, + items: service_items, + }, + ], + coverage_notices: diff.warnings.iter().map(coverage_notice).collect(), + } +} + +fn registry_item(change: &ArtifactChange) -> Option { + let entry = registry_change_entry(change)?; + let (kind, message_id) = change_semantics( + &change.change, + DesktopMessageId::RegistryStartupAdded, + DesktopMessageId::RegistryStartupRemoved, + DesktopMessageId::RegistryStartupModified, + DesktopMessageId::RegistryStartupUnchanged, + DesktopMessageId::RegistryStartupInconclusive, + ); + let mut fields = vec![current_field( + DesktopFieldId::RegistryLocation, + DesktopValue::Evidence { + value: registry_location(entry), + }, + )]; + + match &change.change { + ChangeKind::Modified { + before: Artifact::RegistryStartup(before), + after: Artifact::RegistryStartup(after), + } => { + let before = registry_value(before); + let after = registry_value(after); + if before != after { + fields.push(changed_field( + DesktopFieldId::RegistryCommand, + before, + after, + )); + } else { + fields.push(current_field(DesktopFieldId::RegistryCommand, after)); + } + } + _ => fields.push(current_field( + DesktopFieldId::RegistryCommand, + registry_value(entry), + )), + } + + Some(DesktopChangeItem { + change: kind, + message_id, + headline: registry_name(&entry.value_name), + fields, + }) +} + +fn service_item(change: &ArtifactChange) -> Option { + let service = service_change_entry(change)?; + let (kind, message_id) = change_semantics( + &change.change, + DesktopMessageId::WindowsServiceAdded, + DesktopMessageId::WindowsServiceRemoved, + DesktopMessageId::WindowsServiceModified, + DesktopMessageId::WindowsServiceUnchanged, + DesktopMessageId::WindowsServiceInconclusive, + ); + let fields = match &change.change { + ChangeKind::Modified { + before: Artifact::WindowsService(before), + after: Artifact::WindowsService(after), + } => changed_service_fields(before, after), + ChangeKind::Unchanged { .. } => Vec::new(), + _ => service_summary_fields(service), + }; + + Some(DesktopChangeItem { + change: kind, + message_id, + headline: service + .display_name + .as_deref() + .filter(|value| !value.is_empty()) + .map(evidence) + .unwrap_or_else(|| evidence(&service.service_name)), + fields, + }) +} + +fn service_summary_fields(service: &WindowsService) -> Vec { + vec![ + current_field(DesktopFieldId::ServiceName, evidence(&service.service_name)), + current_field( + DesktopFieldId::ServiceStart, + service_start(service.start_type, service.delayed_auto_start), + ), + current_field( + DesktopFieldId::ServiceBinaryPath, + evidence(&service.binary_path), + ), + current_field( + DesktopFieldId::ServiceAccount, + optional_evidence(service.account.as_deref()), + ), + ] +} + +fn changed_service_fields(before: &WindowsService, after: &WindowsService) -> Vec { + let mut fields = Vec::new(); + push_changed( + &mut fields, + DesktopFieldId::ServiceName, + &before.service_name, + &after.service_name, + evidence(&before.service_name), + evidence(&after.service_name), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceDisplayName, + &before.display_name, + &after.display_name, + optional_evidence(before.display_name.as_deref()), + optional_evidence(after.display_name.as_deref()), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceType, + &before.service_type, + &after.service_type, + number(before.service_type), + number(after.service_type), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceStart, + &(before.start_type, before.delayed_auto_start), + &(after.start_type, after.delayed_auto_start), + service_start(before.start_type, before.delayed_auto_start), + service_start(after.start_type, after.delayed_auto_start), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceDelayedAutoStart, + &before.delayed_auto_start, + &after.delayed_auto_start, + DesktopValue::Boolean { + value: before.delayed_auto_start, + }, + DesktopValue::Boolean { + value: after.delayed_auto_start, + }, + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceErrorControl, + &before.error_control, + &after.error_control, + service_error_control(before.error_control), + service_error_control(after.error_control), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceBinaryPath, + &before.binary_path, + &after.binary_path, + evidence(&before.binary_path), + evidence(&after.binary_path), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceAccount, + &before.account, + &after.account, + optional_evidence(before.account.as_deref()), + optional_evidence(after.account.as_deref()), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceDependencies, + &before.dependencies, + &after.dependencies, + evidence_list(&before.dependencies), + evidence_list(&after.dependencies), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceLoadOrderGroup, + &before.load_order_group, + &after.load_order_group, + optional_evidence(before.load_order_group.as_deref()), + optional_evidence(after.load_order_group.as_deref()), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceTagId, + &before.tag_id, + &after.tag_id, + optional_number(before.tag_id), + optional_number(after.tag_id), + ); + push_changed( + &mut fields, + DesktopFieldId::ServiceDescription, + &before.description, + &after.description, + optional_evidence(before.description.as_deref()), + optional_evidence(after.description.as_deref()), + ); + fields +} + +fn push_changed( + fields: &mut Vec, + field_id: DesktopFieldId, + before_source: &T, + after_source: &T, + before: DesktopValue, + after: DesktopValue, +) { + if before_source != after_source { + fields.push(changed_field(field_id, before, after)); + } +} + +fn coverage_notice(warning: &DiffWarning) -> DesktopCoverageNotice { + let before_status = coverage_status(warning.before_status); + let after_status = coverage_status(warning.after_status); + let message_id = match ( + warning.before_status == Some(CollectorStatus::Complete), + warning.after_status == Some(CollectorStatus::Complete), + ) { + (false, true) => DesktopMessageId::CoverageIncompleteBefore, + (true, false) => DesktopMessageId::CoverageIncompleteAfter, + _ => DesktopMessageId::CoverageIncompleteBoth, + }; + + DesktopCoverageNotice { + group_id: match warning.collector_id.as_str() { + "windows.registry.startup" => Some(DesktopGroupId::Startup), + "windows.services" => Some(DesktopGroupId::WindowsServices), + _ => None, + }, + message_id, + scope_message_id: coverage_scope(&warning.collector_id, &warning.scope_id), + before_status, + after_status, + } +} + +fn coverage_scope(collector_id: &str, scope_id: &str) -> DesktopMessageId { + match (collector_id, scope_id) { + ("windows.registry.startup", "current_user.shared.run") => { + DesktopMessageId::CoverageScopeCurrentUserRun + } + ("windows.registry.startup", "current_user.shared.run_once") => { + DesktopMessageId::CoverageScopeCurrentUserRunOnce + } + ("windows.registry.startup", "local_machine.native.run") => { + DesktopMessageId::CoverageScopeLocalMachineNativeRun + } + ("windows.registry.startup", "local_machine.native.run_once") => { + DesktopMessageId::CoverageScopeLocalMachineNativeRunOnce + } + ("windows.registry.startup", "local_machine.registry32.run") => { + DesktopMessageId::CoverageScopeLocalMachineRegistry32Run + } + ("windows.registry.startup", "local_machine.registry32.run_once") => { + DesktopMessageId::CoverageScopeLocalMachineRegistry32RunOnce + } + ("windows.registry.startup", "local_machine.registry64.run") => { + DesktopMessageId::CoverageScopeLocalMachineRegistry64Run + } + ("windows.registry.startup", "local_machine.registry64.run_once") => { + DesktopMessageId::CoverageScopeLocalMachineRegistry64RunOnce + } + ("windows.services", "current_token.win32") => { + DesktopMessageId::CoverageScopeWindowsServicesCurrentToken + } + _ => DesktopMessageId::CoverageScopeUnknown, + } +} + +fn coverage_status(status: Option) -> DesktopCoverageStatus { + match status { + Some(CollectorStatus::Complete) => DesktopCoverageStatus::Complete, + Some(CollectorStatus::Partial) => DesktopCoverageStatus::Partial, + Some(CollectorStatus::PermissionDenied) => DesktopCoverageStatus::PermissionDenied, + Some(CollectorStatus::Unavailable) => DesktopCoverageStatus::Unavailable, + Some(CollectorStatus::Unsupported) => DesktopCoverageStatus::Unsupported, + Some(CollectorStatus::Failed) => DesktopCoverageStatus::Failed, + None => DesktopCoverageStatus::NotPresent, + } +} + +fn change_artifact(change: &ArtifactChange) -> Option<&Artifact> { + match &change.change { + ChangeKind::Added { after } | ChangeKind::Modified { after, .. } => Some(after), + ChangeKind::Removed { before } => Some(before), + ChangeKind::Unchanged { artifact } => Some(artifact), + ChangeKind::Inconclusive { before, after, .. } => before.as_ref().or(after.as_ref()), + } +} + +fn registry_change_entry(change: &ArtifactChange) -> Option<&RegistryStartupEntry> { + match change_artifact(change) { + Some(Artifact::RegistryStartup(entry)) => Some(entry), + _ => None, + } +} + +fn service_change_entry(change: &ArtifactChange) -> Option<&WindowsService> { + match change_artifact(change) { + Some(Artifact::WindowsService(service)) => Some(service), + _ => None, + } +} + +fn change_semantics( + change: &ChangeKind, + added: DesktopMessageId, + removed: DesktopMessageId, + modified: DesktopMessageId, + unchanged: DesktopMessageId, + inconclusive: DesktopMessageId, +) -> (DesktopChangeKind, DesktopMessageId) { + match change { + ChangeKind::Added { .. } => (DesktopChangeKind::Added, added), + ChangeKind::Removed { .. } => (DesktopChangeKind::Removed, removed), + ChangeKind::Modified { .. } => (DesktopChangeKind::Modified, modified), + ChangeKind::Unchanged { .. } => (DesktopChangeKind::Unchanged, unchanged), + ChangeKind::Inconclusive { .. } => (DesktopChangeKind::Inconclusive, inconclusive), + } +} + +fn current_field(field_id: DesktopFieldId, value: DesktopValue) -> DesktopField { + DesktopField { + field_id, + content: DesktopFieldContent::Current { value }, + } +} + +fn changed_field( + field_id: DesktopFieldId, + before: DesktopValue, + after: DesktopValue, +) -> DesktopField { + DesktopField { + field_id, + content: DesktopFieldContent::Changed { before, after }, + } +} + +fn registry_name(name: &RegistryValueName) -> DesktopValue { + match name { + RegistryValueName::Decoded { value } if value.is_empty() => DesktopValue::Message { + message_id: DesktopMessageId::RegistryDefaultValueName, + }, + RegistryValueName::Decoded { value } => evidence(value), + RegistryValueName::InvalidUtf16 { .. } => DesktopValue::Message { + message_id: DesktopMessageId::RegistryUndecodableValueName, + }, + } +} + +fn registry_value(entry: &RegistryStartupEntry) -> DesktopValue { + match &entry.decoding { + RegistryValueDecoding::Decoded { value } => match value { + RegistryDecodedValue::String { value } + | RegistryDecodedValue::ExpandString { value } => evidence(value), + RegistryDecodedValue::MultiString { values } => evidence_list(values), + RegistryDecodedValue::Dword { value } => number(*value), + RegistryDecodedValue::Qword { value } => DesktopValue::Number { value: *value }, + }, + RegistryValueDecoding::NotApplicable + | RegistryValueDecoding::InvalidData + | RegistryValueDecoding::UnsupportedType => DesktopValue::Message { + message_id: DesktopMessageId::RegistryValueUnavailable, + }, + } +} + +fn registry_location(entry: &RegistryStartupEntry) -> String { + let hive = match entry.hive { + RegistryHive::CurrentUser => "HKCU", + RegistryHive::LocalMachine => "HKLM", + }; + let view = match entry.registry_view { + RegistryView::Shared => "shared", + RegistryView::Native => "native", + RegistryView::Registry32 => "registry32", + RegistryView::Registry64 => "registry64", + }; + let startup_kind = match entry.startup_kind { + RegistryStartupKind::Run => "Run", + RegistryStartupKind::RunOnce => "RunOnce", + }; + format!( + "{hive}\\{} ({startup_kind}, {view})", + terminal_text(&entry.key_path) + ) +} + +fn service_start(start_type: u32, delayed_auto_start: bool) -> DesktopValue { + let message_id = match start_type { + 0 => DesktopMessageId::ServiceStartBoot, + 1 => DesktopMessageId::ServiceStartSystem, + 2 if delayed_auto_start => DesktopMessageId::ServiceStartAutomaticDelayed, + 2 => DesktopMessageId::ServiceStartAutomatic, + 3 => DesktopMessageId::ServiceStartManual, + 4 => DesktopMessageId::ServiceStartDisabled, + _ => DesktopMessageId::ServiceStartUnknown, + }; + DesktopValue::Message { message_id } +} + +fn service_error_control(error_control: u32) -> DesktopValue { + let message_id = match error_control { + 0 => DesktopMessageId::ServiceErrorControlIgnore, + 1 => DesktopMessageId::ServiceErrorControlNormal, + 2 => DesktopMessageId::ServiceErrorControlSevere, + 3 => DesktopMessageId::ServiceErrorControlCritical, + _ => DesktopMessageId::ServiceErrorControlUnknown, + }; + DesktopValue::Message { message_id } +} + +fn evidence(value: &str) -> DesktopValue { + DesktopValue::Evidence { + value: terminal_text(value), + } +} + +fn optional_evidence(value: Option<&str>) -> DesktopValue { + value.map(evidence).unwrap_or(DesktopValue::Message { + message_id: DesktopMessageId::ValueNotSet, + }) +} + +fn evidence_list(values: &[String]) -> DesktopValue { + if values.is_empty() { + DesktopValue::Message { + message_id: DesktopMessageId::ValueNone, + } + } else { + DesktopValue::EvidenceList { + values: values.iter().map(|value| terminal_text(value)).collect(), + } + } +} + +fn number(value: u32) -> DesktopValue { + DesktopValue::Number { + value: u64::from(value), + } +} + +fn optional_number(value: Option) -> DesktopValue { + value.map(number).unwrap_or(DesktopValue::Message { + message_id: DesktopMessageId::ValueNotSet, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use systemdiff_core::{ArtifactKey, RegistryRawEvidence, RunOncePrefixSemantics}; + use systemdiff_diff::{DiffWarningCode, InconclusiveReason}; + + const HASH: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn empty_diff() -> DiffDocument { + DiffDocument { + document_type: "systemdiff.diff".to_owned(), + schema_version: 1, + before_captured_at: "2026-08-20T00:00:00Z".to_owned(), + after_captured_at: "2026-08-20T00:05:00Z".to_owned(), + changes: Vec::new(), + warnings: Vec::new(), + } + } + + fn key(collector_id: &str, scope_id: &str, artifact_kind: &str) -> ArtifactKey { + ArtifactKey { + collector_id: collector_id.to_owned(), + scope_id: scope_id.to_owned(), + artifact_kind: artifact_kind.to_owned(), + canonical_id: "must-not-leak".to_owned(), + } + } + + fn registry(name: RegistryValueName, command: &str) -> RegistryStartupEntry { + RegistryStartupEntry { + hive: RegistryHive::CurrentUser, + registry_view: RegistryView::Shared, + key_path: r"Software\Microsoft\Windows\CurrentVersion\Run".to_owned(), + value_name: name, + startup_kind: RegistryStartupKind::Run, + run_once_prefix: None, + value_type: 1, + content_sha256: HASH.to_owned(), + decoding: RegistryValueDecoding::Decoded { + value: RegistryDecodedValue::String { + value: command.to_owned(), + }, + }, + raw_evidence: Some(RegistryRawEvidence { + content_hex: "00".to_owned(), + captured_byte_count: 1, + original_byte_count: 1, + truncated: false, + }), + } + } + + fn service(binary_path: &str, description: Option<&str>) -> WindowsService { + WindowsService { + service_name: "SystemDiffTest".to_owned(), + display_name: Some("SystemDiff Test Service".to_owned()), + service_type: 0x10, + start_type: 3, + error_control: 1, + binary_path: binary_path.to_owned(), + account: Some("LocalSystem".to_owned()), + dependencies: Vec::new(), + load_order_group: None, + tag_id: None, + delayed_auto_start: false, + description: description.map(str::to_owned), + } + } + + #[test] + fn registry_added_is_classified_and_raw_evidence_is_not_exposed() { + let mut diff = empty_diff(); + diff.changes.push(ArtifactChange { + change_id: "change:v1:00000000".to_owned(), + key: key( + "windows.registry.startup", + "current_user.shared.run", + "registry_startup", + ), + change: ChangeKind::Added { + after: Artifact::RegistryStartup(registry( + RegistryValueName::decoded("SystemDiffDogfood"), + r"C:\Program Files\SystemDiff\dogfood.exe", + )), + }, + }); + + let presentation = build_desktop_presentation(&diff); + + assert_eq!(presentation.contract_version, 1); + assert_eq!(presentation.summary.confirmed_change_count, 1); + assert_eq!(presentation.summary.inconclusive_change_count, 0); + assert_eq!(presentation.groups.len(), 2); + assert_eq!( + presentation.groups[0].items[0].change, + DesktopChangeKind::Added + ); + assert_eq!( + presentation.groups[0].items[0].message_id, + DesktopMessageId::RegistryStartupAdded + ); + let json = serde_json::to_string(&presentation).expect("presentation must serialize"); + assert!(json.contains("SystemDiffDogfood")); + assert!(!json.contains(HASH)); + assert!(!json.contains("must-not-leak")); + assert!(!json.contains("content_hex")); + } + + #[test] + fn service_modified_contains_only_changed_fields() { + let mut diff = empty_diff(); + diff.changes.push(ArtifactChange { + change_id: "change:v1:00000000".to_owned(), + key: key("windows.services", "current_token.win32", "windows_service"), + change: ChangeKind::Modified { + before: Artifact::WindowsService(service("old.exe", Some("before"))), + after: Artifact::WindowsService(service("new.exe", Some("after"))), + }, + }); + + let presentation = build_desktop_presentation(&diff); + let item = &presentation.groups[1].items[0]; + let field_ids: Vec<_> = item.fields.iter().map(|field| field.field_id).collect(); + + assert_eq!(item.change, DesktopChangeKind::Modified); + assert_eq!( + field_ids, + [ + DesktopFieldId::ServiceBinaryPath, + DesktopFieldId::ServiceDescription + ] + ); + assert!( + item.fields + .iter() + .all(|field| matches!(field.content, DesktopFieldContent::Changed { .. })) + ); + } + + #[test] + fn one_sided_partial_service_change_is_inconclusive_with_coverage_notice() { + let mut diff = empty_diff(); + diff.changes.push(ArtifactChange { + change_id: "change:v1:00000000".to_owned(), + key: key("windows.services", "current_token.win32", "windows_service"), + change: ChangeKind::Inconclusive { + before: Some(Artifact::WindowsService(service("service.exe", None))), + after: None, + reason: InconclusiveReason::CoverageIncomplete, + }, + }); + diff.warnings.push(DiffWarning { + code: DiffWarningCode::CoverageIncomplete, + collector_id: "windows.services".to_owned(), + scope_id: "current_token.win32".to_owned(), + before_status: Some(CollectorStatus::Complete), + after_status: Some(CollectorStatus::Partial), + }); + + let presentation = build_desktop_presentation(&diff); + + assert_eq!(presentation.summary.confirmed_change_count, 0); + assert_eq!(presentation.summary.inconclusive_change_count, 1); + assert_eq!( + presentation.groups[1].items[0].change, + DesktopChangeKind::Inconclusive + ); + assert_eq!( + presentation.coverage_notices, + [DesktopCoverageNotice { + group_id: Some(DesktopGroupId::WindowsServices), + message_id: DesktopMessageId::CoverageIncompleteAfter, + scope_message_id: DesktopMessageId::CoverageScopeWindowsServicesCurrentToken, + before_status: DesktopCoverageStatus::Complete, + after_status: DesktopCoverageStatus::Partial, + }] + ); + } + + #[test] + fn empty_diff_keeps_both_stable_groups_and_raw_utc_timestamps() { + let presentation = build_desktop_presentation(&empty_diff()); + + assert_eq!(presentation.started_at_utc, "2026-08-20T00:00:00Z"); + assert_eq!(presentation.finished_at_utc, "2026-08-20T00:05:00Z"); + assert_eq!( + presentation + .groups + .iter() + .map(|group| group.group_id) + .collect::>(), + [DesktopGroupId::Startup, DesktopGroupId::WindowsServices] + ); + assert!( + presentation + .groups + .iter() + .all(|group| group.items.is_empty()) + ); + assert_eq!( + serde_json::to_value(&presentation).expect("presentation must serialize"), + serde_json::json!({ + "contract_version": 1, + "started_at_utc": "2026-08-20T00:00:00Z", + "finished_at_utc": "2026-08-20T00:05:00Z", + "summary": { + "confirmed_change_count": 0, + "inconclusive_change_count": 0 + }, + "groups": [ + { + "group_id": "startup", + "heading_message_id": "group.startup", + "empty_message_id": "group.startup.empty", + "items": [] + }, + { + "group_id": "windows_services", + "heading_message_id": "group.windows_services", + "empty_message_id": "group.windows_services.empty", + "items": [] + } + ], + "coverage_notices": [] + }) + ); + } + + #[test] + fn hostile_or_undecodable_registry_evidence_uses_safe_fallbacks() { + let mut diff = empty_diff(); + let mut entry = registry( + RegistryValueName::InvalidUtf16 { + utf16le_hex: "00d8".to_owned(), + }, + "safe\u{202e}evil\nnext", + ); + entry.startup_kind = RegistryStartupKind::RunOnce; + entry.key_path = r"Software\Microsoft\Windows\CurrentVersion\RunOnce".to_owned(); + entry.run_once_prefix = Some(RunOncePrefixSemantics::NoDocumentedPrefix); + diff.changes.push(ArtifactChange { + change_id: "change:v1:00000000".to_owned(), + key: key( + "windows.registry.startup", + "current_user.shared.run_once", + "registry_startup", + ), + change: ChangeKind::Added { + after: Artifact::RegistryStartup(entry), + }, + }); + + let presentation = build_desktop_presentation(&diff); + let item = &presentation.groups[0].items[0]; + + assert_eq!( + item.headline, + DesktopValue::Message { + message_id: DesktopMessageId::RegistryUndecodableValueName + } + ); + assert!(matches!( + &item.fields[1].content, + DesktopFieldContent::Current { + value: DesktopValue::Evidence { value } + } if value == r"safe\u{202e}evil\nnext" + )); + } + + #[test] + fn serialization_contract_uses_stable_semantic_and_field_ids() { + let mut diff = empty_diff(); + diff.changes.push(ArtifactChange { + change_id: "change:v1:00000000".to_owned(), + key: key("windows.services", "current_token.win32", "windows_service"), + change: ChangeKind::Added { + after: Artifact::WindowsService(service("service.exe", None)), + }, + }); + + let json = serde_json::to_value(build_desktop_presentation(&diff)) + .expect("presentation must serialize"); + + assert_eq!(json["contract_version"], 1); + assert_eq!(json["groups"][0]["group_id"], "startup"); + assert_eq!(json["groups"][1]["group_id"], "windows_services"); + assert_eq!( + json["groups"][1]["items"][0]["message_id"], + "change.windows_service.added" + ); + assert_eq!( + json["groups"][1]["items"][0]["fields"][0]["field_id"], + "service.name" + ); + assert_eq!( + json["groups"][1]["items"][0]["fields"][0]["mode"], + "current" + ); + } +} diff --git a/crates/systemdiff-windows/src/registry.rs b/crates/systemdiff-windows/src/registry.rs index 4cb890c..fc8d2b3 100644 --- a/crates/systemdiff-windows/src/registry.rs +++ b/crates/systemdiff-windows/src/registry.rs @@ -720,7 +720,9 @@ fn utf16le_units(data: &[u8]) -> Option> { return None; } Some( - data.chunks_exact(2) + data.as_chunks::<2>() + .0 + .iter() .map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]])) .collect(), ) diff --git a/docs/adr/0003-desktop-stack.md b/docs/adr/0003-desktop-stack.md index 0747c60..c17674e 100644 --- a/docs/adr/0003-desktop-stack.md +++ b/docs/adr/0003-desktop-stack.md @@ -1,7 +1,8 @@ # ADR 0003: Tauri 2 with React and TypeScript for the desktop client -- Status: Proposed +- Status: Accepted - Date: 2026-08-11 +- Accepted: 2026-08-20 ## Context @@ -9,9 +10,9 @@ SystemDiff needs a polished, localized evidence-exploration UI after the CLI pip Tauri 2 uses a Rust core process and the operating-system WebView. On Windows it requires Microsoft C++ Build Tools and WebView2. React/TypeScript offers a broad contributor pool and is suitable for interactive diff inspection, but it also introduces a fast-moving Node dependency graph. -## Proposed decision +## Decision -After v0.1 CLI functionality, run a narrow spike using Tauri 2, React, TypeScript, and Vite. Accept the stack only if the spike verifies: +Use Tauri 2 with React, TypeScript, and Vite for the first Windows desktop vertical slice. The completed narrow spike found no architectural blocker and established these required boundaries: - typed, least-privilege IPC with no generic shell/filesystem/network capability; - bundled local content and a restrictive content security policy; @@ -21,14 +22,25 @@ After v0.1 CLI functionality, run a narrow spike using Tauri 2, React, TypeScrip - dependency license/security audit and maintainable lockfiles; - testable UI behavior without elevated privileges. -## Alternatives to compare +Rust owns capture, Diff classification, coverage semantics, and a locale-neutral typed presentation model. React owns layout, interaction, local-time display, and `en-US` / `zh-CN` localization. The frontend does not receive Snapshot documents or reinterpret `Added`, `Removed`, `Modified`, or `Inconclusive` evidence. + +The Tauri command surface is limited to the guided session lifecycle and takes no filesystem path, URL, shell command, or raw Snapshot JSON. Production uses bundled local assets, an explicit content security policy, one application window, and no shell, filesystem, HTTP, dialog, opener, updater, or network plugin. Synchronous Windows collection runs on a blocking worker so the WebView remains responsive. + +The desktop Rust package has its own lockfile outside the root Cargo workspace. This keeps Tauri's platform dependency graph out of the existing CLI-only Developer Preview package and license verification. It still reuses SystemDiff crates through path dependencies and receives explicit CI validation. + +Microsoft documents WebView2 support on Windows 10 SAC 1709 and later, but Tauri only describes it as normally preinstalled on Windows 10 1803 and later. The development app therefore requires an installed WebView2 Evergreen Runtime and does not establish a clean-machine 1709 distribution claim. Runtime bootstrap, installer behavior, and the missing-Runtime experience remain release work. + +## Alternatives considered - Tauri with a smaller frontend library or no framework. - Native Rust UI libraries, considering accessibility and ecosystem maturity. - .NET desktop UI calling a Rust library, considering packaging and interop cost. -## Consequences if accepted +## Consequences - The WebView becomes an untrusted UI boundary; only the Rust core process accesses the OS. - Frontend upgrades and supply-chain review become release work. -- Tauri and Node tooling remain absent from v0.1 CI and dependencies until the decision is accepted. +- Tauri and Node tooling are now part of desktop development and CI, with separate Cargo/npm lockfiles and dependency review. +- Windows desktop distribution depends on an appropriate WebView2 Runtime strategy. A no-bundle development executable is not a fully self-contained portable package. +- The desktop IPC presentation contract can evolve with the jointly shipped frontend/backend, but it does not replace or silently change the public Snapshot/Diff schema. +- A small React runtime is accepted for localization, interaction, and contributor accessibility; router, global state, design-system, and general i18n frameworks are not justified by the first three-state workflow. diff --git a/docs/architecture.md b/docs/architecture.md index 03ad8cd..c22a779 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -20,10 +20,11 @@ flowchart LR REPORT --> CLI["systemdiff-cli"] CORE --> CLI WIN --> CLI - GUI["Future Tauri desktop"] --> CORE - GUI --> DIFF - GUI --> RISK - GUI --> REPORT + UI["React / TypeScript"] --> SESSION["Tauri desktop session"] + SESSION --> WIN + SESSION --> DIFF + DIFF --> REPORT + REPORT --> SESSION ``` Dependencies point inward toward stable evidence types. The core does not know about Win32, Tauri, React, terminal styling, or files on disk. @@ -36,9 +37,9 @@ Dependencies point inward toward stable evidence types. The core does not know a | `systemdiff-windows` | Win32/COM adapters, Windows normalization, collector implementations | Diff judgments, localized explanations, remediation | | `systemdiff-diff` | Pure deterministic comparison and compatibility/coverage semantics | OS access, file I/O, risk classification | | `systemdiff-risk` | Rule contract and findings that reference changes/evidence | Evidence mutation, GUI rendering, OS access | -| `systemdiff-report` | JSON and human-readable rendering to an output stream | Collection, platform calls, opening arbitrary files | +| `systemdiff-report` | JSON and human-readable rendering plus a locale-neutral desktop presentation DTO | Collection, platform calls, opening arbitrary files | | `systemdiff-cli` | Arguments, file I/O, exit codes, composition | Domain or collector business logic | -| `apps/desktop` | Future localized interaction and evidence inspection | A second core, direct Win32 calls, hidden evidence | +| `apps/desktop` | Ephemeral guided capture session, narrow Tauri IPC, localized interaction, and evidence inspection | A second core, frontend paths/JSON, direct Win32 logic, evidence classification | These are source boundaries, not a commitment to a dynamic plugin ABI. Out-of-process collectors, a general event bus, an async runtime, and a dependency-injection framework are intentionally absent. @@ -109,6 +110,14 @@ Reports support three deliberately separate views over the same typed evidence: The human renderer consumes a `DiffDocument`. The technical renderer also receives the two already validated source Snapshots because Collector versions and scoped diagnostics are Snapshot evidence and are intentionally not duplicated into Diff v1. The CLI only selects a renderer; presentation stays in `systemdiff-report`. Terminal renderers escape control characters from untrusted observed strings and do not rely on color or ANSI formatting. Renderers do not rescan the system, execute evidence, or open arbitrary files. +The desktop uses a separate locale-neutral presentation contract from the same crate. Rust maps typed Registry/service evidence into fixed `startup` and `windows_services` groups, determines change kinds and changed service fields, counts confirmed versus inconclusive changes, and emits stable message/field identifiers. The WebView translates those identifiers and lays out already-classified values; it never receives a Snapshot or Diff document to reinterpret. Exact technical text is generated in Rust from the validated before/after Snapshots and retained in memory for on-demand disclosure. + +### Desktop session + +The desktop session is an application orchestration boundary, not a new evidence schema. One backend-authoritative state machine moves through Ready, Starting, Capturing, Finishing, and Results. Synchronous Win32 collection runs in Tauri's blocking executor, while Rust transitions state before dispatch so duplicate frontend calls cannot start concurrent captures. + +Before/after Snapshot documents are ephemeral implementation evidence. They are written with the same 64 MiB ceiling under a backend-selected application-local root, use collision-safe create-new files, and are removed after compare, cancel, or a stable normal exit. A process-lifetime advisory lock prevents a second app instance from recovering an active session. Startup recovery considers only direct child directories with the exact SystemDiff session name, ownership marker, allowlisted entries, path containment, and non-reparse-point checks; it never recursively removes an arbitrary path. In-flight native work cannot be interrupted safely, so exit during that state defers exact cleanup to recovery rather than claiming success. Session paths are never sent to or accepted from React. Results retain only the presentation DTO and technical text in memory; there is no history database. + ## Windows collection strategy Windows collection uses Unicode platform APIs through narrowly feature-gated `windows-rs` bindings: @@ -123,7 +132,7 @@ Windows code is the only production area expected to require `unsafe`. Every uns ## Privilege and process boundaries -The default process uses the current user token. Elevation is not an all-or-nothing mode: each collector/scope reports what it could observe. A future GUI must make elevation explicit and must not expose generic shell, filesystem, or process-execution commands to the WebView. +The default process uses the current user token. Elevation is not an all-or-nothing mode: each collector/scope reports what it could observe. The desktop uses `asInvoker`, never requests elevation, and exposes no generic shell, filesystem, network, or process-execution command to the WebView. The Tauri core process is privileged relative to its WebView. IPC commands must be narrow, typed, and least-privilege. The frontend receives only the evidence necessary for the active view; secrets or platform handles never live in frontend state. @@ -135,7 +144,7 @@ The MVP avoids collecting hostname or stable machine identifiers because the fir ## Localization -Core and JSON fields are language-neutral. Findings carry explanation keys and structured parameters. The future desktop UI loads locale resources for `en-US` and `zh-CN`; business logic contains no hard-coded end-user prose. +Core and JSON fields are language-neutral. Findings carry explanation keys and structured parameters. The desktop presentation contract also uses stable language-neutral message and field identifiers. React loads typed `en-US` and `zh-CN` dictionaries, selects Chinese for a `zh*` browser/Windows language, formats ordinary timestamps in the user's locale, and leaves observed Registry/service evidence untranslated. Raw UTC remains available in technical details. ## Dependency policy @@ -150,8 +159,8 @@ Important choices were checked on 2026-08-11. Re-evaluate them when introduced o | `windows-rs` | Microsoft-maintained and active; Apache-2.0 OR MIT; [official repository](https://github.com/microsoft/windows-rs) | Accepted in `systemdiff-windows`; only required Win32 feature families are enabled and unsafe calls remain behind narrow adapters | | `windows-version` | Active; Apache-2.0 OR MIT; [official repository](https://github.com/microsoft/windows-rs) | Accepted for the documented Windows 10/Server version 1709 minimum check without shelling out or parsing localized output | | `clap` | Active; Apache-2.0 OR MIT; [official repository](https://github.com/clap-rs/clap) | Accepted at the CLI boundary only | -| Tauri 2 | Active; Apache-2.0 OR MIT; requires C++ Build Tools and WebView2 on Windows; [prerequisites](https://v2.tauri.app/start/prerequisites/) | Proposed for v0.2; excluded from v0.1 build until a security-focused spike | -| React / TypeScript / Vite | Active; permissive licenses; frontend support windows require regular upgrades | Proposed with Tauri; lockfile and dependency audit required when introduced | +| Tauri 2.11 | Active; Apache-2.0 OR MIT; requires C++ Build Tools and WebView2 on Windows; [prerequisites](https://v2.tauri.app/start/prerequisites/) | Accepted with bundled local content, explicit CSP/capabilities, no privileged plugins, and a separate desktop Cargo lockfile | +| React 19 / TypeScript 6 / Vite 8 | Active; permissive licenses; frontend support windows require regular upgrades | Accepted for the small localized desktop view with exact npm locking; no router, global state, design-system, or general i18n framework | Do not introduce Tokio/`async-trait`, a database, network client, telemetry SDK, generic shell plugin, or schema generator at runtime without a demonstrated requirement. A build-time JSON Schema generator may be evaluated before v0.1. @@ -164,7 +173,11 @@ Do not introduce Tokio/`async-trait`, a database, network client, telemetry SDK, - Windows adapter tests isolate buffer parsing, normalization, and API error mapping. - Default CI never writes real Run keys, services, or tasks and never requires elevation. - The write-capable HKCU Registry E2E is a separate test-only PowerShell harness with two explicit gates, exact-data guarded cleanup, no administrator requirement, and no link into production CLI/API code. +- Desktop CI separately runs locked npm install, TypeScript checking, ESLint, Vite production build, desktop Rust fmt/Clippy/tests on Windows and Ubuntu, and a Windows Tauri no-bundle build. It does not publish a desktop release artifact. +- Desktop Rust tests use injected synthetic Snapshots for state, failure, cleanup, presentation, and coverage behavior. The guarded real desktop dogfood mutation remains opt-in and outside default CI. ## Desktop decision -Tauri's process model keeps Rust core/IPC separate from the system WebView and is materially lighter than duplicating the core in a web service. Its security still depends on narrow capabilities and safe Rust commands. The desktop choice remains `Proposed` until the CLI pipeline works and a spike verifies IPC, localization, packaging, WebView2 behavior, and dependency audit results. +Tauri's process model keeps Rust core/IPC separate from the system WebView and is materially lighter than duplicating the core in a web service. ADR 0003 is accepted after the focused spike verified the in-process Rust boundary, local-only content, narrow command/capability model, bilingual DTO, and current Windows development prerequisites. + +Microsoft documents WebView2 support on Windows 10 SAC 1709 and later, but the Runtime is normally preinstalled only on Windows 10 1803 and later. The development application therefore requires an installed Evergreen Runtime. A Tauri `--no-bundle` executable is not claimed as a clean-machine portable release; missing-Runtime handling, installer/bootstrap choice, signing, and full baseline validation remain release work. diff --git a/docs/assets/systemdiff-desktop-results.jpg b/docs/assets/systemdiff-desktop-results.jpg new file mode 100644 index 0000000..2cf88b3 Binary files /dev/null and b/docs/assets/systemdiff-desktop-results.jpg differ diff --git a/docs/roadmap.md b/docs/roadmap.md index 80f4394..0857596 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -10,7 +10,7 @@ The roadmap is outcome-oriented. Dates are intentionally omitted until the proje Exit condition: the repository is understandable, reviewable, and ready for the first collector issue without pretending the product already works. -Status: complete. The first Collector phase is now underway. +Status: complete. The first two Collector vertical slices and a guided desktop development slice now build on this foundation. ## v0.1: CLI evidence pipeline @@ -33,16 +33,18 @@ Required scope: - Independent collector failures and clear privilege/coverage reporting. - Deterministic fixtures and snapshot-to-diff integration tests that do not need administrator privileges. -Explicitly excluded: whole-drive crawling, remediation, telemetry, cloud analysis, broad persistence coverage, and a large GUI. +Explicitly excluded: whole-drive crawling, remediation, telemetry, cloud analysis, broad persistence coverage, and a large multi-page GUI. The first small guided desktop workflow is being validated in parallel without changing the CLI evidence milestone. ## v0.2: polished desktop workflow -- Validate and adopt the Tauri 2 + React + TypeScript stack after a narrow security-focused spike. -- Guide users through snapshot A → change → snapshot B → explanation. -- Show plain-language summaries without hiding raw evidence. -- Ship maintained `en-US` and `zh-CN` locales. +- Tauri 2 + React + TypeScript was accepted after a narrow security-focused spike. **Implemented for the first development vertical slice; distribution validation continues.** +- Guide users through start capture → make a change → finish and compare without file handling or Snapshot terminology. **Implemented for the first development vertical slice.** +- Show plain-language Startup and Windows Services summaries without hiding raw technical evidence. **Implemented for the first development vertical slice.** +- Ship maintained `en-US` and `zh-CN` locales. **Implemented in the first desktop UI; localization review remains ongoing.** - Establish Windows packaging and code-signing strategy before encouraging ordinary-user installation. +The first slice intentionally has no history, baseline management, settings center, dashboard, risk score, updater, or desktop Release artifact. It proves the product workflow; it is not yet the polished v0.2 distribution milestone. + ## v0.3: safe sharing and richer explanations - Implement tested redaction/sanitization with explicit policy metadata. diff --git a/docs/threat-model.md b/docs/threat-model.md index e276523..4a7698f 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -2,16 +2,16 @@ ## Executive summary -SystemDiff is a local, read-only Windows CLI with a future desktop UI. Its highest risks are misleading evidence caused by incomplete or attacker-controlled collection data, disclosure of privacy-sensitive reports, memory/logic errors at Win32/COM and JSON parsing boundaries, and accidental expansion from observation into privileged execution or remediation. The MVP has no network service, account, telemetry, or cloud dependency, which materially reduces remote exposure. +SystemDiff is local, read-only Windows auditing software with a CLI and a guided Tauri desktop development UI. Its highest risks are misleading evidence caused by incomplete or attacker-controlled collection data, disclosure of privacy-sensitive reports or ephemeral desktop sessions, memory/logic errors at Win32/COM and JSON parsing boundaries, and accidental expansion from observation into privileged execution or remediation. The MVP has no network service, account, telemetry, or cloud dependency, which materially reduces remote exposure. ## Scope and assumptions In scope: - runtime boundaries described in `docs/architecture.md`; -- snapshot/diff parsing, collection, rules, reporting, and future Tauri IPC; +- snapshot/diff parsing, collection, rules, reporting, and current Tauri IPC/session storage; - build/CI dependency integrity; -- repository paths `crates/`, future `apps/desktop/`, `fixtures/`, `.github/workflows/`, and `rules/` when created. +- repository paths `crates/`, `apps/desktop/`, `fixtures/`, `.github/workflows/`, and `rules/` when created. Out of scope: @@ -26,7 +26,7 @@ Assumptions: - software changed between snapshots may be untrusted and may race or deceive enumeration; - snapshot/diff files may come from another machine and are untrusted input; - reports may be shared publicly despite warnings; -- the future desktop WebView is less trusted than the Rust core process. +- the desktop WebView is less trusted than the Rust core process. The production CLI and Collector API remain read-only. A separate developer E2E harness can write one synthetic current-user Run value only after an environment-variable gate and explicit switch; it refuses an existing value and verifies exact type/data before cleanup. Default CI does not run it, and no Registry write binding is reachable from production Rust code. @@ -34,7 +34,7 @@ Open questions that may change rankings: - installer/update mechanism and any future expansion beyond the current Windows 10/Server version 1709 minimum; - whether snapshots will ever be cryptographically signed or compared across machines; -- final Tauri command/capability set; +- formal desktop installer, WebView2 Runtime bootstrap, and clean-machine baseline behavior; - public security contact and release-signing process. ## System model @@ -47,7 +47,7 @@ Open questions that may change rankings: - `systemdiff-diff`: pure comparison with coverage semantics. - `systemdiff-risk`: evidence-referencing enrichment. - `systemdiff-report`: JSON and terminal serialization. -- future Tauri core/WebView: local IPC and presentation boundary. +- Tauri core/WebView: local session IPC and presentation boundary. - GitHub Actions/dependencies: build and artifact integrity boundary. ### Data flows and trust boundaries @@ -56,7 +56,8 @@ Open questions that may change rankings: - Snapshot files -> CLI/core parser: attacker-controlled local JSON via file I/O; a 64 MiB bounded read and header-first schema route precede full Snapshot construction, while typed validation and identity uniqueness protect later processing. Finer object, string, nesting, and count limits remain future hardening. - Core evidence -> diff/rules: typed in-process values; compatibility, coverage, deterministic identity, and no evidence execution are the guarantees. - Diff/findings -> report files/terminal: privacy-sensitive local output; destination choice is user-controlled, and future sanitization must be explicit. -- Future WebView -> Tauri core: typed local IPC; bundled origin, allowlisted commands, least-privilege capabilities, and input validation are required. +- WebView -> Tauri core: typed local IPC; bundled origin, explicit CSP, one-window capability, and five no-path session commands constrain the boundary. The WebView receives a locale-neutral presentation DTO or on-demand technical text, never Snapshot paths/JSON. +- Desktop session -> app-local storage: unredacted before/after evidence is create-new and bounded under an application-owned directory. A process advisory lock, exact ownership marker/allowlist, path containment, non-reparse checks, and non-recursive cleanup limit crash recovery and concurrent-instance risk. - Repository/dependencies -> CI artifacts: developer-controlled source plus third-party actions/crates; read-only token permissions, immutable action references, the committed Cargo lockfile, review, exact package allowlists, checksums, and artifact-download verification reduce risk. #### Diagram @@ -69,8 +70,9 @@ flowchart LR CORE["Evidence core"] DIFF["Diff and rules"] REPORT["Reports"] - UI["Future WebView"] - TAURI["Future Tauri core"] + UI["Desktop WebView"] + TAURI["Tauri session core"] + TEMP["Ephemeral app-local evidence"] end FILES["Untrusted snapshot files"] --> CORE OS --> WIN @@ -79,6 +81,7 @@ flowchart LR DIFF --> REPORT UI --> TAURI TAURI --> CORE + TAURI --> TEMP REPORT --> SHARE["User sharing boundary"] SOURCE["Source and dependencies"] --> CI["GitHub Actions"] CI --> BIN["Release artifacts"] @@ -119,7 +122,8 @@ flowchart LR | Registry/service/task APIs | Local collection | Windows -> native adapter | ACLs, races, malformed buffers/strings | `docs/collectors.md` | | Report destination | CLI output path/stdout | Process -> filesystem/user | Sensitive output and overwrite behavior | `docs/product-principles.md` | | Rule inputs | Parsed changes | Evidence -> judgment | Rules must reference, not rewrite, evidence | `docs/architecture.md` | -| Future Tauri IPC | WebView commands | Web content -> native core | Narrow commands/capabilities only | `docs/adr/0003-desktop-stack.md` | +| Tauri session IPC | WebView commands | Web content -> native core | Bundled local content; fixed no-argument session commands; no shell/fs/http/network plugin | `docs/adr/0003-desktop-stack.md` | +| Desktop temporary evidence | Tauri backend only | Native core -> app-local filesystem | Unredacted, bounded, create-new, exact cleanup/recovery; no frontend paths | `apps/desktop/README.md` | | CI dependencies/actions | Pull requests and dependency updates | External supply chain -> build | Fork PRs cannot enter the upstream-push artifact upload path; no secrets; immutable pins | `.github/workflows/ci.yml` | ## Top abuse paths @@ -128,7 +132,7 @@ flowchart LR 2. A user attaches an unredacted task/snapshot file to a public issue -> paths, identities, or embedded secrets become public -> privacy loss persists in repository history. 3. A crafted JSON document contains excessive objects/strings or duplicate identities -> parser/index consumes resources or overwrites evidence -> denial of service or misleading diff. 4. Malformed native buffer/string data crosses unsafe FFI conversion -> unchecked length/encoding causes memory or logic failure -> collection crashes or evidence is corrupted. -5. A future WebView compromise invokes an over-broad native command -> evidence path is treated as executable or a generic plugin writes to the system -> privilege impact exceeds read-only scope. +5. A WebView compromise invokes an over-broad native command -> evidence path is treated as executable or a generic plugin writes to the system -> privilege impact exceeds read-only scope. 6. A rule parses an observed command and treats a heuristic as fact -> explanation drops uncertainty/raw context -> ordinary users are frightened or advanced users are misled. 7. A compromised CI action/dependency modifies a build -> unsigned/unverified artifact is distributed -> users run attacker code under a trusted project name. @@ -140,7 +144,8 @@ flowchart LR | TM-002 | Snapshot provider | Can supply arbitrary local JSON | Exhaust memory/CPU or exploit parser assumptions | CLI denial of service; misleading evidence | Availability and diff integrity | 64 MiB bounded file read, header-first version routing, typed schema, and duplicate rejection (`docs/data-format.md`) | No object-count, string-size, nesting, or parser-fuzz limits yet | Add finer structural limits only from measured need; fuzz parsers later | Structured parse failure codes; resource-limit tests | Medium | Medium | Medium | | TM-003 | Windows data/API edge case | Collector handles raw buffers/COM values | Trigger unsafe length, lifetime, or encoding bug | Crash, memory corruption, corrupted evidence | Token, evidence, availability | Unsafe is isolated to narrow `windows-rs` Registry/SCM adapters with checked units, ranges, pointers, initialized aligned buffers, bounded growth, RAII handles, and strict pure normalization (`docs/architecture.md`) | No fuzzing; Tasks COM adapter not implemented | Keep unsafe blocks minimal and independently reviewed; fuzz pure decoders; apply equivalent discipline to future COM work | Windows regression fixtures and sanitizer/fuzz jobs where practical | Medium | High | High | | TM-004 | User/workflow mistake | Real report is shared publicly | Publish sensitive raw evidence | Lasting privacy disclosure | Snapshot/report confidentiality | Sensitive-by-default and redaction metadata (`docs/product-principles.md`) | Sanitizer absent | Blocking share warning in UI, documented manual review, policy-versioned pure sanitizer, synthetic issue fixtures | Scan project issues for accidental reports; sanitizer golden tests | High | High | High | -| TM-005 | Future compromised WebView | Desktop exposes broad command/capability | Invoke native execution/write or read excess data | Privilege misuse and boundary violation | Token, host integrity, evidence confidentiality | Proposed narrow IPC (`docs/adr/0003-desktop-stack.md`) | Desktop not yet threat-tested | Bundled content, restrictive CSP, explicit commands, no generic shell/fs/http plugins, command authorization tests | Log command IDs without sensitive payloads; capability review in CI | Low pre-v0.2 | High | Medium | +| TM-005 | Compromised desktop WebView | Desktop exposes broad command/capability | Invoke native execution/write or read excess data | Privilege misuse and boundary violation | Token, host integrity, evidence confidentiality | Bundled local content, explicit CSP, main-window capability, fixed no-argument session commands, and no shell/fs/http/network plugin (`docs/adr/0003-desktop-stack.md`) | A future feature/plugin could widen the surface; WebView2 is a platform dependency | Review every new command/capability; keep OS paths and Snapshot JSON backend-only; test command/state authorization | Dependency review and capability diff inspection in every desktop PR | Low | High | Medium | +| TM-009 | Crash, second instance, or local same-user process | Can access or race the desktop app-local directory | Leave, replace, or trick cleanup of unredacted ephemeral Snapshot evidence | Privacy disclosure or deletion of unrelated local data | Snapshot confidentiality and host filesystem integrity | 64 MiB bound, create-new files, process advisory lock, ownership marker, exact direct-child allowlist, canonical containment, reparse rejection, non-recursive deletion, and cleanup tests (`apps/desktop/src-tauri/src/storage.rs`) | Same-user local processes can still read ordinary app-local files; storage is not encryption | Add tested redaction before share/export; revisit stronger at-rest protection only from a concrete threat/user need | Startup recovery summary and deterministic storage failure tests | Low | High | Medium | | TM-006 | Rule author or malformed evidence | Rule sees ambiguous command/path data | Overstate heuristic or detach finding from evidence | Misleading/fearmongering output | Finding integrity and user trust | Evidence-before-judgment principle; findings reference changes (`docs/architecture.md`) | No real rule corpus/reviewer rubric | Stable reason IDs, calibrated classifications, explanation keys, counterexample fixtures, independent review | Golden finding snapshots and rule precision review | Medium | Medium | Medium | | TM-007 | Supply-chain attacker | Dependency/action update reaches CI | Execute during build or replace artifact | Malicious binaries under project identity | Build/release integrity | Minimal CI permissions, committed lockfile, immutable action pins, an upstream-push-only preview upload path, exact archive allowlists, SHA-256, PE/manifest inspection, and downloaded-artifact smoke verification (`.github/workflows/ci.yml`) | Preview artifacts are unsigned and expiring; no immutable Release or publisher identity exists | Review Dependabot changes, retain download-back verification, add Authenticode and provenance to a future immutable Release | Dependency review, checksum mismatch failures, artifact inventory, signing verification later | Low | High | Medium | | TM-008 | Feature contributor | Maintainer accepts scope expansion | Add remediation, evidence execution, credential access, or evasion | System modification or dual-use abuse | Host integrity and project trust | Explicit prohibited boundary (`AGENTS.md`, `docs/product-principles.md`) | Policy is review-enforced | Stop and require maintainer decision, ADR and new threat model; keep remediation separate if ever approved | PR checklist and independent security review | Medium | High | High | @@ -162,14 +167,14 @@ flowchart LR | `crates/systemdiff-risk/` | Converts evidence into calibrated judgment | TM-006, TM-008 | | `crates/systemdiff-report/` | Serializes privacy-sensitive documents | TM-002, TM-004 | | `crates/systemdiff-cli/` | Untrusted path/file input and output behavior | TM-002, TM-004 | -| `apps/desktop/` | Future WebView/native privilege boundary | TM-005 | +| `apps/desktop/` | WebView/native IPC and ephemeral evidence boundary | TM-004, TM-005, TM-009 | | `.github/workflows/` | Third-party build code and token permissions | TM-007 | | `AGENTS.md` | Durable safety and scope enforcement for AI-assisted changes | TM-008 | ## Quality check -- Covered current file, native API, report, rule, and CI entry points plus future desktop IPC. +- Covered current file, native API, report, rule, CI, desktop IPC, and temporary session entry points. - Represented every identified trust boundary in at least one threat. - Separated runtime behavior from CI/build tooling and future components. - Based deployment/data assumptions on the offline-first local CLI/desktop scope supplied by the maintainer. -- Marked unresolved release, support, contact, and desktop details explicitly. +- Marked unresolved release, support, contact, WebView2 distribution, and desktop packaging details explicitly. diff --git a/scripts/set-desktop-dogfood-registry-value.ps1 b/scripts/set-desktop-dogfood-registry-value.ps1 new file mode 100644 index 0000000..8275f43 --- /dev/null +++ b/scripts/set-desktop-dogfood-registry-value.ps1 @@ -0,0 +1,122 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateSet('Add', 'Remove')] + [string]$Action, + + [Parameter(Mandatory = $true)] + [switch]$ConfirmSyntheticRegistryTest +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($env:SYSTEMDIFF_RUN_DESKTOP_DOGFOOD -ne '1' -or -not $ConfirmSyntheticRegistryTest) { + throw 'Refusing to write test Registry evidence. Set SYSTEMDIFF_RUN_DESKTOP_DOGFOOD=1 and pass -ConfirmSyntheticRegistryTest.' +} + +# This mutation-only harness is deliberately separate from the production +# desktop executable. It never writes outside the current user's existing Run +# key and cleanup is allowed only when the exact known type and data still match. +$keyPath = 'Software\Microsoft\Windows\CurrentVersion\Run' +$valueName = 'SystemDiffDogfood' +$expectedData = 'cmd.exe /d /c exit 0' + +function Get-MatchingValueName { + param( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey]$Key, + [Parameter(Mandatory = $true)] + [string]$Name + ) + + foreach ($candidate in $Key.GetValueNames()) { + if ([StringComparer]::OrdinalIgnoreCase.Equals($candidate, $Name)) { + return $candidate + } + } + return $null +} + +function Assert-ExactSyntheticValue { + param( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey]$Key, + [Parameter(Mandatory = $true)] + [string]$Name + ) + + $actualName = Get-MatchingValueName -Key $Key -Name $Name + if ($null -eq $actualName) { + throw "Synthetic Registry value '$Name' is absent." + } + $actualKind = $Key.GetValueKind($actualName) + $actualData = $Key.GetValue( + $actualName, + $null, + [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames + ) + if ($actualKind -ne [Microsoft.Win32.RegistryValueKind]::String -or $actualData -cne $expectedData) { + throw "Refusing operation: '$actualName' does not have the exact known synthetic type and data." + } + return $actualName +} + +$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($keyPath, $true) +if ($null -eq $key) { + throw 'The existing HKCU Run key could not be opened; the harness will not create it.' +} + +try { + if ($Action -eq 'Add') { + if ($null -ne (Get-MatchingValueName -Key $key -Name $valueName)) { + throw "Refusing to overwrite existing Registry value '$valueName'." + } + + $writeAttempted = $false + try { + # Recheck immediately before the non-atomic Registry value write. + if ($null -ne (Get-MatchingValueName -Key $key -Name $valueName)) { + throw "Registry value '$valueName' appeared after preflight; refusing to overwrite it." + } + $writeAttempted = $true + $key.SetValue($valueName, $expectedData, [Microsoft.Win32.RegistryValueKind]::String) + $actualName = Assert-ExactSyntheticValue -Key $key -Name $valueName + Write-Output "Added exact synthetic HKCU Run value: $actualName" + Write-Output "Synthetic data: $expectedData" + } + catch { + if ($writeAttempted) { + $actualName = Get-MatchingValueName -Key $key -Name $valueName + if ($null -ne $actualName) { + $actualKind = $key.GetValueKind($actualName) + $actualData = $key.GetValue( + $actualName, + $null, + [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames + ) + if ($actualKind -eq [Microsoft.Win32.RegistryValueKind]::String -and $actualData -ceq $expectedData) { + $key.DeleteValue($actualName, $true) + } + } + } + throw + } + } + else { + $actualName = Get-MatchingValueName -Key $key -Name $valueName + if ($null -eq $actualName) { + Write-Output 'Cleanup: synthetic Registry value is already absent' + return + } + $exactName = Assert-ExactSyntheticValue -Key $key -Name $valueName + $key.DeleteValue($exactName, $true) + if ($null -ne (Get-MatchingValueName -Key $key -Name $valueName)) { + throw 'Cleanup failed: the synthetic Registry value still exists.' + } + Write-Output 'Cleanup: exact-data guarded deletion verified' + } +} +finally { + $key.Dispose() +}