Skip to content

Latest commit

 

History

History
372 lines (286 loc) · 36.8 KB

File metadata and controls

372 lines (286 loc) · 36.8 KB

MiniStore — Claude Development Guide

Caveman Mode — Full

Response style: drop articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments ok. Shorter synonyms preferred. Technical terms stay precise. Structure: [thing] [action] [reason]. [next step]. Suspend for security warnings, irreversible-action confirmations, multi-step sequences where compression risks misunderstanding. Code, commits, PRs always standard writing — caveman never applies there.


Headroom Mode — Token Frugality

Standing directive, peer to Caveman, modeled on the Headroom context-compression plugin. Goal: spend the fewest tokens that still solve the task at full fidelity. Applies to this agent and every subagent/tool — carry a one-line Headroom reminder into subagent prompts.

  • Read narrowly. Prefer Grep/Glob and targeted line-range Reads over whole-file reads. Never re-read a file already in context, and never re-read a file you just edited to "verify" — Edit/Write fail loudly if they don't apply.
  • Slice, don't dump. For large files or tool output, use head/tail/jq/byte-ranges to reach the relevant span; quote only the lines that carry the answer.
  • Batch. Issue independent tool calls together in one turn rather than serially.
  • Delegate heavy reading. Fan out broad searches and multi-file audits to subagents (e.g. Explore); keep their conclusions in the main thread, not the raw file contents they waded through.
  • Output economy. Relay the result and the few lines that prove it. Don't echo large command output, restate established context, or recap prior turns.

Suspend only where compression would drop load-bearing detail: full error text needed to diagnose, security warnings, irreversible-action confirmations, multi-step sequences. (Caveman governs prose terseness; Headroom governs how much you read and emit.)


MiniStore is a fork of SideStore/AltStore: a self-hosted iOS app store that lets users sideload apps using only their Apple ID. No VPN integration is present. Device communication over Wi-Fi is handled by minimuxer (a Rust library) bridged through the SideStore/ Swift layer (MinimuxerWrapper.swift, IfManager.swift).


Claude Permissions

Claude may edit CLAUDE.md and any file under .claude/ without asking for confirmation. These are documentation and intelligence files — not app code — and keeping them current is a standing instruction. All other permission rules (no force-push, no destructive ops without confirmation, etc.) remain in effect.


Intelligence File Index

File What it covers
.claude/map.md Every source file in every target — read before touching an unfamiliar file
.claude/patterns.md Recurring implementation patterns (operations, CoreData, notifications, UI)
.claude/decisions.md Architecture Decision Records — the why behind non-obvious choices
.claude/gotchas.md Known landmines, silent failures, and non-obvious behaviours
.claude/regression-watch.md Load-bearing invariants — change one and you reintroduce a fixed bug. Read before touching operations/refresh/nav-bar/backup code
.claude/github-info.md gh CLI syntax, flags, release management, Actions patterns
.claude/swift-isp.md Swift ISP, protocol-oriented design, performance, reliability, Swift 6.2 language features
.claude/swift6-compat.md Swift 6/6.2 strict-concurrency errors, warnings, and new language features
.claude/xcode26.md Xcode 26 build system, iOS 26 UIKit/Liquid Glass, Swift 6.2, new APIs, deprecations
.claude/sidestore-delta.md What MiniStore changed vs upstream SideStore — read before merging upstream
.claude/plugin_index.md Registry of every active Claude Code plugin/skill/agent
.claude/plugins/*.md Per-plugin reference docs (caveman, headroom, taste, impeccable)
.claude/update_log.md Change log for intelligence file updates

Repo Layout

AltStore/           Main app target (Swift + ObjC)
  Browse/           Source browser tab
  My Apps/          Installed apps tab + expiry countdown
  Managing Apps/    AppManager.swift — install / refresh / revoke operations
  Operations/       NSOperation subclasses for each async task
  Components/       Shared UI cells, cards, reusable views
  Settings/         App settings UI
AltStoreCore/       Shared framework (models, extensions, CoreData stack)
AltWidget/          WidgetKit extension
AltBackup/          Companion backup app (bundled as IPA inside main app)
SideStore/          minimuxer + IfManager bridging layer
  MinimuxerWrapper.swift   Swift bridge to minimuxer Rust library
  IfManager.swift          Network interface introspection
Shared/             ObjC/C headers shared between targets
xcconfigs/          Per-target xcconfig files (all inherit Build.xcconfig)
Build.xcconfig      Single source of truth for version, bundle IDs, team ID
side.json           MiniStore self-update source (stable) — hosts MiniStore itself for
                    in-app updates ONLY. See "side.json disambiguation" below.
sidenightly.json    MiniStore self-update source (nightly) — activated by "Enable Beta
                    Updates" toggle. Same structure as side.json, nightly track only.
scripts/ci/         Python CI helpers (workflow.py, release notes, metadata)
.github/workflows/  pr.yml / nightly.yml / stable.yml / attach_build_products.yml

Note: There is NO TunnelProv/ directory in this repo. MiniStore has no NetworkExtension target. Do not create one.

Full file-by-file reference: .claude/map.md — covers every Swift, ObjC, and resource file in all targets.


Targets

Target Bundle ID (Release) Entitlements
SideStore (main app) com.SideStore.SideStore AltStoreFree.entitlements (CI) / AltStore.entitlements (paid)
AltWidgetExtension com.SideStore.SideStore.AltWidgetExtension AltWidgetExtension.entitlements
AltBackup separate IPA embedded in main app bundle AltBackup.entitlements

Debug builds append .$(DEVELOPMENT_TEAM) to every bundle ID (e.g. com.SideStore.SideStore.S32Z3HMYVQ). Never hard-code bundle IDs.


Build System

Local builds:

make build               # xcodebuild archive → SideStore.xcarchive
make fakesign            # ldid fake-sign all extensions using Release entitlements
make ipa                 # package SideStore.xcarchive → SideStore.ipa
make build-and-test      # build + run unit tests on iPhone 17 Pro / iOS 26 simulator
make clean               # remove SideStore.ipa and build/

CodeSigning.xcconfig (gitignored, see CodeSigning.xcconfig.sample) overrides DEVELOPMENT_TEAM, CODE_SIGN_ENTITLEMENTS, and signing identity for local paid builds. CI always uses AltStoreFree.entitlements with CODE_SIGNING_ALLOWED=NO.

Key xcconfig variables:

  • MARKETING_VERSION — user-visible version (e.g. 0.6.3), set in Build.xcconfig
  • DEVELOPMENT_TEAM — Apple team ID, default S32Z3HMYVQ for CI
  • ORG_IDENTIFIER — reverse-DNS prefix, default com.SideStore
  • APP_GROUP_IDENTIFIER — shared app group (com.SideStore.SideStore in release)

CI Pipelines

Workflow Trigger Output
pr.yml PR open / push to PR branch (Swift/config files only) MiniStore-pr.N+sha.ipa attached to PR
nightly.yml Cron 00:00 UTC + manual dispatch Nightly release if commits since last success
stable.yml Tag push or manual dispatch (version_override input) Stable release
attach_build_products.yml On completion of "Pull Request MiniStore Build" Artifact links posted to PR

All pipelines run on macos-26 / Xcode 26.2. Version strings are stamped by scripts/ci/workflow.py set-marketing-version before building.

PR builds trigger on changes to: **/*.swift, **/*.m, **/*.h, Makefile, **/*.xcconfig, AltStore.xcodeproj/**, Dependencies/**, scripts/**, .github/workflows/pr.yml


Git Workflow

Branch naming

One branch per session, persistent across resumes.

  • Resumed session: .claude/.current-branch exists → hook auto-checkouts it → continue on that branch. Do not create a new branch.
  • New session: no .current-branch file → create one branch using claude/<short-description> (no session ID), then immediately run:
    echo "claude/<branch-name>" > .claude/.current-branch
    All work for that session stays on that one branch.
  • Starting genuinely new work in a new session: create a new branch, overwrite .current-branch.
  • .claude/.current-branch is gitignored — it is local state only, never committed.
  • Never push directly to main.

Push

git push -u origin claude/<branch>

Default branch

The default branch is develop. Never target main — it does not exist in this repo.

Self-update raw URLs (updated 2026-06-20): the self-update URLs hardcoded in Source.swift now point to this repo — The-Big-Mini/MiniStore/develop/side.json (and sidenightly.json). The separate MiniStore-Public mirror was retired; this repo is public and serves the JSON directly from develop. Existing installs migrate from the old MiniStore-Public URLs automatically (see resolveOrCreateMiniStoreSource legacy/known-bad lists).

Retry policy

Exponential backoff on network errors only: 2 s → 4 s → 8 s → 16 s. Never retry on 403 or 422.

Commit message format

type(scope): short imperative description

Why this change is needed.

https://claude.ai/code/session_<id>

Types: fix feat refactor docs chore test


Source Management

MiniStore seeds two built-in sources at launch (in DatabaseManager.prepareDatabase()):

Source URL Purpose
Mini's Repo https://OofMini.github.io/Minis-Repo/mini.json Personal curated app source
MiniStore Updates (stable) https://raw.githubusercontent.com/The-Big-Mini/MiniStore/develop/side.json Self-update source — stable channel
MiniStore Updates (nightly) https://raw.githubusercontent.com/The-Big-Mini/MiniStore/develop/sidenightly.json Self-update source — nightly channel

The self-update source uses two separate JSON files, each containing a single releaseChannels track:

File Track Who sees it
side.json stable Everyone (default)
sidenightly.json nightly Users who enable "Beta Updates" in Settings

When the user toggles "Enable Beta Updates" in Settings, SettingsViewController.switchMiniStoreUpdateSource() calls Source.setSourceURL() on the CoreData record to switch it between the two URLs. Source.fetchMiniStoreUpdateSource() tries both identifiers (stable and nightly) so the lookup always succeeds regardless of which was last active.

Both self-update sources are excluded from the Sources tab via a fetch predicate that filters out both Source.stableUpdateIdentifier and Source.nightlyUpdateIdentifier.

SideStore official source removal

On first launch, DatabaseManager.prepareDatabase() deletes any source with identifier sidestore.io/apps-v2.json. This cleans up the old SideStore source for users migrating from SideStore. The deletion is intentional and permanent.

Migration policies

Two CoreData migration policies redirect old SideStore source URLs:

  • Source11To17MigrationPolicy — handles old apps.sidestore.io URL → altStoreSourceURL
  • Source17To17_1MigrationPolicy — handles old apps.sidestore.io URL → altStoreSourceURL

These must be kept — users upgrading from old SideStore versions need them.


side.json Disambiguation

There are two completely separate JSON files that could be confused.

Self-update source (side.json) Personal app source (mini.json)
Purpose Self-update only — lists MiniStore (the app) so it can update itself Personal curated app source — lists EeveeSpotify, X, YTLite, etc.
Authored in The-Big-Mini/MiniStore (this repo) OofMini/Minis-Repo (separate repo)
Served from The-Big-Mini/MiniStore (this repo, develop branch — hardcoded in Source.swift) OofMini.github.io (GitHub Pages)
URL https://raw.githubusercontent.com/The-Big-Mini/MiniStore/develop/side.json https://OofMini.github.io/Minis-Repo/mini.json
Who consumes it MiniStore reads it to detect new versions of itself Any AltStore-compatible client the user adds it to
Contents Single release channel per file (stable in side.json, nightly in sidenightly.json) Multiple apps: EeveeSpotify, X, YTLite, etc.
Do NOT Add third-party apps to this file Assume this relates to MiniStore self-updates

Pending Work

Known TODOs in codebase

File Line Description Priority
AltStoreCore/Model/InstalledApp.swift 129 Version comparison when multiple tracks are independent (acknowledged limitation, inherited SideStore logic) Medium
AltStoreCore/Model/StoreApp.swift 622 Support multiple device types in screenshot filtering (inherited SideStore feature work) Medium
AltStoreCore/Model/Source.swift 272 Support alternate source URLs in recommended-source check (inherited SideStore feature work) Medium
AltStoreCore/Model/NewsItem.swift 78 Move app-specific news to appEntity — large inherited refactor, deferred Low

Ongoing

  • Broad bug audit complete — all major source files have been audited. See .claude/update_log.md for the full audit history. MyAppsViewController, DatabaseManager, DatabaseManager+Async.swift, Sources/ tab VCs, and AppDetailCollectionViewController were audited and found clean (2026-05-10).
  • print() → Logger migration complete — ALL targets (2026-06-09): Main app, AltStoreCore, AltWidget, and SideStore/Utils/ use Logger.main.*. SideStore/MinimuxerWrapper.swift (category MinimuxerBridge), SideStore/EMProxyWrapper.swift (category EMProxyBridge, simulator-only no-op stubs), Shared/Connections + Shared/Server Protocol (categories Connections/ServerProtocol), and AltBackup/ (category AltBackup) use file-private Logger(subsystem: "com.rileytestut.AltStore", category:) directly — these targets have no AltStoreCore dependency, so they share the hardcoded subsystem string for unified Console filtering. Only remaining prints: SideStore/Tests/ (intentional).
  • Dead code + incorrect pattern cleanup complete (2026-05-31): All Settings VCs use cross-dissolve animation for OLED/accent reloadData. Retain cycles in AdvancedSettingsViewController UIAlertAction closures fixed. Expiry notification cancel bug fixed (prefix-filter instead of wrong single ID). All 5 experimental features confirmed fully wired into production code.
  • RSTAsyncBlockOperation operation.finish() audit complete (2026-06-03): Systemic missing finish() on success/failure paths in all prefetch handlers. Fixed in: BrowseViewController, NewsViewController, SourcesViewController, SourceDetailContentViewController, AddSourceViewController (×2), AppCardCollectionViewCell, AppScreenshotsViewController, PreviewAppScreenshotsViewController, ErrorLogViewController (×4 paths including nil-context guard). All operations in MyAppsViewController and AppManager already called finish() correctly.
  • Error-surfacing audit complete (2026-06-09): Systemic weak zone found at entry points (URL-scheme handlers, file-open handlers, document-picker imports) using guard ... try? ... else { return } — failures vanished with no log or UI. Fixed: Sources "Refresh All" per-source failures (aggregate toast + log), .sideconf account import (toast + authenticate result handling), pairing-file open (toast + log), malformed appbackupresponse failure deep links (now deliver OperationError.unknownResult instead of letting BackupAppOperation time out), AppDelegate pairing deep link "urlName" key bug (keys are lowercased — handler could never match), AltBackup eaten backup result (now fires local completion notification), KeychainItem getter/setter silent error swallowing (do/catch + log, nil semantics kept), ConsoleLog silent stderr fallback, beta-updates toggle revert toast, badge-count + prewarm fetch logging. Core operation pipeline confirmed clean (errors propagate to finish(.failure) or log+toast).
  • Extended bug audit complete (2026-06-03): AppPermission17To17_1MigrationPolicy left _permission nil for unknown type strings — added empty-string sentinel. StoreApp11To17_1 and StoreApp17To17_1 migration policies passed untyped Any? values directly to non-optional @NSManaged String properties — added as? String ?? "" coercion. OperationError.maximumAppIDLimitReached force-unwrapped DateComponentsFormatter.string(from:) which returns nil when expiry is past. UIColor+Hex scanInt32 overflowed on ARGB values with alpha ≥ 0x80. ProcessInfo+AltStore BuildVersion.< nil-suffix comparison inverted (GM sorted before beta). super.init() missing in BackupController and ActiveAppsTimelineProvider. CI build failure (Unicode curly quotes in ReviewPermissionsViewController) fixed. AltWidget/AppsTimelineProvider allSatisfy condition inverted — healthy widgets got relevance 0. Countdown.swift numberOfDays < 0 dead code — capsule shape never shown for expired apps. ChangelogViewController retain cycle — resolved by full SwiftUI rewrite (2026-06-11).

SwiftUI migration

Active migration from UIKit to SwiftUI (started 2026-06-11). Goal: full departure from UIKit. Strategy:

  • New screens: write in SwiftUI, host via SettingsHostingController (settings stack) or UIHostingController elsewhere
  • Existing UIKit screens: rewrite incrementally, most contained first (Settings screens → tabs last)

Round 2 completed (2026-06-12): All five tabs (SourcesView, UpdatesView, MyAppsView+MyAppsViewModel, BrowseView, NewsView); AppIDsView (was AppIDsViewController — 30 s watchdog + generation counter preserved, orphaned Main.storyboard scene + dead unwindToMyAppsViewController IBAction removed); ReviewPermissionsView (was ReviewPermissionsViewController — same completionHandler contract with VerifyAppOperation); SourceDetailView (was SourceDetailViewController + SourceDetailContentViewController + SourceHeaderView/xib + NewsCollectionViewCell/xib — scroll-driven blur/nav-fade rebuilt in SwiftUI; nav chrome (icon+name title view, ADD/REMOVE PillButton, iOS 26 glass-capsule workaround) lives in SourceDetailHostingController; 4 Sources.storyboard scenes + showSourceDetails segue removed; News/Browse hosting controllers gained programmatic init(source:) for the View All pushes).

  • Gotcha (round 2): SwiftUI .toolbar inside a CHILD UIHostingController (addChild embed) never reaches the nav bar — the bar reads the parent VC's navigationItem. Create bar buttons in UIKit on the VC that sits on the nav stack (MyAppsViewController sideload “+”). Direct hosting-controller subclasses on the stack (Browse/News/Sources/Updates) are unaffected.
  • Pattern (round 2): rows wrap AppBannerView via UIViewRepresentable for exact visual parity; a tap recognizer whose delegate returns !(touch.view is UIControl) lets the pill button receive taps while the row navigates. withObservationTracking re-arm loop drives UIKit nav chrome from @Observable models.
  • Round 2 addendum: AddSourceView (was AddSourceViewController — Combine debounce pipeline became a cancellable Task with 200 ms sleep; +/✓ staging keyed by source identifier; both sheet storyboard scenes removed, presentAddSource builds ForwardingNavigationController(rootViewController: AddSourceHostingController()) programmatically; SourceComponents.swift + AddSourceTextFieldCell deleted). Sources.storyboard now contains only the two tab scenes.
  • Round 4 completed (2026-06-13): Authentication flow fully migrated. AuthenticationView (+ AuthenticationModel), SelectTeamView, InstructionsView, RefreshAltStoreView (+ RefreshAltStoreModel) and their hosting controllers replace AuthenticationViewController, SelectTeamViewController, InstructionsViewController, RefreshAltStoreViewController and the entire Authentication.storyboard. AuthenticationOperation now builds every screen — and the modal nav controller — programmatically (UINavigationController(navigationBarClass: NavigationBar.self, ...) reproducing the storyboard's opaque-SettingsBackground/white-title appearance). Contracts preserved verbatim: authenticationHandler/completionHandler, the requiresTwoFactorAuthentication spinner-stop, the white "Failed to Log In" toast, SelectTeam's nil-handler-before-resume, RefreshAltStore's determinate Progress.fractionCompleted KVO → circular ProgressView and Try Again/Refresh Later alert. InsetGroupTableViewCell + SettingsHeaderFooterView (+ .xib) deleted — they were only used by SelectTeamViewController.
  • Remaining UIKit: LaunchViewController (322, bootstrap), TabBarController/nav controllers/ToastView (infrastructure — stays by design). The UIKit→SwiftUI screen migration is otherwise complete.
  • Parity audit (2026-06-16): every migrated screen diffed against its git-historical UIKit original (Detail, all 5 tabs, auth flow, all Settings). Fixes shipped: App Detail AppDetailHostingController was missing navigationItem.largeTitleDisplayMode = .never (large-title bar covered the back button + banner and showed a glass ghost — the reported "no back button / ghost +/frozen parallax" triple; see gotchas.md "largeTitleDisplayMode nav-bar parity landmine"); overscroll blur was inverted; back chevron now app.tintColor. Auth RefreshAltStore on-screen "Refresh Later" now always finishes .cancelled (alert keeps forwarding the error); Instructions step labels restored minimumScaleFactor(0.5); sign-in field .emailAddress. My Apps expiry countdown refreshes on tab re-selection (model.refreshExpiryClock() in viewIsAppearing); installed-app screenshot prewarm restored. AddSource re-subscribes to didAddSource/didRemoveSource (button +/✓ state was stale) + typed-URL .altPrimary color. Settings Beta Testing icon back to .altPrimary; ErrorLog "Show More" now gated on measured render height, not char count. Audited-clean (no critical): Source Detail, Sources/Updates/Browse, ReviewPermissions (completionHandler contract), AppIDs (watchdog+generation), BetaTesting (toggle-order invariant), ExperimentalFeatures, TabOrder, NetworkDiagnostics, RefreshingApps, WhatsNew, RefreshAttempts, Licenses, AccentColor/Display/TechThings/Advanced/MiniStoreSigning/Developer/AltAppIcons. Known-accepted residuals (SwiftUI-equivalent / improvement, not regressions): News shows "No News" on empty-success (original blank); some sibling settings screens dropped now-inert OLED/accent crossfade observers; auth scroll-to-top-after-attempt relies on SwiftUI keyboard avoidance.
  • Round 3 completed (2026-06-12): AppDetailView + AppDetailHostingController (was AppViewController, AppContentViewController, AppDetailCollectionViewController, AppContentViewControllerCells, AppScreenshotsViewController, PreviewAppScreenshotsViewController, dead HeaderContentViewController — ~1,800 lines → 905 lines). Parallax scroll rebuilt with GeometryReader+PreferenceKey; blur overlay driven by scroll fraction; UnevenRoundedRectangle content card corners flatten as nav bar reveals. CollapsingMarkdownView wrapped via UIViewRepresentable with sizeThatFits + @Binding sizingTick for correct dynamic height after collapse toggle. AppBannerView wrapped as UIViewRepresentable with Coordinator observing CoreData/foreground/OLED notifications. Screenshots carousel uses scrollPosition(id:) for scroll-to-initial. Nav chrome in AppDetailHostingController; withObservationTracking re-arm loop drives from model.navBarFraction. iOS 26 glass-capsule workaround: nil out rightBarButtonItem when fraction < 0.01. StoryboardID.swift deleted (only held dead showApp segue ID). Four Main.storyboard App Detail scenes removed. All makeAppViewController call sites updated to AppDetailHostingController.makeAppViewController.
  • Completed: WhatsNewView (was ChangelogViewController), RefreshAttemptsView (was RefreshAttemptsViewController + storyboard scene), LicensesView (was dead storyboard-only LicensesViewController — resurrected, now reachable from Tech Things), ExperimentalFeaturesView (was ExperimentalFeaturesSettingsViewController — Smart Refresh action sheet became a menu Picker), TabOrderView (was SettingsTabOrderViewController — permanent edit mode via .environment(\.editMode, .constant(.active))), NetworkDiagnosticsView (was NetworkDiagnosticsViewController — async/await ping test), BetaTestingView (was BetaTestingSettingsViewController — takes weak var navigationController for in-stack pushes; toggle revert uses an isRevertingToggle flag because SwiftUI onChange fires on programmatic writes, unlike UIKit valueChanged), DeveloperView + DeveloperProfileStore (was DeveloperSettingsViewController — profile cache now an @Observable singleton; DeveloperProfileStore.prefetch() replaces the old VC static prefetch in AppDelegate + SettingsViewController), DisplayView (was DisplaySettingsViewController — accent swatch + OLED/accent re-render via notification-driven .id bump), TechThingsView (was TechThingsSettingsViewController — clear-cache confirm + error alerts in SwiftUI, error-log export still presents UIActivityViewController via nav controller, Error Log push still storyboard-based until that screen migrates), RefreshingAppsView (was RefreshingAppsSettingsViewController — shortcut preview via ShortcutPreviewCoordinator holding the UIDocumentInteractionController), AccentColorView (was AccentColorSettingsViewController — 5-column LazyVGrid, system UIColorPickerViewController kept for the custom picker via ColorPickerCoordinator), AdvancedSettingsView (was AdvancedSettingsViewController — hidden debug section behind 3-finger triple-swipe-up via DebugGestureAttacher UIViewRepresentable that climbs the responder chain to attach the recognizer; mail via MailComposeCoordinator; pairing-file done-alert intentionally has no dismiss button — UIAlertController kept for that), AltAppIconsView (was AltAppIconsViewController — UIApplication.didChangeAppIconNotification extension moved with it; forced dark scheme replaces overrideUserInterfaceStyle), MiniStoreSigningView (was MiniStoreSigningSettingsViewController — document pickers + promptForPassword presented via nav controller, all toasts in nav view), ErrorLogView + ErrorDetailsView (was ErrorLogViewController/Cell/ErrorDetailsViewController + 3 storyboard scenes — @SwiftUI.SectionedFetchRequest over localizedDateString, details + console-log as .sheet with detents, minimuxer log via LogQuickLookCoordinator; needs .environment(\.managedObjectContext, ...) at both push sites; needs import CoreData for NSManagedObjectID), SettingsRootView + SettingsRootViewController (was SettingsViewController + static-cell storyboard scene — hosting controller subclasses UIHostingController with init?(coder:rootView:) so the storyboard scene stays; ALL nav-bar invariants preserved verbatim: bar-level mutations only in viewDidAppear, large-title collapse via transition coordinator, item-level appearances in viewWillAppear. The shared UIViewController extension (applySettingsNavBar/settingsPinContentTop/promptForPassword) moved to SettingsHostingController.swift. InsetGroupTableViewCell + SettingsHeaderFooterView were kept for SelectTeamViewController, then deleted in Round 4 when it was migrated)
  • Infrastructure: SettingsHostingController<Content> — generic host that sets navigationItem.title, disables large titles, applies applySettingsNavBar(); use it for every SwiftUI settings screen
  • Pattern: @MainActor @Observable view model, async fetch methods, .task {} + .refreshable {} in view; CoreData lists use @FetchRequest with .environment(\.managedObjectContext, DatabaseManager.shared.viewContext) injected at the call site
  • Navigation: UIKit nav stack owns the stack; .navigationTitle is inert inside UIHostingController — title goes through SettingsHostingController.init
  • Do NOT use NavigationStack or NavigationView inside a hosted view pushed onto the UIKit nav stack
  • Gotcha: app-local final class Button: UIButton (Components/Button.swift) shadows SwiftUI.Button in the main target — write SwiftUI.Button / SwiftUI.Label explicitly until that class is renamed or removed
  • Gotcha: AltStoreCore exports public typealias FetchRequest = NSFetchRequest<NSFetchRequestResult> (Protocols/Fetchable.swift) — collides with SwiftUI's property wrapper; write @SwiftUI.FetchRequest and SwiftUI.FetchedResults explicitly

Self-update JSON (side.json / sidenightly.json)

Both files live in this repo and are served raw from develop. The separate MiniStore-Public mirror was retired (2026-06-20)deploy_public.yml and the deploy_public.py helper were deleted. All JSON changes happen here now.

side.json and sidenightly.json are auto-stamped by CI on every stable/nightly build (version, date, size, sha256, downloadURL all written automatically) by the stamp step in stable.yml / nightly.yml, which commits the result back to this repo and uploads the IPA to this repo's releases. Manual editing is not needed for release fields.

Existing installs that still point at the old MiniStore-Public URLs are migrated to the in-repo URLs on launch — the old URLs are listed in resolveOrCreateMiniStoreSource (legacy) and deleteOrphanedMiniStoreSources (known-bad) in DatabaseManager, plus Source.fetchMiniStoreUpdateSource candidates.

Still inherited from SideStore: alpha.yml + workflow.py deploy() push an alpha channel to SideStore/apps-v2.json (gated behind the CROSS_REPO_PUSH_KEY secret). This is unrelated to MiniStore-Public and is dormant unless that secret is set. Left in place.

Asset note: side.json/sidenightly.json screenshot URLs point at MiniStore/develop/Screenshots/MiniStore-{1,2,3}.PNG. Pulled from the retired MiniStore-Public repo into Screenshots/ (2026-06-20) — live once merged to develop.

Last updated: 2026-06-21


Plugins & Extensions

Recommended Claude Code plugins for this project. Install via the Claude Code marketplace or manually.

Plugin Repo What it does
swift-ios-skills dpearson2699/swift-ios-skills 84 agent skills for iOS 26+ / Swift 6.3 — SwiftUI, CoreData, concurrency, hardware, AI/ML, and more
apple-platform-build-tools kylehughes/apple-platform-build-tools-claude-code-plugin Build/test/archive Xcode projects from Claude; includes xcrun/xcodebuild/simctl reference docs and a subagent for scheme discovery and simulator management
claude-mem thedotmack/claude-mem Persistent memory across sessions — captures what the agent does, compresses with AI, injects relevant context back into future sessions
claude-code-lsps Piebald-AI/claude-code-lsps LSP servers for 30+ languages — gives Claude go-to-definition, find-references, hover info, and symbol search
awesome-claude-code-subagents VoltAgent/awesome-claude-code-subagents 154+ specialized subagents across 10 categories (core dev, infra, security, data/AI, etc.) with isolated context windows
pointbreak-claude withpointbreak/pointbreak-claude AI-assisted debugging — set breakpoints, inspect variables, step through code via /debug, /step, /inspect slash commands
ui-ux-pro-max-skill nextlevelbuilder/ui-ux-pro-max-skill Design intelligence — 161 UI styles, 161 color palettes, 57 font pairings, accessibility checklists; supports SwiftUI
superpowers obra/superpowers Agentic dev methodology — TDD cycles, implementation planning, parallel subagent execution, spec refinement, and automated code review from brainstorm through deploy
claude-swift-engineering johnrogers/claude-swift-engineering 12 specialized Swift agents for iOS/macOS — architecture planning, implementation, testing, and review for Swift 6.2 / SwiftUI / TCA, plus 18 knowledge skills covering TCA patterns and iOS design principles
swift-lsp claude.com/plugins/swift-lsp Official Claude plugin — Swift Language Server Protocol integration for code intelligence (go-to-definition, autocomplete, symbol search) in Swift files
context7 upstash/context7 MCP server that injects up-to-date, version-specific library docs and code examples directly into context — eliminates hallucinated APIs and outdated patterns
Axiom CharlesWiltgen/Axiom 236 skills for Apple platform development (iOS/iPadOS/watchOS/tvOS) covering UI, data, concurrency, performance, networking, and accessibility, plus 39 agents for scanning common issues like memory leaks and data migration errors
swiftui-agent-skill twostraws/swiftui-agent-skill SwiftUI Pro skill by Paul Hudson — guidance on navigation, layouts, animations, state management, and accessibility; flags deprecated APIs and LLM-common mistakes; trigger with /swiftui-pro
caveman juliusbrussee/caveman Output-token compressor (/caveman lite|full|ultra|wenyan, /caveman-commit, /caveman-review, /caveman-stats). MiniStore's "Caveman Mode — Full" directive is modeled on it. Doc: .claude/plugins/caveman.md
headroom-desktop gglucass/headroom-desktop Desktop tray app that compresses prompts/tool-output over the wire (~50%). MiniStore's "Headroom Mode" directive is modeled on it. Doc: .claude/plugins/headroom.md
taste-skill Leonxlnx/taste-skill Web-frontend design skills (variance/motion/density dials). Web only — design-vocabulary reference for this native-iOS repo, not a code generator. Doc: .claude/plugins/taste.md
impeccable pbakaus/impeccable Web design skill — 23 commands + 44 detector rules (/impeccable init|audit|critique|polish). Web only — does not lint SwiftUI/UIKit. Doc: .claude/plugins/impeccable.md

Caveman & Headroom are active conventions, not just recommendations — they are enforced as the "Caveman Mode — Full" and "Headroom Mode — Token Frugality" directives at the top of this file. The plugin docs under .claude/plugins/ describe the upstream tooling and its precedence relationship to those directives.


Rules

  1. Read the full file before touching it. No exceptions.
  2. Commit after every self-contained change. Never leave the tree dirty with multiple independent changes.
  3. No force-push without explicit instruction. No --no-verify. No reset --hard on a pushed branch without explicit instruction.
  4. Never swallow errors. Log domain + code. Set a visible error state.
  5. Claude may freely edit CLAUDE.md and all .claude/ files without confirmation. These are documentation — keeping them current is a standing instruction.
  6. Follow Swift ISP and protocol-oriented design. New protocols must be narrow. New ResultOperation subclasses must call finish(_:) exactly once on every path. See .claude/swift-isp.md for the full checklist.
  7. Use correct gh flag syntax. gh release edit uses --draft=false and --prerelease=false — never --no-draft / --no-prerelease. See .claude/github-info.md.
  8. Never add VPN-related code. No TunnelProv target, no NetworkExtension framework, no packet tunnel, no VPN tab. isVPNEnabled UserDefault exists only as a stub (always false) for key-space compatibility. Exception (sanctioned, 2026-06-10): the LocalDevVPN redirect flow is allowed — it is a URL-scheme hop to the external LocalDevVPN app, not in-app VPN code. Components: enableEMPforWireguard UserDefault, "LocalDevVPN Auto-Connect" toggle in Refreshing Apps settings, localdevvpn in LSApplicationQueriesSchemes, and MyAppsViewController.checkMinimuxer (deep-link out → background-task return via sidestore:// → minimuxer retarget + retry). Do not remove these citing this rule.