Two files, one repository
kurikomi-labs/komi-store ships 2 formats across 13 indexed files. The question worth asking is whether the second one says anything the first does not.
CompareAGENTS.md ↔ CLAUDE.md
| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 2 | 17 | 13 | 6% |
| Commands | 4 | 5 | 3 | 33% |
| Section tags | 3 | 2 | 1 | 50% |
What each file covers
Sections
2 shared · 17 only in A · 13 only in B- − AGENTS.md
- − Project Overview
- − Build & Run Commands
- − Android debug build
- − Desktop (run in dev mode)
- − Full build check (both platforms)
- − Lint (ktlint auto-formats on preBuild/compileKotlin* tasks automatically)
- − Desktop installers
- − State Management Pattern (every screen)
- − Module Layout
- − Convention Plugins (build-logic)
- − Dependency Injection
- − Key Cross-Cutting Concerns
- − Coding Conventions
- − Adding a New Feature
- − Feature-Level Documentation
- − Versions
- + Komi Store
- + Build
- + Structure
- + State pattern (every screen)
- + DI
- + Core repositories (`core/domain`)
- + Tech
- + Convention plugins (`build-logic/convention/`)
- + Adding a feature
- + Key configuration
- + Active skills (apply on matching domain)
- + Conventions
- + Approach
- Architecture
- Navigation
Commands
4 shared · 5 only in A · 3 only in B- − ./gradlew ktlintFormat
- − ./gradlew ktlintCheck
- − ./gradlew :composeApp:packageDmg
- − ./gradlew :composeApp:packageExe
- − ./gradlew :composeApp:packageDeb
- + ./gradlew :composeApp:packageExe :composeApp:packageMsi
- + ./gradlew :composeApp:packageDmg :composeApp:packagePkg
- + ./gradlew :composeApp:packageDeb :composeApp:packageRpm
- ./gradlew :composeApp:assembleDebug
- ./gradlew :composeApp:run
- ./gradlew build
- gradle/libs.versions.toml
Section tags
3 shared · 2 only in A · 1 only in B- − lint-format
- − docs
- + git-pr
- build
- code-style
- architecture
Line diff
kurikomi-labs/komi-store · AGENTS.md
@@ −1 @@
1# AGENTS.md
2
3This file provides guidance to WARP (warp.dev) when working with code in this repository.
4
5## Project Overview
6
7Komi Store is a cross-platform app store for GitHub releases built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. It targets **Android** (min API 26, target 36) and **Desktop** (Windows, macOS, Linux via JVM).
8
9Package: `zed.rainxch.githubstore`
10
11## Build & Run Commands
12
13```bash
14# Android debug build
15./gradlew :composeApp:assembleDebug
16
17# Desktop (run in dev mode)
18./gradlew :composeApp:run
19
20# Full build check (both platforms)
21./gradlew build
22
23# Lint (ktlint auto-formats on preBuild/compileKotlin* tasks automatically)
24./gradlew ktlintFormat # manual format all modules
25./gradlew ktlintCheck # check without fixing
26
27# Desktop installers
28./gradlew :composeApp:packageDmg # macOS
29./gradlew :composeApp:packageExe # Windows
30./gradlew :composeApp:packageDeb # Linux
31```
32
33**Requirements:** JDK 21+ (Temurin recommended), Android SDK for Android builds.
34
35**Setup:** Create a GitHub OAuth App and put `GITHUB_CLIENT_ID=<your_id>` in `local.properties` (root). Callback URL: `githubstore://callback`.
36
37## Architecture
38
39**Clean Architecture + MVVM** with strict layer separation:
40
41- **Domain** — Repository interfaces, models, use cases. No framework dependencies.
42- **Data** — Repository implementations, Ktor API clients, Room DAOs, DTOs, mappers. Each feature's DI module lives in `data/di/SharedModule.kt`.
43- **Presentation** — ViewModels with `StateFlow`/`Channel`, Compose screens.
44
45### State Management Pattern (every screen)
46
47Every ViewModel follows the same State/Action/Event pattern:
48
49- `State` — data class holding all UI state, exposed via `StateFlow`
50- `Action` — sealed interface for user input (clicks, refreshes)
51- `Event` — sealed interface for one-off effects (navigation, toasts), sent via `Channel.receiveAsFlow()`
52
53### Module Layout
54
55```text
56composeApp/ # App entry points, navigation, DI wiring
57 src/commonMain/ # Shared UI & wiring
58 src/androidMain/ # Android entry (MainActivity)
59 src/jvmMain/ # Desktop entry (DesktopApp.kt)
60core/
61 domain/ # Shared interfaces, models, use cases
62 data/ # Networking (Ktor), database (Room), DI, platform impls
63 presentation/ # Material 3 theming, reusable UI components, localized strings (13 languages)
64feature/<name>/
65 domain/ # Feature-specific interfaces & models
66 data/ # Feature-specific implementations & Koin DI module
67 presentation/ # Feature ViewModel + Compose screens
68build-logic/convention/ # Custom Gradle convention plugins
69```
70
71Some features (favourites, starred, recently-viewed, tweaks) are **presentation-only** — they use core repositories directly and register ViewModels in `composeApp/.../di/ViewModelsModule.kt` instead of having a `data/di/` layer.
72
73### Convention Plugins (build-logic)
74
75| Plugin ID | Use For |
76| :--- | :--- |
77| `convention.kmp.library` | KMP shared library modules (domain, data) |
78| `convention.cmp.library` | Compose Multiplatform library modules |
79| `convention.cmp.feature` | Feature presentation modules (auto-adds Compose + Koin + core:presentation) |
80| `convention.cmp.application` | Main app module |
81| `convention.room` | Room database modules |
82| `convention.buildkonfig` | Build-time config (reads from local.properties) |
83
84### Navigation
85
86Type-safe navigation using `@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../navigation/GithubStoreGraph.kt`. Routes are wired in `AppNavigation.kt`. Parameterized routes: `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate)`, `DeveloperProfileScreen(username)`.
87
88### Dependency Injection
89
90**Koin** — each feature's data layer defines a module in `data/di/SharedModule.kt`. All modules are registered in `composeApp/.../di/initKoin.kt`. ViewModels injected via `koinViewModel()`. `DetailsViewModel` and `MirrorPickerViewModel` use manual Koin `viewModel { }` with `parametersOf()` for constructor args; all others use `viewModelOf(::ClassName)`.
91
92### Key Cross-Cutting Concerns
93
94- **Auth flow:** GitHub device-flow OAuth. Primary path goes through backend proxy (`/v1/auth/device/start`, `/v1/auth/device/poll`); falls back to direct GitHub only on infrastructure errors (5xx, timeouts). HTTP 4xx and GitHub's negative 200-bodies never trigger fallback. Backend rate limits (10 starts/hr, 200 polls/hr per IP) are hard — do not add retry loops.
95- **`X-GitHub-Token` header:** Forwarded on every backend passthrough route — `/v1/search`, `/v1/search/explore`, `/v1/repo/{owner}/{name}`, `/v1/releases/{owner}/{name}`, `/v1/readme/{owner}/{name}`, `/v1/user/{username}`. Backend re-sends as `Authorization: token $token` so upstream GitHub calls run under the user's 5000/hr OAuth quota; without it the request falls back to the shared 60/hr anonymous bucket and a single 4xx can poison the backend's 15-min negative cache for everyone. DB-only routes (`/v1/categories`, `/v1/topics`, `/v1/events`, `/v1/auth/device/*`, `/v1/badge/*`) never get the header. Sourced via `BackendApiClient.currentUserGithubToken()` (`private`), never logged. 401 from passthrough routes ≠ session expired — `AuthenticationStateImpl` debounces consecutive 401s under the same token before clearing the session.
96- **Platform branching:** Source sets are `commonMain` (shared), `androidMain` (Android), `jvmMain` (Desktop). Some features (apps, installation, Shizuku) are Android-only.
97- **Shizuku (Android):** Optional silent install via AIDL service. Falls back to standard installer on failure.
98
99## Coding Conventions
100
101- Packages: `zed.rainxch.{module}.{layer}` (e.g. `zed.rainxch.home.data.repository`)
102- Private state: underscore prefix `_state`, `_events`
103- Sealed classes/interfaces for type-safe routes, actions, events
104- Repository pattern: interface in `domain/`, implementation in `data/`
105- Ktlint auto-runs on `preBuild`/`compileKotlin*` tasks; `ignoreFailures = true`
106- Ktlint rules: wildcard imports allowed, filename rule disabled, `@Composable` functions exempt from function naming rule (see `.editorconfig`)
107
108## Adding a New Feature
109
1101. Create `feature/<name>/domain/`, `feature/<name>/data/`, `feature/<name>/presentation/`
1112. Add `build.gradle.kts` in each using the appropriate convention plugin
1123. Add `include` entries in `settings.gradle.kts`
1134. Define domain interfaces/models in `domain/`
1145. Implement repository + Koin DI module in `data/di/SharedModule.kt`
1156. Create ViewModel (State/Action/Event pattern) and Screen in `presentation/`
1167. Add navigation route to `GithubStoreGraph.kt` and wire in `AppNavigation.kt`
1178. Register the Koin module in `initKoin.kt`
118
119## Feature-Level Documentation
120
121Each `feature/` directory contains its own `CLAUDE.md` with module structure, key interfaces, navigation routes, and implementation notes. Read those for feature-specific guidance.
122
123## Versions
124
125All library versions managed in `gradle/libs.versions.toml`. Key versions: Kotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1.
126
kurikomi-labs/komi-store · CLAUDE.md
@@ +1 @@
1# Komi Store
2
3Cross-platform app store for GitHub + Codeberg + Forgejo releases. **Kotlin Multiplatform** + **Compose Multiplatform**. Android (min API 26) + Desktop (JVM: Win/macOS/Linux). Package `zed.rainxch.githubstore`. Version 1.8.3 (code 18). Target SDK 36.
4
5## Build
6
7```bash
8./gradlew :composeApp:assembleDebug # Android
9./gradlew :composeApp:run # Desktop dev
10./gradlew :composeApp:packageExe :composeApp:packageMsi # Win installer
11./gradlew :composeApp:packageDmg :composeApp:packagePkg # macOS
12./gradlew :composeApp:packageDeb :composeApp:packageRpm # Linux
13./gradlew build # full
14```
15
16JDK 21+. Android SDK for Android.
17
18## Structure
19
20```text
21composeApp/ # entry points, navigation, DI wiring (commonMain / androidMain / jvmMain)
22core/
23 domain/ # interfaces, models, use cases (no framework deps)
24 data/ # repos, Ktor, Room, Koin, platform impls
25 presentation/ # Material 3 theme + reusable components + 14-locale strings
26feature/
27 apps auth details dev-profile favourites homeP profile recently-viewed search starred tweaks
28build-logic/convention/ # convention plugins
29```
30
31Each feature: up to 3 sub-modules (`domain/`, `data/`, `presentation/`). `favourites`, `starred`, `recently-viewed` are presentation-only.
32
33## Architecture
34
35Clean Architecture + MVVM. Layers: **Domain** (contracts), **Data** (Ktor + Room + Koin DI), **Presentation** (ViewModels with `StateFlow`/`Channel`, Compose).
36
37### State pattern (every screen)
38
39```kotlin
40class XViewModel : ViewModel() {
41 private val _state = MutableStateFlow(XState())
42 val state = _state.asStateFlow() // or .stateIn(WhileSubscribed)
43 private val _events = Channel<XEvent>()
44 val events = _events.receiveAsFlow()
45 fun onAction(action: XAction) { ... }
46}
47```
48
49`State` = data class. `Action` = sealed (user input). `Event` = sealed (one-off effects).
50
51### Navigation
52
53`@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../app/navigation/`. Routes: `HomeScreen`, `SearchScreen`, `AuthenticationScreen`, `ProfileScreen`, `TweaksScreen`, `FavouritesScreen`, `StarredReposScreen`, `RecentlyViewedScreen`, `AppsScreen`, `OnboardingScreen`, `ExternalImportScreen`, `MirrorPickerScreen`, `StarredPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `WhatsNewHistoryScreen`, `AnnouncementsScreen`, `HostTokensScreen`, `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate, sourceHost)`, `DeveloperProfileScreen(username)`. `DetailsScreen.sourceHost` is non-null for Codeberg / Forgejo / custom-forge repos — routes all `DetailsRepository` calls through `ForgejoClientRegistry` instead of the GitHub-backed default path.
54
55### DI
56
57Koin. Feature modules in `data/di/SharedModule.kt`. ViewModels in `composeApp/.../app/di/ViewModelsModule.kt` (`viewModelOf(::X)` or explicit `viewModel { ... }`). Wired in `initKoin.kt`.
58
59## Core repositories (`core/domain`)
60
61`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `SeenReposRepository`, `HiddenReposRepository`, `SearchHistoryRepository`, `TweaksRepository`, `AuthenticationState`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`, `ExternalImportRepository`, `TelemetryRepository`, `HostTokenRepository` (per-host PATs, KSafe-encrypted). Network: `ForgejoApiClient` + `ForgejoClientRegistry` (per-host Ktor clients, thread-safe via Mutex, proxy-aware, closes cached engines on shutdown / proxy change). Util: `AssetVariant` (token/glob/stem fingerprinting), `assetPlatformOf`, `RepoIdCodec` (23-bit host fingerprint + 40-bit raw id packed into the existing 64-bit `repoId` slot — sign bit = foreign source), `RepositoryUrlParser` (recognises GitHub + Codeberg + gitea.com + git.disroot.org + user-added forge hosts). System interfaces: `Installer`, `InstallerStatusProvider`, `PackageMonitor`, `SystemInstallSerializer`.
62
63## Tech
64
65Kotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1, kotlinx.serialization 1.10.0, DataStore 1.2.0, Landscapist 2.9.5, Kermit 2.0.8, MOKO Permissions 0.20.1, Navigation Compose 2.9.2, multiplatform-markdown-renderer 0.39.2, Shizuku 13.1.5, WorkManager 2.11.1, kotlinx.datetime 0.7.1. Versions in `gradle/libs.versions.toml`.
66
67## Convention plugins (`build-logic/convention/`)
68
69`convention.kmp.library` (domain/data), `convention.cmp.library` (core/presentation), `convention.cmp.feature` (feature presentation), `convention.cmp.application` (main app), `convention.room`, `convention.buildkonfig`.
70
71## Adding a feature
72
731. `feature/<name>/{domain,data,presentation}/` with appropriate convention plugin
742. `include` in `settings.gradle.kts`
753. Domain interfaces → impl + Koin module in `data/di/SharedModule.kt` → ViewModel + Screen
764. Route in `GithubStoreGraph.kt` + wire in `AppNavigation.kt` + register Koin in `initKoin.kt`
77
78## Key configuration
79
80- **GitHub OAuth:** `GITHUB_CLIENT_ID` in `local.properties`. Deep links: `githubstore://auth` (web-OAuth handoff), `githubstore://callback` (legacy device-flow leftover), `githubstore://repo`, `githubstore://apps`.
81- **Shizuku (Android):** silent install via `ShizukuProvider` → AIDL → `pm install -S`. Fallback to standard installer on failure.
82- **Desktop logs:** `CrashReporter` (first line of `DesktopApp.main`) tees stdout/stderr to rotating `session.log` + writes `crash-<ts>.log` on uncaught. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Win), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android = Logcat.
83- **macOS distribution:** Homebrew cask in tap `openhub-store/tap` (separate repo `homebrew-tap`). `brew install --cask github-store`. Unsigned at present — user must `xattr -dr com.apple.quarantine /Applications/GitHub-Store.app` after install. CI builds `.dmg` + `.pkg` on every push to `generate-installers`; tap cask updates automatically on release.
84- **`X-GitHub-Token` header:** Client attaches when `TokenStore.currentToken()` is non-null on `/v1/search`, `/v1/search/explore`, `/v1/repo`, `/v1/releases`, `/v1/readme`, `/v1/user`. Backend re-sends as `Authorization: token $token` to GitHub. Without it, backend round-robins a 4-token service pool. Upstream 401 remapped to backend `502` (handled like "GitHub unreachable" — fall back via `shouldFallbackToGithubOrRethrow`). `429` = no fallback (same wall), only backoff. `UnauthorizedInterceptor` only on direct-GitHub client; `AuthenticationStateImpl` debounces consecutive 401s by token snapshot.
85- **Auth flow (web-OAuth-first):** Primary path is web OAuth with PKCE + handoff. `feature/auth/data/crypto/PkceGenerator` mints `(state, codeVerifier, codeChallenge)`; `WebAuthApi.register` POSTs verifier + challenge + state to `https://github-store.org/auth/register` (Cloudflare Worker stashes them in Workers KV) and returns `authUrl`. User opens it, authorizes on `github.com`, GitHub redirects to `github-store.org/auth/callback?code&state` where the Worker exchanges the code via `api.github-store.org` (backend stores `(handoffId → access_token)` for 60s in Postgres with atomic `DELETE…RETURNING`), then bounces back to `githubstore://auth?h=<handoffId>`. App reads handoff via `WebAuthApi.consumeHandoff` (GETDEL semantics). Secondary path: device flow via backend `/v1/auth/device/start` + `/poll`, `AuthPath` (`Backend`|`Direct`) tracked in `SavedStateHandle`, only escalates `Backend → Direct` on infra errors. Tertiary: paste a Personal Access Token (`signInWithPat` — validates against `/user`, persists optimistically when GitHub unreachable). Backend rate limits: 10 device-starts/hr, 200 device-polls/hr per IP. Endpoints in `core/data/network/BackendEndpoints.kt` (`BACKEND_ORIGIN`, `WEB_ORIGIN`).
86- **Windows installer signing (SignPath Foundation):** CI workflow `.github/workflows/build-desktop-platforms.yml` job `sign-windows` after every push to `generate-installers` branch. Action pinned to commit SHA (not `@v2`). Secrets: `SIGNPATH_API_TOKEN`, `SIGNPATH_ORGANIZATION_ID` (`1ecf111e-...`). Variable `SIGNPATH_SIGNING_POLICY_SLUG` = `test-signing` until prod cert issued; flip to `release-signing`. Project slug `GitHub-Store`, artifact config slug `initial`. Unsigned artifact deleted post-sign; only `windows-installers-signed` reaches the draft release.
87- **WinGet publish:** `.github/workflows/winget-publish.yml` fires on `release: [released]`. Action `vedantmgoyal9/winget-releaser@main`. Secret `WINGET_TOKEN` = PAT with `Contents+Pull requests: write` on `OpenHub-Store/winget-pkgs` (fork of `microsoft/winget-pkgs`). Pin `fork-user: OpenHub-Store` explicitly so the action doesn't infer from token owner.
88- **Forges (Codeberg / Forgejo / Gitea):** `ForgejoApiClient` per host (60s req / 30s connect+socket timeouts, exponential retry on 5xx + IOException). `ForgejoClientRegistry.clientFor(host)` cached + Mutex-guarded. Direct-to-forge — no backend mediator. `RepoIdCodec` packs host fingerprint into `repoId` so the existing GitHub-shaped schema survives. README via `/contents/README.md?ref={branch}` (Forgejo has NO `/readme` endpoint). License sniffed from `/contents/LICENSE` regex against SPDX headers. Downloads aggregated by summing `asset.download_count` across releases.
89- **Per-host PATs:** `HostTokenRepository` stores `{host, token, label, createdAt}` rows AES-256-GCM encrypted via KSafe. `HostTokenInterceptor` (Ktor plugin) injects `Authorization: token $pat` on matched host. `HostNames.apiHostToTokenHost` maps `api.github.com → github.com` so the GitHub-direct client looks up the right PAT. UI at `Tweaks → Access Tokens` (`HostTokensScreen`).
90- **KSafe:** AES-256-GCM with hardware-backed Keystore on Android. Wraps every persisted credential / pref via `core/data/secure/KSafeSafe.kt` extension funcs (`safeGet`, `safePut`, `safeDelete`, `safeGetFlow`) — surface log + return null/false on transient failure instead of throwing through coroutine scopes.
91- **Translation providers:** `TranslationProvider` enum = `GOOGLE`, `YOUDAO`, `LIBRE_TRANSLATE`, `DEEPL`, `MICROSOFT`. Each per-provider config persisted via `TweaksRepository` (KSafe-encrypted). `TranslationRepositoryImpl.resolveTranslator()` picks the impl. LibreTranslate defaults to the bundled `translate.disroot.org` mirror when user URL pref blank. DeepL auto-routes `:fx`-suffixed keys to `api-free.deepl.com`. Microsoft uses No-Trace by default — text never stored, never used for training.
92- **Gradle:** Config + build cache enabled. 4GB Gradle heap, 3GB Kotlin daemon. Official Kotlin style.
93
94## Active skills (apply on matching domain)
95
96- **caveman** — session default, terse output.
97- **karpathy-guidelines** — anti-overcomplication, minimal diffs, surface assumptions, verifiable success criteria. Every coding task.
98- **one-skill-to-rule-them-all** — watch for skill-capture opportunities during multi-step work.
99- **gsd-inbox** - Triage open GitHub issues + PRs against templates. Our exact pattern — automate the "check issue #N, draft reply, ship fix" loop.
100- **gsd-ship** - Create PR + review + prep for merge. Every task ends here.
101- **gsd-quick** - Trivial task with atomic commits + state tracking. Matches our small-commit policy.
102- **gsd-debug** - Systematic debugging with persistent state across context resets. For bug-hunt cycles.
103- **android-* skills** (`~/.claude/skills/android/`) — auto-fire by description match; apply when in matching domain:
104 - `android-compose-ui` — composables, recomposition, animations, modifiers, design system
105 - `android-data-layer` — repos, DTOs, Room, Ktor, mappers
106 - `android-di-koin` — Koin module setup, ViewModel injection
107 - `android-error-handling` — Result wrapper, typed errors
108 - `android-module-structure` — feature-layered modules, convention plugins
109 - `android-navigation` — type-safe Compose nav
110 - `android-presentation-mvi` — State/Action/Event, Root/Screen split, UiText, SavedStateHandle
111 - `android-testing` — testing patterns
112
113## Conventions
114
115- Packages `zed.rainxch.{module}.{layer}`
116- Private state fields prefix `_state`
117- Sealed routes/actions/events
118- Repository pattern: interface in `domain/`, impl in `data/`
119- Source sets: `commonMain` shared, `androidMain`, `jvmMain`
120- **No KDoc, no inline comments** unless the user explicitly asks. No function/class docs. Inline only for non-obvious invariants, tricky concurrency, workarounds. Applies globally.
121- Feature-specific guidance in each `feature/*/CLAUDE.md`
122
123## Approach
124
125- Read existing files before writing. Don't re-read unless changed.
126- Thorough in reasoning, concise in output.
127- Skip files over 100KB unless required.
128- No sycophantic openers or closing fluff.
129- No emojis or em-dashes.
130- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting, researching if necessary.
131
@@ −1 +1 @@
1−# AGENTS.md
1+# Komi Store
22
3−This file provides guidance to WARP (warp.dev) when working with code in this repository.
3+Cross-platform app store for GitHub + Codeberg + Forgejo releases. **Kotlin Multiplatform** + **Compose Multiplatform**. Android (min API 26) + Desktop (JVM: Win/macOS/Linux). Package `zed.rainxch.githubstore`. Version 1.8.3 (code 18). Target SDK 36.
44
5−## Project Overview
5+## Build
66
7−Komi Store is a cross-platform app store for GitHub releases built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. It targets **Android** (min API 26, target 36) and **Desktop** (Windows, macOS, Linux via JVM).
8−
9−Package: `zed.rainxch.githubstore`
10−
11−## Build & Run Commands
12−
137 ```bash
14−# Android debug build
15−./gradlew :composeApp:assembleDebug
8+./gradlew :composeApp:assembleDebug # Android
9+./gradlew :composeApp:run # Desktop dev
10+./gradlew :composeApp:packageExe :composeApp:packageMsi # Win installer
11+./gradlew :composeApp:packageDmg :composeApp:packagePkg # macOS
12+./gradlew :composeApp:packageDeb :composeApp:packageRpm # Linux
13+./gradlew build # full
14+```
1615
17−# Desktop (run in dev mode)
18−./gradlew :composeApp:run
16+JDK 21+. Android SDK for Android.
1917
20−# Full build check (both platforms)
21−./gradlew build
18+## Structure
2219
23−# Lint (ktlint auto-formats on preBuild/compileKotlin* tasks automatically)
24−./gradlew ktlintFormat # manual format all modules
25−./gradlew ktlintCheck # check without fixing
26−
27−# Desktop installers
28−./gradlew :composeApp:packageDmg # macOS
29−./gradlew :composeApp:packageExe # Windows
30−./gradlew :composeApp:packageDeb # Linux
20+```text
21+composeApp/ # entry points, navigation, DI wiring (commonMain / androidMain / jvmMain)
22+core/
23+ domain/ # interfaces, models, use cases (no framework deps)
24+ data/ # repos, Ktor, Room, Koin, platform impls
25+ presentation/ # Material 3 theme + reusable components + 14-locale strings
26+feature/
27+ apps auth details dev-profile favourites homeP profile recently-viewed search starred tweaks
28+build-logic/convention/ # convention plugins
3129 ```
3230
33−**Requirements:** JDK 21+ (Temurin recommended), Android SDK for Android builds.
31+Each feature: up to 3 sub-modules (`domain/`, `data/`, `presentation/`). `favourites`, `starred`, `recently-viewed` are presentation-only.
3432
35−**Setup:** Create a GitHub OAuth App and put `GITHUB_CLIENT_ID=<your_id>` in `local.properties` (root). Callback URL: `githubstore://callback`.
36−
3733 ## Architecture
3834
39−**Clean Architecture + MVVM** with strict layer separation:
35+Clean Architecture + MVVM. Layers: **Domain** (contracts), **Data** (Ktor + Room + Koin DI), **Presentation** (ViewModels with `StateFlow`/`Channel`, Compose).
4036
41−- **Domain** — Repository interfaces, models, use cases. No framework dependencies.
42−- **Data** — Repository implementations, Ktor API clients, Room DAOs, DTOs, mappers. Each feature's DI module lives in `data/di/SharedModule.kt`.
43−- **Presentation** — ViewModels with `StateFlow`/`Channel`, Compose screens.
37+### State pattern (every screen)
4438
45−### State Management Pattern (every screen)
39+```kotlin
40+class XViewModel : ViewModel() {
41+ private val _state = MutableStateFlow(XState())
42+ val state = _state.asStateFlow() // or .stateIn(WhileSubscribed)
43+ private val _events = Channel<XEvent>()
44+ val events = _events.receiveAsFlow()
45+ fun onAction(action: XAction) { ... }
46+}
47+```
4648
47−Every ViewModel follows the same State/Action/Event pattern:
49+`State` = data class. `Action` = sealed (user input). `Event` = sealed (one-off effects).
4850
49−- `State` — data class holding all UI state, exposed via `StateFlow`
50−- `Action` — sealed interface for user input (clicks, refreshes)
51−- `Event` — sealed interface for one-off effects (navigation, toasts), sent via `Channel.receiveAsFlow()`
51+### Navigation
5252
53−### Module Layout
53+`@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../app/navigation/`. Routes: `HomeScreen`, `SearchScreen`, `AuthenticationScreen`, `ProfileScreen`, `TweaksScreen`, `FavouritesScreen`, `StarredReposScreen`, `RecentlyViewedScreen`, `AppsScreen`, `OnboardingScreen`, `ExternalImportScreen`, `MirrorPickerScreen`, `StarredPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `WhatsNewHistoryScreen`, `AnnouncementsScreen`, `HostTokensScreen`, `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate, sourceHost)`, `DeveloperProfileScreen(username)`. `DetailsScreen.sourceHost` is non-null for Codeberg / Forgejo / custom-forge repos — routes all `DetailsRepository` calls through `ForgejoClientRegistry` instead of the GitHub-backed default path.
5454
55−```text
56−composeApp/ # App entry points, navigation, DI wiring
57− src/commonMain/ # Shared UI & wiring
58− src/androidMain/ # Android entry (MainActivity)
59− src/jvmMain/ # Desktop entry (DesktopApp.kt)
60−core/
61− domain/ # Shared interfaces, models, use cases
62− data/ # Networking (Ktor), database (Room), DI, platform impls
63− presentation/ # Material 3 theming, reusable UI components, localized strings (13 languages)
64−feature/<name>/
65− domain/ # Feature-specific interfaces & models
66− data/ # Feature-specific implementations & Koin DI module
67− presentation/ # Feature ViewModel + Compose screens
68−build-logic/convention/ # Custom Gradle convention plugins
69−```
55+### DI
7056
71−Some features (favourites, starred, recently-viewed, tweaks) are **presentation-only** — they use core repositories directly and register ViewModels in `composeApp/.../di/ViewModelsModule.kt` instead of having a `data/di/` layer.
57+Koin. Feature modules in `data/di/SharedModule.kt`. ViewModels in `composeApp/.../app/di/ViewModelsModule.kt` (`viewModelOf(::X)` or explicit `viewModel { ... }`). Wired in `initKoin.kt`.
7258
73−### Convention Plugins (build-logic)
59+## Core repositories (`core/domain`)
7460
75−| Plugin ID | Use For |
76−| :--- | :--- |
77−| `convention.kmp.library` | KMP shared library modules (domain, data) |
78−| `convention.cmp.library` | Compose Multiplatform library modules |
79−| `convention.cmp.feature` | Feature presentation modules (auto-adds Compose + Koin + core:presentation) |
80−| `convention.cmp.application` | Main app module |
81−| `convention.room` | Room database modules |
82−| `convention.buildkonfig` | Build-time config (reads from local.properties) |
61+`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `SeenReposRepository`, `HiddenReposRepository`, `SearchHistoryRepository`, `TweaksRepository`, `AuthenticationState`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`, `ExternalImportRepository`, `TelemetryRepository`, `HostTokenRepository` (per-host PATs, KSafe-encrypted). Network: `ForgejoApiClient` + `ForgejoClientRegistry` (per-host Ktor clients, thread-safe via Mutex, proxy-aware, closes cached engines on shutdown / proxy change). Util: `AssetVariant` (token/glob/stem fingerprinting), `assetPlatformOf`, `RepoIdCodec` (23-bit host fingerprint + 40-bit raw id packed into the existing 64-bit `repoId` slot — sign bit = foreign source), `RepositoryUrlParser` (recognises GitHub + Codeberg + gitea.com + git.disroot.org + user-added forge hosts). System interfaces: `Installer`, `InstallerStatusProvider`, `PackageMonitor`, `SystemInstallSerializer`.
8362
84−### Navigation
63+## Tech
8564
86−Type-safe navigation using `@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../navigation/GithubStoreGraph.kt`. Routes are wired in `AppNavigation.kt`. Parameterized routes: `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate)`, `DeveloperProfileScreen(username)`.
65+Kotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1, kotlinx.serialization 1.10.0, DataStore 1.2.0, Landscapist 2.9.5, Kermit 2.0.8, MOKO Permissions 0.20.1, Navigation Compose 2.9.2, multiplatform-markdown-renderer 0.39.2, Shizuku 13.1.5, WorkManager 2.11.1, kotlinx.datetime 0.7.1. Versions in `gradle/libs.versions.toml`.
8766
88−### Dependency Injection
67+## Convention plugins (`build-logic/convention/`)
8968
90−**Koin** — each feature's data layer defines a module in `data/di/SharedModule.kt`. All modules are registered in `composeApp/.../di/initKoin.kt`. ViewModels injected via `koinViewModel()`. `DetailsViewModel` and `MirrorPickerViewModel` use manual Koin `viewModel { }` with `parametersOf()` for constructor args; all others use `viewModelOf(::ClassName)`.
69+`convention.kmp.library` (domain/data), `convention.cmp.library` (core/presentation), `convention.cmp.feature` (feature presentation), `convention.cmp.application` (main app), `convention.room`, `convention.buildkonfig`.
9170
92−### Key Cross-Cutting Concerns
71+## Adding a feature
9372
94−- **Auth flow:** GitHub device-flow OAuth. Primary path goes through backend proxy (`/v1/auth/device/start`, `/v1/auth/device/poll`); falls back to direct GitHub only on infrastructure errors (5xx, timeouts). HTTP 4xx and GitHub's negative 200-bodies never trigger fallback. Backend rate limits (10 starts/hr, 200 polls/hr per IP) are hard — do not add retry loops.
95−- **`X-GitHub-Token` header:** Forwarded on every backend passthrough route — `/v1/search`, `/v1/search/explore`, `/v1/repo/{owner}/{name}`, `/v1/releases/{owner}/{name}`, `/v1/readme/{owner}/{name}`, `/v1/user/{username}`. Backend re-sends as `Authorization: token $token` so upstream GitHub calls run under the user's 5000/hr OAuth quota; without it the request falls back to the shared 60/hr anonymous bucket and a single 4xx can poison the backend's 15-min negative cache for everyone. DB-only routes (`/v1/categories`, `/v1/topics`, `/v1/events`, `/v1/auth/device/*`, `/v1/badge/*`) never get the header. Sourced via `BackendApiClient.currentUserGithubToken()` (`private`), never logged. 401 from passthrough routes ≠ session expired — `AuthenticationStateImpl` debounces consecutive 401s under the same token before clearing the session.
96−- **Platform branching:** Source sets are `commonMain` (shared), `androidMain` (Android), `jvmMain` (Desktop). Some features (apps, installation, Shizuku) are Android-only.
97−- **Shizuku (Android):** Optional silent install via AIDL service. Falls back to standard installer on failure.
73+1. `feature/<name>/{domain,data,presentation}/` with appropriate convention plugin
74+2. `include` in `settings.gradle.kts`
75+3. Domain interfaces → impl + Koin module in `data/di/SharedModule.kt` → ViewModel + Screen
76+4. Route in `GithubStoreGraph.kt` + wire in `AppNavigation.kt` + register Koin in `initKoin.kt`
9877
99−## Coding Conventions
78+## Key configuration
10079
101−- Packages: `zed.rainxch.{module}.{layer}` (e.g. `zed.rainxch.home.data.repository`)
102−- Private state: underscore prefix `_state`, `_events`
103−- Sealed classes/interfaces for type-safe routes, actions, events
104−- Repository pattern: interface in `domain/`, implementation in `data/`
105−- Ktlint auto-runs on `preBuild`/`compileKotlin*` tasks; `ignoreFailures = true`
106−- Ktlint rules: wildcard imports allowed, filename rule disabled, `@Composable` functions exempt from function naming rule (see `.editorconfig`)
80+- **GitHub OAuth:** `GITHUB_CLIENT_ID` in `local.properties`. Deep links: `githubstore://auth` (web-OAuth handoff), `githubstore://callback` (legacy device-flow leftover), `githubstore://repo`, `githubstore://apps`.
81+- **Shizuku (Android):** silent install via `ShizukuProvider` → AIDL → `pm install -S`. Fallback to standard installer on failure.
82+- **Desktop logs:** `CrashReporter` (first line of `DesktopApp.main`) tees stdout/stderr to rotating `session.log` + writes `crash-<ts>.log` on uncaught. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Win), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android = Logcat.
83+- **macOS distribution:** Homebrew cask in tap `openhub-store/tap` (separate repo `homebrew-tap`). `brew install --cask github-store`. Unsigned at present — user must `xattr -dr com.apple.quarantine /Applications/GitHub-Store.app` after install. CI builds `.dmg` + `.pkg` on every push to `generate-installers`; tap cask updates automatically on release.
84+- **`X-GitHub-Token` header:** Client attaches when `TokenStore.currentToken()` is non-null on `/v1/search`, `/v1/search/explore`, `/v1/repo`, `/v1/releases`, `/v1/readme`, `/v1/user`. Backend re-sends as `Authorization: token $token` to GitHub. Without it, backend round-robins a 4-token service pool. Upstream 401 remapped to backend `502` (handled like "GitHub unreachable" — fall back via `shouldFallbackToGithubOrRethrow`). `429` = no fallback (same wall), only backoff. `UnauthorizedInterceptor` only on direct-GitHub client; `AuthenticationStateImpl` debounces consecutive 401s by token snapshot.
85+- **Auth flow (web-OAuth-first):** Primary path is web OAuth with PKCE + handoff. `feature/auth/data/crypto/PkceGenerator` mints `(state, codeVerifier, codeChallenge)`; `WebAuthApi.register` POSTs verifier + challenge + state to `https://github-store.org/auth/register` (Cloudflare Worker stashes them in Workers KV) and returns `authUrl`. User opens it, authorizes on `github.com`, GitHub redirects to `github-store.org/auth/callback?code&state` where the Worker exchanges the code via `api.github-store.org` (backend stores `(handoffId → access_token)` for 60s in Postgres with atomic `DELETE…RETURNING`), then bounces back to `githubstore://auth?h=<handoffId>`. App reads handoff via `WebAuthApi.consumeHandoff` (GETDEL semantics). Secondary path: device flow via backend `/v1/auth/device/start` + `/poll`, `AuthPath` (`Backend`|`Direct`) tracked in `SavedStateHandle`, only escalates `Backend → Direct` on infra errors. Tertiary: paste a Personal Access Token (`signInWithPat` — validates against `/user`, persists optimistically when GitHub unreachable). Backend rate limits: 10 device-starts/hr, 200 device-polls/hr per IP. Endpoints in `core/data/network/BackendEndpoints.kt` (`BACKEND_ORIGIN`, `WEB_ORIGIN`).
86+- **Windows installer signing (SignPath Foundation):** CI workflow `.github/workflows/build-desktop-platforms.yml` job `sign-windows` after every push to `generate-installers` branch. Action pinned to commit SHA (not `@v2`). Secrets: `SIGNPATH_API_TOKEN`, `SIGNPATH_ORGANIZATION_ID` (`1ecf111e-...`). Variable `SIGNPATH_SIGNING_POLICY_SLUG` = `test-signing` until prod cert issued; flip to `release-signing`. Project slug `GitHub-Store`, artifact config slug `initial`. Unsigned artifact deleted post-sign; only `windows-installers-signed` reaches the draft release.
87+- **WinGet publish:** `.github/workflows/winget-publish.yml` fires on `release: [released]`. Action `vedantmgoyal9/winget-releaser@main`. Secret `WINGET_TOKEN` = PAT with `Contents+Pull requests: write` on `OpenHub-Store/winget-pkgs` (fork of `microsoft/winget-pkgs`). Pin `fork-user: OpenHub-Store` explicitly so the action doesn't infer from token owner.
88+- **Forges (Codeberg / Forgejo / Gitea):** `ForgejoApiClient` per host (60s req / 30s connect+socket timeouts, exponential retry on 5xx + IOException). `ForgejoClientRegistry.clientFor(host)` cached + Mutex-guarded. Direct-to-forge — no backend mediator. `RepoIdCodec` packs host fingerprint into `repoId` so the existing GitHub-shaped schema survives. README via `/contents/README.md?ref={branch}` (Forgejo has NO `/readme` endpoint). License sniffed from `/contents/LICENSE` regex against SPDX headers. Downloads aggregated by summing `asset.download_count` across releases.
89+- **Per-host PATs:** `HostTokenRepository` stores `{host, token, label, createdAt}` rows AES-256-GCM encrypted via KSafe. `HostTokenInterceptor` (Ktor plugin) injects `Authorization: token $pat` on matched host. `HostNames.apiHostToTokenHost` maps `api.github.com → github.com` so the GitHub-direct client looks up the right PAT. UI at `Tweaks → Access Tokens` (`HostTokensScreen`).
90+- **KSafe:** AES-256-GCM with hardware-backed Keystore on Android. Wraps every persisted credential / pref via `core/data/secure/KSafeSafe.kt` extension funcs (`safeGet`, `safePut`, `safeDelete`, `safeGetFlow`) — surface log + return null/false on transient failure instead of throwing through coroutine scopes.
91+- **Translation providers:** `TranslationProvider` enum = `GOOGLE`, `YOUDAO`, `LIBRE_TRANSLATE`, `DEEPL`, `MICROSOFT`. Each per-provider config persisted via `TweaksRepository` (KSafe-encrypted). `TranslationRepositoryImpl.resolveTranslator()` picks the impl. LibreTranslate defaults to the bundled `translate.disroot.org` mirror when user URL pref blank. DeepL auto-routes `:fx`-suffixed keys to `api-free.deepl.com`. Microsoft uses No-Trace by default — text never stored, never used for training.
92+- **Gradle:** Config + build cache enabled. 4GB Gradle heap, 3GB Kotlin daemon. Official Kotlin style.
10793
108−## Adding a New Feature
94+## Active skills (apply on matching domain)
10995
110−1. Create `feature/<name>/domain/`, `feature/<name>/data/`, `feature/<name>/presentation/`
111−2. Add `build.gradle.kts` in each using the appropriate convention plugin
112−3. Add `include` entries in `settings.gradle.kts`
113−4. Define domain interfaces/models in `domain/`
114−5. Implement repository + Koin DI module in `data/di/SharedModule.kt`
115−6. Create ViewModel (State/Action/Event pattern) and Screen in `presentation/`
116−7. Add navigation route to `GithubStoreGraph.kt` and wire in `AppNavigation.kt`
117−8. Register the Koin module in `initKoin.kt`
96+- **caveman** — session default, terse output.
97+- **karpathy-guidelines** — anti-overcomplication, minimal diffs, surface assumptions, verifiable success criteria. Every coding task.
98+- **one-skill-to-rule-them-all** — watch for skill-capture opportunities during multi-step work.
99+- **gsd-inbox** - Triage open GitHub issues + PRs against templates. Our exact pattern — automate the "check issue #N, draft reply, ship fix" loop.
100+- **gsd-ship** - Create PR + review + prep for merge. Every task ends here.
101+- **gsd-quick** - Trivial task with atomic commits + state tracking. Matches our small-commit policy.
102+- **gsd-debug** - Systematic debugging with persistent state across context resets. For bug-hunt cycles.
103+- **android-* skills** (`~/.claude/skills/android/`) — auto-fire by description match; apply when in matching domain:
104+ - `android-compose-ui` — composables, recomposition, animations, modifiers, design system
105+ - `android-data-layer` — repos, DTOs, Room, Ktor, mappers
106+ - `android-di-koin` — Koin module setup, ViewModel injection
107+ - `android-error-handling` — Result wrapper, typed errors
108+ - `android-module-structure` — feature-layered modules, convention plugins
109+ - `android-navigation` — type-safe Compose nav
110+ - `android-presentation-mvi` — State/Action/Event, Root/Screen split, UiText, SavedStateHandle
111+ - `android-testing` — testing patterns
118112
119−## Feature-Level Documentation
113+## Conventions
120114
121−Each `feature/` directory contains its own `CLAUDE.md` with module structure, key interfaces, navigation routes, and implementation notes. Read those for feature-specific guidance.
115+- Packages `zed.rainxch.{module}.{layer}`
116+- Private state fields prefix `_state`
117+- Sealed routes/actions/events
118+- Repository pattern: interface in `domain/`, impl in `data/`
119+- Source sets: `commonMain` shared, `androidMain`, `jvmMain`
120+- **No KDoc, no inline comments** unless the user explicitly asks. No function/class docs. Inline only for non-obvious invariants, tricky concurrency, workarounds. Applies globally.
121+- Feature-specific guidance in each `feature/*/CLAUDE.md`
122122
123−## Versions
123+## Approach
124124
125−All library versions managed in `gradle/libs.versions.toml`. Key versions: Kotlin 2.3.10, Compose Multiplatform 1.10.3, Ktor 3.4.0, Room 2.8.4, Koin 4.1.1.
125+- Read existing files before writing. Don't re-read unless changed.
126+- Thorough in reasoning, concise in output.
127+- Skip files over 100KB unless required.
128+- No sycophantic openers or closing fluff.
129+- No emojis or em-dashes.
130+- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting, researching if necessary.
126131
