Skip to content

Architecture ​

The repository uses ports and adapters without turning every small function into a separate layer.

text
SolidJS WebView -> narrow Tauri IPC -> Tauri app adapter -> core use cases
                                                       -> CommandRunner port
CLI presentation -----------------> CLI app adapter ---> CommandRunner port

Repository layout ​

PathResponsibility
packages/corePlatform-independent domain entities, DTOs, adapter traits, and application orchestration
packages/storageAtomic local snapshots shared by the desktop app and CLI
apps/tauri-appSolidJS WebView, Tauri IPC, and desktop process adapter
apps/cliCLI presentation and Tokio process adapter
apps/android-appNative Kotlin/Compose WebAuthn guidance with user-confirmed browser results
apps/webauthn-webStatic browser-local passkey registration and authentication test tool
docsEnglish and Chinese source documentation
docsiteVitePress documentation workspace
scripts and justfilesRepository automation, checks, and release tooling

Core ​

packages/core contains application DTOs, domain state, stable error codes, use-case orchestration, and adapter traits. It has no dependency on Tauri, Clap, or a concrete process implementation. CommandRequest holds a native executable path, an argument vector, a timeout, and an aggregate output limit. CommandOutput preserves stdout and stderr as bytes so non-UTF-8 device output is not silently corrupted.

Read-only ADB use cases construct command requests only from a validated ADB path, serials from the current device snapshot, parsed non-negative user IDs, the fixed Credential Provider service action, and a three-key read allowlist. The generic runner is an internal Rust port and is never an IPC or CLI user-input surface.

The application model distinguishes values from entities. ADB validation results, device properties, components, settings, findings, and errors are immutable values. Discoveries, ADB selections, device enumerations, diagnoses, previews, plans, and executions are session entities with non-interchangeable opaque IDs and explicit parent IDs. Snapshots are persistent entities with revisions. DiagnosisReport.completeness describes the quality of a report and is separate from the diagnosis entity's asynchronous lifecycle.

The backend session has a monotonic revision and a single latest diagnosis ID. Every asynchronous operation captures both its revision and parent IDs before doing work and verifies them again before publishing a result. A late diagnosis therefore cannot replace a newer device context. Session diagnoses are memory-only and retain only the latest diagnosis plus diagnoses referenced by an active Preview, Plan, or Execution.

The Core-owned change state machine and abstract snapshot store use schema v2. packages/storage is the shared local filesystem adapter used by GUI and CLI. Preview, Plan, and Execution entities all retain their source diagnosis ID. Plan creation atomically persists a planned snapshot before consuming the Preview. Execution persists executing before the first possible write, and cancellation, expiry, drift, apply, recovery, or restore produces a terminal state that cannot be replayed. Legacy v1 files remain on disk as unsupported warnings and are never used for restore.

Applications ​

The Tauri app implements CommandRunner with the Rust API of tauri-plugin-shell. The plugin's JavaScript binding is absent, and the WebView capability contains no shell permission. The CLI implements the same port with tokio::process::Command. Both adapters use argument arrays, bounded byte collection, timeouts, and child termination.

The frontend owns rendering, opaque candidate/device selection, confirmation, localization, theming, and accessible interaction. Stateful application services are class XxxService instances with constructor dependencies, private mutable state, and prototype methods. injection-js composes them with InjectionToken, explicit useFactory: (...deps) => new XxxService(...) providers, and dependency lists; it uses no decorators, ambient injection, or reflection metadata. createXxx() is reserved for stateless controllers and replaceable adapter or reusable library construction boundaries. Each domain service owns the events it publishes: a private RxJS DomainEvent<T> Subject exposes a read-only, exactly typed Observable only to downstream domain services. There is no shared event bus, global discriminated event union, or event-type filter. Rendering consumes Solid state or immutable snapshots and never subscribes to these events. Downstream services translate upstream events into auditable invalidated states without mutating another service's signal.

Owned subscriptions and session scopes implement TC39 Explicit Resource Management: synchronous resources expose idempotent [Symbol.dispose](), lexical owners use using, and dynamic aggregates use DisposableStack. A single cleanup stack is named disposableStack or with a role-specific singular name such as constructionStack; resource and resources refer to domain EntityResource state. Subject completion and RxJS subscription teardown are owned by the publishing or subscribing service's stack. Asynchronous disposal is reserved for cleanup that actually requires awaiting. TypeScript includes only the narrow ESNext.Disposable proposal library. Vite and Vitest reuse one unplugin-swc compatibility configuration. Concrete Chrome 111, Edge 111, and Safari 16.4 engine versions are the source of truth; the Vite target strings and SWC target map are derived from that one object instead of mixing browser releases with an ECMAScript edition. Solid's compiler runs first; SWC then lowers using and injects the focused core-js Symbol.dispose, DisposableStack, and SuppressedError modules from actual usage. The top-level Vite Oxc source transformer is disabled by unplugin-swc, while Vite's truthful final build.target and Oxc minifier remain in place. Host-only build and test configuration lives in apps/tauri-app/config, outside the WebView source graph. The jsdom unit suite does not claim Safari compatibility, nor does source lowering prove that Vite's development client supports the oldest production WebView; a future Vitest Browser suite will use a separate Playwright WebKit project with the same transform configuration.

WorkflowService.view derives the only active domain view from those resources. Page containers inject only their own service and workflow intentions; presentation components receive values and callbacks. Diagnosis is represented as idle, resolving, resolved, failed, or retained-but-unusable invalidated; a result renders only when its entity ID equals the latest session diagnosis ID. The frontend never parses ADB output. Tailwind CSS 4 consumes CSS-first semantic tokens, while local Solid primitives provide consistent sizes, variants, nested radii, focus states, and spacing without adding a component runtime. Park UI is a design reference only; workflow components do not depend on Park UI, Ark UI, or Panda CSS.

IPC schema v2 binds ADB selection to a Discovery ID, enumeration to an ADB Selection ID, diagnosis to an Enumeration and Device ID, and every change entity to a Diagnosis ID. Native paths and serials stay in backend session state. The System/Light/Dark preference is validated and stored by the backend in the application-private preferences file; the WebView receives no filesystem permission. Each Live or Demo workflow runs in a disposable child Injector. The root Injector does not provide DeviceGateway; the Live child binds the Tauri adapter and the Demo child binds only the deterministic fixture adapter, so Demo cannot fall through to device IPC.

The child session scope is also the arena-like lifetime boundary for its graph. Services, owned Subjects, subscriptions, Solid effects, and tutorial resources are registered beneath it and are released together in dependency-safe reverse order; rendering does not own or individually tear down domain subscriptions. Child-Injector consumers render beneath a Solid boundary keyed by Session scope identity, so replacing a scope reconstructs every context consumer instead of leaving it attached to the previous workflow graph. The global tutorial entry asks before replacing an active Live or Demo scope, refuses to switch during a device write, and always starts from a new isolated Demo's first workflow view after its DOM target is mounted. Tutorial metadata distinguishes passive explanation targets from actionable controls; a successful interaction on the currently highlighted control advances only after the next target mounts, while leaving the guided path dismisses Driver before the workflow continues. The Pin outcome leads into the snapshot and restore walkthrough rather than terminating the tutorial early. Demo identity and the exit action render at the session-shell level, so every simulated workflow view has the same escape path and exiting disposes the complete Demo scope.

Delivery architecture ​

acp-fixer-metadata.toml is the source of truth for the version, release targets, and macOS signing policy. Typed modules under scripts/lib/release own version/ref policy, staging, manifests, and idempotency decisions; GitHub workflow YAML only coordinates those operations. Tests, Release, Web, and Android Release have separate workflows and least-privilege permissions. A release is bound to an exact successful Tests run and source SHA, platform jobs upload private inputs, and a single assembly job verifies the complete eight-artifact matrix before attestation and publication. Stable macOS output requires signing/notarization; Windows uses one build path without Authenticode or CA credentials. All channels, including alpha/beta, require GitHub Artifact Attestations and SHA-256. The manifest signed field means platform signing only; provenance is presented separately. Stable Windows builds still require approval, and failed macOS signing never falls back. Workflows and release tooling never discover or invoke ADB.

English | 中文

Independent open-source project · MIT License