{"owner":"milanvarady","repo":"Applite","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nApplite is a native macOS GUI application for Homebrew Casks, designed as an \"app store for third-party apps\" rather than a full Homebrew wrapper. Target audience is non-technical users who want simple app installation/management.\n\n- **Language**: Swift with SwiftUI (`@Observable`, `@MainActor`, async/await)\n- **Platform**: macOS 14+ (Apple Silicon and Intel)\n- **Build System**: Xcode with Swift Package Manager for dependencies. Uses Xcode 16+ **file-system synchronized groups** (folder-based project): the on-disk folder structure *is* the project structure. Adding, removing, moving, or renaming source files needs **no `project.pbxproj` edits** — just change the files on disk and Xcode picks them up automatically. Do not hand-edit the pbxproj for file management.\n- **Database**: GRDB.swift (SQLite) at `~/Library/Application Support/Applite/casks.sqlite`\n\n## Build & Run\n\nOpen `Applite.xcodeproj` in Xcode and build/run (⌘R). Dependencies resolve automatically via SPM.\n\n## Architecture\n\n### Data Flow\n\n1. **App Launch**: `ContentView.task(id: bootstrap.attempt)` calls `caskManager.bootstrapAndLoad()`\n2. **First Run**: `HomebrewBootstrap` installs/validates brew; while `bootstrap.needsSetupOverlay` is true, `ContentView` covers the window with the non-dismissable `ComponentsInstallView` modal (`Features/Bootstrap/`). There is no separate onboarding flow — the app opens straight into the main UI\n3. **Main UI**: `ContentView` with `NavigationSplitView` sidebar navigation\n4. **Data Loading**: `CaskManager.loadData()` runs in two stages — catalog (DB-only, instant) then brew CLI state (slow). The UI lights up after stage 1; installed/outdated state arrives reactively as stage 2 completes.\n\n### Key Components\n\nThe shared cask/brew engine lives under `Applite/Core/`, split into focused subfolders:\n\n**Persistence** (`Applite/Core/Database/`)\n- `AppDatabase` - Schema migrations, DatabasePool with WAL mode, FTS5 virtual table on `casks`\n- `CaskRecord` - GRDB `FetchableRecord`/`PersistableRecord`, decodes from `CaskDTO`\n- `CaskDatabaseService` - CRUD, FTS5 search (async), API sync\n\n**Cask engine** (`Applite/Core/CaskCore/`) — the `@Observable` runtime layer\n- `CaskViewModel` - `@Observable @MainActor` view model wrapping `CaskRecord` with runtime state (`isInstalled`, `isOutdated`, `progressState`)\n- `CaskViewModelRegistry` - Single-identity store; `viewModels(for:)` is get-or-create so the same cask shares one VM across views. **Identity is `fullToken` everywhere** — DB primary key, registry key, brew ops. The bare `token` is not unique (two taps can each ship a \"firefox\"), so it must never key anything; it's indexed for lookup only\n- `CaskDataLoader` - Orchestrates: `loadCatalogData()` (DB-only), `refreshInstalled()`/`refreshOutdated()` (brew CLI), `search(query:)` (FTS5). Defines `CategoryLoadResult` and `TapLoadResult`.\n- `CaskWarning` - Warning enum (deprecated/disabled/caveat)\n- `CaskProgressState`, `CaskLoadError` - install progress + load error types\n\n**Plain models** (`Applite/Core/Models/`)\n- `CaskDTO`, `CaskAdditionalInfo`, `BrewAnalytics` - decode-only DTOs for the Homebrew API/JSON\n- `Category`, `CategoryLoadResult+LocalizedName`, `TapLoadResult`, `SidebarItem`, `SortingOptions`\n\nOther `Core/` subfolders: `Core/Brew/` (brew CLI services + `BrewPaths`, `Shell`, `Installation/`), `Core/Preferences/`, `Core/Infrastructure/` (`AlertManager`, `AppPaths`, `SendNotification`, `MirrorEnvironment`, `NetworkProxyManager`, …).\n\n**CaskManager** (`Applite/Core/CaskCore/CaskManager.swift`)\n- Thin `@Observable @MainActor` coordinator owning `dataLoader`, `registry`, `brewService`\n- `categories: [CategoryLoadResult]` and `taps: [TapLoadResult]` populated after stage 1\n- `isResolvingInstalledState: Bool` is true during stage 2 (brew CLI); `isRefreshingCatalog: Bool` is true during a `forceSync` reload\n- \"Is brew usable\" has exactly one owner: `bootstrap.phase` (`HomebrewBootstrap.Phase`). `isBrewReady` / `needsSetupOverlay` derive from it, and a broken brew surfaces only as the setup overlay. Don't add a parallel flag — `CaskManager.hasBrokenInstall` and `BrokenInstallView` were removed for exactly that reason. A `BrewService` op that finds the path invalid calls `recoverBrew` (wired to `bootstrap.run()`) instead of reacting on its own\n- `alert: AlertManager` is the **main window's one alert surface** — brew failures, catalog/load failures and view-raised errors all queue in it, and `ContentView` presents it once at the window root via `.alertManager(_:)`. Rule: **one manager per window, bound at that window's root**; never inside a repeated view (binding it per cask card was the F5/P3-5 bug). Alerts carry their own buttons (`AppAlert.Action`), so nothing hand-rolls `.alert`. Windows that can't see that root (Settings' `UninstallView`) own a local one\n- `loadData(forceSync:)` is non-throwing — it path-validates, runs stage 1, then stage 2, surfacing any failure through `alert` (with Retry/Quit actions). The same entry point powers initial load, the ⌘R menu action, and the \"Refresh Catalog\" prompt in Settings\n- Forwards install/uninstall/update to `BrewService`\n\n**Brew services** (`Applite/Core/Brew/`)\n- `BrewService` - Brew CLI operations; tracks `activeTasks: [ActiveBrewTask]`\n- `InstalledCaskService` - Wraps `brew list --cask` and `brew outdated --cask` (the slow stage 2)\n\n**Views** — split across `App/` (entry + `Commands`), `Navigation/` (shell), `Components/` (generic reusable views), `AppViews/` (the shared cask \"app card\" cluster), `Features/<Screen>/` (one folder per screen), and `Windows/` (standalone windows)\n- `Navigation/` split into `ContentView` / `SidebarViews` / `DetailView`. `ContentView` is a `NavigationSplitView`; the detail closure picks `SearchView` (when `searchInput` is non-empty) or `DetailViews` (tab-driven), while a broken/installing brew is covered by the `ComponentsInstallView` overlay gated on `bootstrap.needsSetupOverlay`. The `.home` sidebar tab renders `DiscoverView` directly (no wrapper)\n- `selection: SidebarItem?` is optional. Typing in the search field stashes the current selection into `lastSelection` and clears `selection` so a sidebar tap can interrupt the search; tapping a sidebar item while a search is active clears `searchInput`; clearing the search (Esc) restores `lastSelection`. Two `onChange` guards (`!searchInput.isEmpty` / `selection == nil`) keep the watchers from looping\n- `Features/Search/SearchView` - Owns its own results state. Uses `.task(id: query)` with a 200ms `Task.sleep` for debounced live search; `ContentUnavailableView.search(text:)` for the empty state. Sort/filter are scoped here, not in ContentView\n- `Features/Search/SortingOptionsToolbar` - Toolbar shared by SearchView (sort + hide-unpopular + hide-disabled toggles)\n- `AppViews/` - App card display components (split across 8+ files). `AppliteAppView` (self-card in the installed list) reads the live app icon from `NSApplication.shared.applicationIconImage` so the new Icon Composer / Liquid Glass icon renders correctly\n- `Features/Settings/SettingsView+BrewSettingsView` - Shows a single fixed-height \"Refresh Catalog\" prompt at the bottom whenever the brew-path option or the \"Include Casks from Taps\" toggle differs from the baselines captured `.onAppear`. The button calls `caskManager.loadData(forceSync: true)` and resets the baselines on success. The old \"relaunch app\" flow was replaced\n- `Features/Bootstrap/` - `ComponentsInstallView` (the setup overlay) + `SetupStatusIcon`. Not an onboarding flow; it's a modal over the main window\n- `App/Commands.swift` - Menu bar commands. \"Refresh App Catalog\" lives in the Applite menu (⌘R) and invokes `caskManager.loadData(forceSync: true)`\n\n### External Data Sources\n\n- Homebrew Cask API: `https://formulae.brew.sh/api/cask.json`\n- Analytics API: `https://formulae.brew.sh/api/analytics/cask-install/365d.json`\n- Custom taps via `brew ruby` script (`Applite/Resources/brew-tap-cask-info.rb`), invoked by `CaskDataLoader.fetchTapDTOs`. The script no-ops `Homebrew::Trust.require_trusted_cask!` so metadata loads from already-tapped repos on Brew 6+ without requiring `brew trust` (Applite only reads metadata; real `brew install` still honors trust). It also injects `tap` and `full_token` into each entry because `FromPathLoader`'s `to_h` leaves them `nil`\n\n### Preferences\n\nUser settings stored via `@AppStorage` with keys defined in `Applite/Core/Preferences/Preferences.swift`.\n\n## Dependencies\n\n- **Sparkle** - Auto-updates\n- **Kingfisher** - Async image loading/caching\n- **GRDB.swift** - SQLite database\n- **ButtonKit**, **SwiftUI-Shimmer** - UI components\n\n## Code Patterns\n\n- `@Observable` + `@MainActor` for view models and managers (macOS 14+)\n- Async/await throughout; DB I/O uses GRDB's async API (`dbPool.read { ... }`/`.write { ... }`) — never block the main actor on disk\n- Prefer SwiftUI built-ins over hand-rolled equivalents (e.g. `ContentUnavailableView`)\n- Prefer Swift-native concurrency (e.g. `.task(id:)` for debounced cancellable work) over add-on packages where the native primitive suffices\n- **One view struct per file**; do not split an owned type across multiple `Type+View.swift` extension files. Genuine extensions on *external/stdlib* types (`Array+`, `String+`, `URL+`, `View+Modify`) live in `Extensions/` and are fine. View helpers tightly coupled to a parent's `@State` stay as `private` computed properties/methods in the parent's own file (e.g. `AppView`'s `actionsView`), not as a struct or a separate extension file\n- Two-stage data load: never block UI on `brew list --cask` / `brew outdated --cask`; let the registry update those flags reactively\n\n## Contributing Notes\n\n- For typos/minor bugs: PRs welcome directly\n- For larger changes: Open issue or discuss on Discord first\n- Project goal is simplicity for non-technical users; advanced features should not clutter main UI\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nApplite is a native macOS GUI application for Homebrew Casks, designed as an \"app store for third-party apps\" rather than a full Homebrew wrapper. Target audience is non-technical users who want simple app installation/management.\n\n- **Language**: Swift with SwiftUI (`@Observable`, `@MainActor`, async/await)\n- **Platform**: macOS 14+ (Apple Silicon and Intel)\n- **Build System**: Xcode with Swift Package Manager for dependencies. Uses Xcode 16+ **file-system synchronized groups** (folder-based project): the on-disk folder structure *is* the project structure. Adding, removing, moving, or renaming source files needs **no `project.pbxproj` edits** — just change the files on disk and Xcode picks them up automatically. Do not hand-edit the pbxproj for file management.\n- **Database**: GRDB.swift (SQLite) at `~/Library/Application Support/Applite/casks.sqlite`\n\n## Build & Run\n\nOpen `Applite.xcodeproj` in Xcode and build/run (⌘R). Dependencies resolve automatically via SPM.\n\n## Architecture\n\n### Data Flow\n\n1. **App Launch**: `ContentView.task(id: bootstrap.attempt)` calls `caskManager.bootstrapAndLoad()`\n2. **First Run**: `HomebrewBootstrap` installs/validates brew; while `bootstrap.needsSetupOverlay` is true, `ContentView` covers the window with the non-dismissable `ComponentsInstallView` modal (`Features/Bootstrap/`). There is no separate onboarding flow — the app opens straight into the main UI\n3. **Main UI**: `ContentView` with `NavigationSplitView` sidebar navigation\n4. **Data Loading**: `CaskManager.loadData()` runs in two stages — catalog (DB-only, instant) then brew CLI state (slow). The UI lights up after stage 1; installed/outdated state arrives reactively as stage 2 completes.\n\n### Key Components\n\nThe shared cask/brew engine lives under `Applite/Core/`, split into focused subfolders:\n\n**Persistence** (`Applite/Core/Database/`)\n- `AppDatabase` - Schema migrations, DatabasePool with WAL mode, FTS5 virtual table on `casks`\n- `CaskRecord` - GRDB `FetchableRecord`/`PersistableRecord`, decodes from `CaskDTO`\n- `CaskDatabaseService` - CRUD, FTS5 search (async), API sync\n\n**Cask engine** (`Applite/Core/CaskCore/`) — the `@Observable` runtime layer\n- `CaskViewModel` - `@Observable @MainActor` view model wrapping `CaskRecord` with runtime state (`isInstalled`, `isOutdated`, `progressState`)\n- `CaskViewModelRegistry` - Single-identity store; `viewModels(for:)` is get-or-create so the same cask shares one VM across views. **Identity is `fullToken` everywhere** — DB primary key, registry key, brew ops. The bare `token` is not unique (two taps can each ship a \"firefox\"), so it must never key anything; it's indexed for lookup only\n- `CaskDataLoader` - Orchestrates: `loadCatalogData()` (DB-only), `refreshInstalled()`/`refreshOutdated()` (brew CLI), `search(query:)` (FTS5). Defines `CategoryLoadResult` and `TapLoadResult`.\n- `CaskWarning` - Warning enum (deprecated/disabled/caveat)\n- `CaskProgressState`, `CaskLoadError` - install progress + load error types\n\n**Plain models** (`Applite/Core/Models/`)\n- `CaskDTO`, `CaskAdditionalInfo`, `BrewAnalytics` - decode-only DTOs for the Homebrew API/JSON\n- `Category`, `CategoryLoadResult+LocalizedName`, `TapLoadResult`, `SidebarItem`, `SortingOptions`\n\nOther `Core/` subfolders: `Core/Brew/` (brew CLI services + `BrewPaths`, `Shell`, `Installation/`), `Core/Preferences/`, `Core/Infrastructure/` (`AlertManager`, `AppPaths`, `SendNotification`, `MirrorEnvironment`, `NetworkProxyManager`, …).\n\n**CaskManager** (`Applite/Core/CaskCore/CaskManager.swift`)\n- Thin `@Observable @MainActor` coordinator owning `dataLoader`, `registry`, `brewService`\n- `categories: [CategoryLoadResult]` and `taps: [TapLoadResult]` populated after stage 1\n- `isResolvingInstalledState: Bool` is true during stage 2 (brew CLI); `isRefreshingCatalog: Bool` is true during a `forceSync` reload\n- \"Is brew usable\" has exactly one owner: `bootstrap.phase` (`HomebrewBootstrap.Phase`). `isBrewReady` / `needsSetupOverlay` derive from it, and a broken brew surfaces only as the setup overlay. Don't add a parallel flag — `CaskManager.hasBrokenInstall` and `BrokenInstallView` were removed for exactly that reason. A `BrewService` op that finds the path invalid calls `recoverBrew` (wired to `bootstrap.run()`) instead of reacting on its own\n- `alert: AlertManager` is the **main window's one alert surface** — brew failures, catalog/load failures and view-raised errors all queue in it, and `ContentView` presents it once at the window root via `.alertManager(_:)`. Rule: **one manager per window, bound at that window's root**; never inside a repeated view (binding it per cask card was the F5/P3-5 bug). Alerts carry their own buttons (`AppAlert.Action`), so nothing hand-rolls `.alert`. Windows that can't see that root (Settings' `UninstallView`) own a local one\n- `loadData(forceSync:)` is non-throwing — it path-validates, runs stage 1, then stage 2, surfacing any failure through `alert` (with Retry/Quit actions). The same entry point powers initial load, the ⌘R menu action, and the \"Refresh Catalog\" prompt in Settings\n- Forwards install/uninstall/update to `BrewService`\n\n**Brew services** (`Applite/Core/Brew/`)\n- `BrewService` - Brew CLI operations; tracks `activeTasks: [ActiveBrewTask]`\n- `InstalledCaskService` - Wraps `brew list --cask` and `brew outdated --cask` (the slow stage 2)\n\n**Views** — split across `App/` (entry + `Commands`), `Navigation/` (shell), `Components/` (generic reusable views), `AppViews/` (the shared cask \"app card\" cluster), `Features/<Screen>/` (one folder per screen), and `Windows/` (standalone windows)\n- `Navigation/` split into `ContentView` / `SidebarViews` / `DetailView`. `ContentView` is a `NavigationSplitView`; the detail closure picks `SearchView` (when `searchInput` is non-empty) or `DetailViews` (tab-driven), while a broken/installing brew is covered by the `ComponentsInstallView` overlay gated on `bootstrap.needsSetupOverlay`. The `.home` sidebar tab renders `DiscoverView` directly (no wrapper)\n- `selection: SidebarItem?` is optional. Typing in the search field stashes the current selection into `lastSelection` and clears `selection` so a sidebar tap can interrupt the search; tapping a sidebar item while a search is active clears `searchInput`; clearing the search (Esc) restores `lastSelection`. Two `onChange` guards (`!searchInput.isEmpty` / `selection == nil`) keep the watchers from looping\n- `Features/Search/SearchView` - Owns its own results state. Uses `.task(id: query)` with a 200ms `Task.sleep` for debounced live search; `ContentUnavailableView.search(text:)` for the empty state. Sort/filter are scoped here, not in ContentView\n- `Features/Search/SortingOptionsToolbar` - Toolbar shared by SearchView (sort + hide-unpopular + hide-disabled toggles)\n- `AppViews/` - App card display components (split across 8+ files). `AppliteAppView` (self-card in the installed list) reads the live app icon from `NSApplication.shared.applicationIconImage` so the new Icon Composer / Liquid Glass icon renders correctly\n- `Features/Settings/SettingsView+BrewSettingsView` - Shows a single fixed-height \"Refresh Catalog\" prompt at the bottom whenever the brew-path option or the \"Include Casks from Taps\" toggle differs from the baselines captured `.onAppear`. The button calls `caskManager.loadData(forceSync: true)` and resets the baselines on success. The old \"relaunch app\" flow was replaced\n- `Features/Bootstrap/` - `ComponentsInstallView` (the setup overlay) + `SetupStatusIcon`. Not an onboarding flow; it's a modal over the main window\n- `App/Commands.swift` - Menu bar commands. \"Refresh App Catalog\" lives in the Applite menu (⌘R) and invokes `caskManager.loadData(forceSync: true)`\n\n### External Data Sources\n\n- Homebrew Cask API: `https://formulae.brew.sh/api/cask.json`\n- Analytics API: `https://formulae.brew.sh/api/analytics/cask-install/365d.json`\n- Custom taps via `brew ruby` script (`Applite/Resources/brew-tap-cask-info.rb`), invoked by `CaskDataLoader.fetchTapDTOs`. The script no-ops `Homebrew::Trust.require_trusted_cask!` so metadata loads from already-tapped repos on Brew 6+ without requiring `brew trust` (Applite only reads metadata; real `brew install` still honors trust). It also injects `tap` and `full_token` into each entry because `FromPathLoader`'s `to_h` leaves them `nil`\n\n### Preferences\n\nUser settings stored via `@AppStorage` with keys defined in `Applite/Core/Preferences/Preferences.swift`.\n\n## Dependencies\n\n- **Sparkle** - Auto-updates\n- **Kingfisher** - Async image loading/caching\n- **GRDB.swift** - SQLite database\n- **ButtonKit**, **SwiftUI-Shimmer** - UI components\n\n## Code Patterns\n\n- `@Observable` + `@MainActor` for view models and managers (macOS 14+)\n- Async/await throughout; DB I/O uses GRDB's async API (`dbPool.read { ... }`/`.write { ... }`) — never block the main actor on disk\n- Prefer SwiftUI built-ins over hand-rolled equivalents (e.g. `ContentUnavailableView`)\n- Prefer Swift-native concurrency (e.g. `.task(id:)` for debounced cancellable work) over add-on packages where the native primitive suffices\n- **One view struct per file**; do not split an owned type across multiple `Type+View.swift` extension files. Genuine extensions on *external/stdlib* types (`Array+`, `String+`, `URL+`, `View+Modify`) live in `Extensions/` and are fine. View helpers tightly coupled to a parent's `@State` stay as `private` computed properties/methods in the parent's own file (e.g. `AppView`'s `actionsView`), not as a struct or a separate extension file\n- Two-stage data load: never block UI on `brew list --cask` / `brew outdated --cask`; let the registry update those flags reactively\n\n## Contributing Notes\n\n- For typos/minor bugs: PRs welcome directly\n- For larger changes: Open issue or discuss on Discord first\n- Project goal is simplicity for non-technical users; advanced features should not clutter main UI\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nApplite is a native macOS GUI application for Homebrew Casks, designed as an \"app store for third-party apps\" rather than a full Homebrew wrapper. Target audience is non-technical users who want simple app installation/management.\n\n- **Language**: Swift with SwiftUI (`@Observable`, `@MainActor`, async/await)\n- **Platform**: macOS 14+ (Apple Silicon and Intel)\n- **Build System**: Xcode with Swift Package Manager for dependencies. Uses Xcode 16+ **file-system synchronized groups** (folder-based project): the on-disk folder structure *is* the project structure. Adding, removing, moving, or renaming source files needs **no `project.pbxproj` edits** — just change the files on disk and Xcode picks them up automatically. Do not hand-edit the pbxproj for file management.\n- **Database**: GRDB.swift (SQLite) at `~/Library/Application Support/Applite/casks.sqlite`\n\n## Build & Run\n\nOpen `Applite.xcodeproj` in Xcode and build/run (⌘R). Dependencies resolve automatically via SPM.\n\n## Architecture\n\n### Data Flow\n\n1. **App Launch**: `ContentView.task(id: bootstrap.attempt)` calls `caskManager.bootstrapAndLoad()`\n2. **First Run**: `HomebrewBootstrap` installs/validates brew; while `bootstrap.needsSetupOverlay` is true, `ContentView` covers the window with the non-dismissable `ComponentsInstallView` modal (`Features/Bootstrap/`). There is no separate onboarding flow — the app opens straight into the main UI\n3. **Main UI**: `ContentView` with `NavigationSplitView` sidebar navigation\n4. **Data Loading**: `CaskManager.loadData()` runs in two stages — catalog (DB-only, instant) then brew CLI state (slow). The UI lights up after stage 1; installed/outdated state arrives reactively as stage 2 completes.\n\n### Key Components\n\nThe shared cask/brew engine lives under `Applite/Core/`, split into focused subfolders:\n\n**Persistence** (`Applite/Core/Database/`)\n- `AppDatabase` - Schema migrations, DatabasePool with WAL mode, FTS5 virtual table on `casks`\n- `CaskRecord` - GRDB `FetchableRecord`/`PersistableRecord`, decodes from `CaskDTO`\n- `CaskDatabaseService` - CRUD, FTS5 search (async), API sync\n\n**Cask engine** (`Applite/Core/CaskCore/`) — the `@Observable` runtime layer\n- `CaskViewModel` - `@Observable @MainActor` view model wrapping `CaskRecord` with runtime state (`isInstalled`, `isOutdated`, `progressState`)\n- `CaskViewModelRegistry` - Single-identity store; `viewModels(for:)` is get-or-create so the same cask shares one VM across views. **Identity is `fullToken` everywhere** — DB primary key, registry key, brew ops. The bare `token` is not unique (two taps can each ship a \"firefox\"), so it must never key anything; it's indexed for lookup only\n- `CaskDataLoader` - Orchestrates: `loadCatalogData()` (DB-only), `refreshInstalled()`/`refreshOutdated()` (brew CLI), `search(query:)` (FTS5). Defines `CategoryLoadResult` and `TapLoadResult`.\n- `CaskWarning` - Warning enum (deprecated/disabled/caveat)\n- `CaskProgressState`, `CaskLoadError` - install progress + load error types\n\n**Plain models** (`Applite/Core/Models/`)\n- `CaskDTO`, `CaskAdditionalInfo`, `BrewAnalytics` - decode-only DTOs for the Homebrew API/JSON\n- `Category`, `CategoryLoadResult+LocalizedName`, `TapLoadResult`, `SidebarItem`, `SortingOptions`\n\nOther `Core/` subfolders: `Core/Brew/` (brew CLI services + `BrewPaths`, `Shell`, `Installation/`), `Core/Preferences/`, `Core/Infrastructure/` (`AlertManager`, `AppPaths`, `SendNotification`, `MirrorEnvironment`, `NetworkProxyManager`, …).\n\n**CaskManager** (`Applite/Core/CaskCore/CaskManager.swift`)\n- Thin `@Observable @MainActor` coordinator owning `dataLoader`, `registry`, `brewService`\n- `categories: [CategoryLoadResult]` and `taps: [TapLoadResult]` populated after stage 1\n- `isResolvingInstalledState: Bool` is true during stage 2 (brew CLI); `isRefreshingCatalog: Bool` is true during a `forceSync` reload\n- \"Is brew usable\" has exactly one owner: `bootstrap.phase` (`HomebrewBootstrap.Phase`). `isBrewReady` / `needsSetupOverlay` derive from it, and a broken brew surfaces only as the setup overlay. Don't add a parallel flag — `CaskManager.hasBrokenInstall` and `BrokenInstallView` were removed for exactly that reason. A `BrewService` op that finds the path invalid calls `recoverBrew` (wired to `bootstrap.run()`) instead of reacting on its own\n- `alert: AlertManager` is the **main window's one alert surface** — brew failures, catalog/load failures and view-raised errors all queue in it, and `ContentView` presents it once at the window root via `.alertManager(_:)`. Rule: **one manager per window, bound at that window's root**; never inside a repeated view (binding it per cask card was the F5/P3-5 bug). Alerts carry their own buttons (`AppAlert.Action`), so nothing hand-rolls `.alert`. Windows that can't see that root (Settings' `UninstallView`) own a local one\n- `loadData(forceSync:)` is non-throwing — it path-validates, runs stage 1, then stage 2, surfacing any failure through `alert` (with Retry/Quit actions). The same entry point powers initial load, the ⌘R menu action, and the \"Refresh Catalog\" prompt in Settings\n- Forwards install/uninstall/update to `BrewService`\n\n**Brew services** (`Applite/Core/Brew/`)\n- `BrewService` - Brew CLI operations; tracks `activeTasks: [ActiveBrewTask]`\n- `InstalledCaskService` - Wraps `brew list --cask` and `brew outdated --cask` (the slow stage 2)\n\n**Views** — split across `App/` (entry + `Commands`), `Navigation/` (shell), `Components/` (generic reusable views), `AppViews/` (the shared cask \"app card\" cluster), `Features/<Screen>/` (one folder per screen), and `Windows/` (standalone windows)\n- `Navigation/` split into `ContentView` / `SidebarViews` / `DetailView`. `ContentView` is a `NavigationSplitView`; the detail closure picks `SearchView` (when `searchInput` is non-empty) or `DetailViews` (tab-driven), while a broken/installing brew is covered by the `ComponentsInstallView` overlay gated on `bootstrap.needsSetupOverlay`. The `.home` sidebar tab renders `DiscoverView` directly (no wrapper)\n- `selection: SidebarItem?` is optional. Typing in the search field stashes the current selection into `lastSelection` and clears `selection` so a sidebar tap can interrupt the search; tapping a sidebar item while a search is active clears `searchInput`; clearing the search (Esc) restores `lastSelection`. Two `onChange` guards (`!searchInput.isEmpty` / `selection == nil`) keep the watchers from looping\n- `Features/Search/SearchView` - Owns its own results state. Uses `.task(id: query)` with a 200ms `Task.sleep` for debounced live search; `ContentUnavailableView.search(text:)` for the empty state. Sort/filter are scoped here, not in ContentView\n- `Features/Search/SortingOptionsToolbar` - Toolbar shared by SearchView (sort + hide-unpopular + hide-disabled toggles)\n- `AppViews/` - App card display components (split across 8+ files). `AppliteAppView` (self-card in the installed list) reads the live app icon from `NSApplication.shared.applicationIconImage` so the new Icon Composer / Liquid Glass icon renders correctly\n- `Features/Settings/SettingsView+BrewSettingsView` - Shows a single fixed-height \"Refresh Catalog\" prompt at the bottom whenever the brew-path option or the \"Include Casks from Taps\" toggle differs from the baselines captured `.onAppear`. The button calls `caskManager.loadData(forceSync: true)` and resets the baselines on success. The old \"relaunch app\" flow was replaced\n- `Features/Bootstrap/` - `ComponentsInstallView` (the setup overlay) + `SetupStatusIcon`. Not an onboarding flow; it's a modal over the main window\n- `App/Commands.swift` - Menu bar commands. \"Refresh App Catalog\" lives in the Applite menu (⌘R) and invokes `caskManager.loadData(forceSync: true)`\n\n### External Data Sources\n\n- Homebrew Cask API: `https://formulae.brew.sh/api/cask.json`\n- Analytics API: `https://formulae.brew.sh/api/analytics/cask-install/365d.json`\n- Custom taps via `brew ruby` script (`Applite/Resources/brew-tap-cask-info.rb`), invoked by `CaskDataLoader.fetchTapDTOs`. The script no-ops `Homebrew::Trust.require_trusted_cask!` so metadata loads from already-tapped repos on Brew 6+ without requiring `brew trust` (Applite only reads metadata; real `brew install` still honors trust). It also injects `tap` and `full_token` into each entry because `FromPathLoader`'s `to_h` leaves them `nil`\n\n### Preferences\n\nUser settings stored via `@AppStorage` with keys defined in `Applite/Core/Preferences/Preferences.swift`.\n\n## Dependencies\n\n- **Sparkle** - Auto-updates\n- **Kingfisher** - Async image loading/caching\n- **GRDB.swift** - SQLite database\n- **ButtonKit**, **SwiftUI-Shimmer** - UI components\n\n## Code Patterns\n\n- `@Observable` + `@MainActor` for view models and managers (macOS 14+)\n- Async/await throughout; DB I/O uses GRDB's async API (`dbPool.read { ... }`/`.write { ... }`) — never block the main actor on disk\n- Prefer SwiftUI built-ins over hand-rolled equivalents (e.g. `ContentUnavailableView`)\n- Prefer Swift-native concurrency (e.g. `.task(id:)` for debounced cancellable work) over add-on packages where the native primitive suffices\n- **One view struct per file**; do not split an owned type across multiple `Type+View.swift` extension files. Genuine extensions on *external/stdlib* types (`Array+`, `String+`, `URL+`, `View+Modify`) live in `Extensions/` and are fine. View helpers tightly coupled to a parent's `@State` stay as `private` computed properties/methods in the parent's own file (e.g. `AppView`'s `actionsView`), not as a struct or a separate extension file\n- Two-stage data load: never block UI on `brew list --cask` / `brew outdated --cask`; let the registry update those flags reactively\n\n## Contributing Notes\n\n- For typos/minor bugs: PRs welcome directly\n- For larger changes: Open issue or discuss on Discord first\n- Project goal is simplicity for non-technical users; advanced features should not clutter main UI\n","category":"root","tokens":2509}]}