This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Applite 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.
@Observable, @MainActor, async/await)project.pbxproj edits — just change the files on disk and Xcode picks them up automatically. Do not hand-edit the pbxproj for file management.~/Library/Application Support/Applite/casks.sqliteOpen Applite.xcodeproj in Xcode and build/run (⌘R). Dependencies resolve automatically via SPM.
ContentView.task(id: bootstrap.attempt) calls caskManager.bootstrapAndLoad()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 UIContentView with NavigationSplitView sidebar navigationCaskManager.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.The shared cask/brew engine lives under Applite/Core/, split into focused subfolders:
Persistence (Applite/Core/Database/)
AppDatabase - Schema migrations, DatabasePool with WAL mode, FTS5 virtual table on casksCaskRecord - GRDB FetchableRecord/PersistableRecord, decodes from CaskDTOCaskDatabaseService - CRUD, FTS5 search (async), API syncCask engine (Applite/Core/CaskCore/) — the @Observable runtime layer
CaskViewModel - @Observable @MainActor view model wrapping CaskRecord with runtime state (isInstalled, isOutdated, progressState)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 onlyCaskDataLoader - Orchestrates: loadCatalogData() (DB-only), refreshInstalled()/refreshOutdated() (brew CLI), search(query:) (FTS5). Defines CategoryLoadResult and TapLoadResult.CaskWarning - Warning enum (deprecated/disabled/caveat)CaskProgressState, CaskLoadError - install progress + load error typesPlain models (Applite/Core/Models/)
CaskDTO, CaskAdditionalInfo, BrewAnalytics - decode-only DTOs for the Homebrew API/JSONCategory, CategoryLoadResult+LocalizedName, TapLoadResult, SidebarItem, SortingOptionsOther Core/ subfolders: Core/Brew/ (brew CLI services + BrewPaths, Shell, Installation/), Core/Preferences/, Core/Infrastructure/ (AlertManager, AppPaths, SendNotification, MirrorEnvironment, NetworkProxyManager, …).
CaskManager (Applite/Core/CaskCore/CaskManager.swift)
@Observable @MainActor coordinator owning dataLoader, registry, brewServicecategories: [CategoryLoadResult] and taps: [TapLoadResult] populated after stage 1isResolvingInstalledState: Bool is true during stage 2 (brew CLI); isRefreshingCatalog: Bool is true during a forceSync reloadbootstrap.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 ownalert: 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 oneloadData(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 SettingsBrewServiceBrew services (Applite/Core/Brew/)
BrewService - Brew CLI operations; tracks activeTasks: [ActiveBrewTask]InstalledCaskService - Wraps brew list --cask and brew outdated --cask (the slow stage 2)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)
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)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 loopingFeatures/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 ContentViewFeatures/Search/SortingOptionsToolbar - Toolbar shared by SearchView (sort + hide-unpopular + hide-disabled toggles)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 correctlyFeatures/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 replacedFeatures/Bootstrap/ - ComponentsInstallView (the setup overlay) + SetupStatusIcon. Not an onboarding flow; it’s a modal over the main windowApp/Commands.swift - Menu bar commands. “Refresh App Catalog” lives in the Applite menu (⌘R) and invokes caskManager.loadData(forceSync: true)https://formulae.brew.sh/api/cask.jsonhttps://formulae.brew.sh/api/analytics/cask-install/365d.jsonbrew 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 nilUser settings stored via @AppStorage with keys defined in Applite/Core/Preferences/Preferences.swift.
@Observable + @MainActor for view models and managers (macOS 14+)dbPool.read { ... }/.write { ... }) — never block the main actor on diskContentUnavailableView).task(id:) for debounced cancellable work) over add-on packages where the native primitive sufficesType+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 filebrew list --cask / brew outdated --cask; let the registry update those flags reactively