Files
simslim-mirror/CLAUDE.md
Interlap 41a8277879 fix: keep AMS daemons enabled with --except store so in-app purchases work (#22)
The in-app purchase payment sheet is rendered by Apple Media Services
(AMSUI), whose daemons lived only in the icloud category. Slimming with
--except store therefore disabled them: products still loaded and the buy
request succeeded, but the purchase died with AMSErrorDomain "Payment
Sheet Failed Presentation failed".

Categories may now overlap. A label lives in every category whose feature
needs it, and Profile.Desired() keeps a label enabled when any excepted
category lists it (previously a label was disabled if any non-excepted
category listed it). The amsaccountsd/amsengagementd/amsondevicestoraged
trio joins the store category while remaining in icloud.

The storekit doctor feature now checks itunesstored and the AMS daemons
alongside storekitd, so `doctor --requires storekit` catches this state.
The GUI computes its disabled count and per-service kept state from a
deduplicated effective set so shared labels are neither double-counted
nor shown as disabled when another kept category needs them.
2026-08-03 09:52:56 +02:00

8.4 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

simslim runs many more iOS simulators on one Mac by disabling the background daemons a simulator doesn't need, cutting each simulator's memory ~4x. It is a Go CLI plus a SwiftUI macOS app that wraps it. Everything is driven through xcrun simctl; the tool only ever touches the simulators you point it at, never the host Mac. macOS-only.

Commands

go build ./cmd/simslim          # build the CLI
go test ./...                   # run all tests (Makefile: make test)
go test -run TestName ./...     # run a single test
make check                      # full CI gate — must pass before a PR
make format                     # gofmt + swift-format (run before committing)
make app                        # build build/SimSlim.app (Go + swiftc, macOS only)
open build/SimSlim.app

make check runs go test, go vet, swift-format lint --strict --recursive gui, zsh -n scripts/build-app.sh, and plutil -lint gui/Info.plist. CI (.github/workflows/ci.yml) runs exactly this on macos-26 / Xcode 26.6, then builds and verifies the app bundle.

Tests are pure unit tests (parsing, delta logic, allowlist invariants) — they do not boot real simulators, so they run anywhere. Only the app build and manual runs need Xcode + an iOS runtime.

Architecture

Two packages. The repo root is package simslim, an importable library holding every piece of slimming logic and no external dependencies. cmd/simslim/ is package main, the CLI: main.go dispatches os.Args[1] to a cmd* function per subcommand and enforces macOS-only up front.

Anything that talks to a terminal — printing, --json encoding via writeJSON, the interactive wizard in wizard.go, fatal(), usage() — lives in cmd/simslim/. The library never writes to stdout; it returns values and reports progress through the Reporter callback the caller supplies.

Exported identifiers in the root package are the public API, so renaming one is a breaking change for importers as well as for the CLI.

The slimming model (the core idea). profiles.go defines Categories, an allowlist of launchd daemon labels grouped by user-facing feature (siri, search, icloud, …). Categories may overlap — a label lives in every category whose feature needs it (e.g. the AMS payment-sheet daemons are in both store and icloud), and Profile.Desired() keeps a label enabled when any excepted category lists it. SlimmableSet() is the deduplicated union of every label in Categories. managedSet() adds each category's AlwaysEnabled compatibility services, which simslim may only repair back to enabled; these are the only labels the tool may ever disable or enable. Anything outside those sets is never touched. service_descriptions.go supplies the short per-daemon explanations shown by the GUI; its coverage and length are enforced in profiles_test.go. profile_file.go loads a committed JSON profile (simslim on --profile <path>) whose except/keep arrays mirror the flags of the same name, validates it against the allowlist, and resolves it to a Profile. The dependency-free profile command's interactive wizard lives in cmd/simslim/wizard.go. features.go defines Features, a finer-grained catalog than Categories: each feature (push, storekit, universal-links, …) names just the daemons one testable capability needs. doctor reads a booted simulator's disabled labels and reports any required feature whose daemons are down, exiting non-zero — a CI preflight. features_test.go asserts every feature label is slimmable. slim.go's ensure() boots the device, reads its currently disabled labels, computes a delta against the desired set, and applies the changes with launchctl disable/enable run inside the simulator via simctl spawn. The disables are written as persistent launchd overrides, so a slimmed simulator comes up slim on every subsequent boot in one boot. on disables the profile; off re-enables the whole managed set back to stock.

simctl wrapper. simctl.go is the only place that shells out to xcrun simctl (list/boot/shutdown/clone/erase/delete/spawn). measure.go sums phys_footprint across the simulator's launchd process tree (via pgrep/ps/top) — that's the memory figure that decides how many simulators fit. MeasureProcesses keeps the per-process detail (footprint + cpu, from the same snapshot) for the top drill-down. fleet.go's FleetSnapshot composes booted devices + slim status + MeasureMany into the fleet view; the live TUI is cmd/simslim/top.go (Bubble Tea), which also has a --json/non-TTY one-shot fallback. disk.go, disk_cleanup.go, and disk_inventory.go handle disk measurement and the separate, permanent disk-cleanup feature.

JSON is a contract. Every read-only and management command supports --json. The structs in output.go (DeviceSummary, StatusOutput, SimulatorMutationOutput, etc.) plus the category/plan structs are the stable interface the SwiftUI app decodes. Changing a JSON field breaks the GUI in gui/*.swift — keep them in sync. gui/Backend.swift invokes the bundled CLI with --json and decodes these types; there is no other IPC.

The SwiftUI app (gui/) has no Xcode project. scripts/build-app.sh compiles the Swift sources directly with swiftc, cross-builds the Go CLI, bundles it into SimSlim.app/Contents/Resources/simslim, generates the icon, and ad-hoc codesigns. The app is a thin front end that shells out to that bundled binary.

Safety invariants — preserve these

  • Never add a deadlock-prone daemon to Categories. A handful of daemons wedge a simulator when disabled. profiles_test.go holds a forbiddenLabels list and asserts none appear in any category; if you add labels, keep that test green.
  • Only managed labels are ever mutated. delta() is scoped to managedSet() on both sides. Compatibility labels are omitted from every desired slim state, so they can only be repaired to enabled.
  • Destructive/management commands resolve the exact UDID first (findDevice / shutdownIfBooted) so a simctl alias like all can never fan out a boot, erase, delete, or filesystem path across every simulator.
  • Disk cleanup only removes allowlisted per-device directories, refuses to run without --confirm, and never touches the shared, signed iOS runtime (built-in apps, core OS language resources). disk-plan is strictly read-only. Durable storage (Documents, app bundles, user media) is measured but never deletable.

Conventions

  • CLI parsing uses github.com/urfave/cli/v3; the top command's live TUI uses github.com/charmbracelet/bubbletea + lipgloss. These are the CLI's only dependencies — the root library still has none, so importers never inherit a terminal stack. The command tree lives in cmd/simslim/app.go (newApp); each subcommand's Action is a cmd* function in cmd/simslim/main.go that reads flags via cmd.Bool/String(...) and positionals via cmd.Args(). Flags may appear before or after positional args (e.g. simslim on <udid> --except search) — v3 parses flags anywhere by default. --set is a global flag (inherited by every subcommand) registered in its flag Action. main.go still owns version/help/no-args and the macOS-only guard before handing off to the tree, and routes every command error through fatal() for the stable simslim: <msg> (exit 1); unknown commands exit 2 with usage.
  • Two timeouts live in simctl.go: ShutdownTimeout (30s, a const) and BootTimeout (10min, because a first slim reconfigure boots twice). BootTimeout is a package var, not a const, so the CLI's global --boot-timeout flag (env SIMSLIM_BOOT_TIMEOUT, wired in app.go like --set) can raise it for slow CI runners where the per-daemon launchctl transitions would otherwise blow the deadline mid-reconfigure.
  • Progress for multi-minute operations goes to stderr via the Reporter callback; machine-readable JSON goes to stdout. Under --json, suppress the stderr chatter so consumers reading a combined stream still get clean JSON.
  • Memory estimates (ApproxMemoryMB) are iOS-26.5 clean-boot medians and are not additive. Every category must have a positive measured estimate.

Git

Never add a Co-Authored-By: Claude trailer or any "Generated with Claude" footer to commit messages.