Describe here the purpose of your PR.
What changed
tuist setup cache installs a KeepAlive LaunchAgent that runs cache-start. When the user is logged out, cache-start threw while resolving the cache endpoint, exited non-zero, and launchd respawned it every ~10 seconds. Each respawn created a brand-new session directory under ~/.local/state/tuist/sessions/ and re-ran work that could never succeed.
Three coupled changes:
CacheStartCommandService — resolve the server URL and check authentication first. When there is no credential, log a hint and return cleanly (exit 0) instead of throwing, so a logged-out daemon does no work and leaves no session directory. A transient refresh/network error still throws, so launchd keeps retrying in that case.
LaunchAgentService — the plist’s KeepAlive changes from <true/> to <dict><key>SuccessfulExit</key><false/></dict>, so launchd only respawns the daemon on an unsuccessful (crash) exit, not on the clean “nothing to do” exit. This is what actually stops the 10s respawn storm.
SetupCacheCommandService — fail fast: check authentication up front and abort with an actionable error before installing the LaunchAgent. Without this, setup looked successful while the daemon it installed immediately exited for lack of credentials. (1) + (2) make the storm impossible; (3) makes the logged-out outcome explicit at the moment the user runs setup.
Why this approach
KeepAlive = true ignores the exit code and respawns unconditionally, so making cache-start exit cleanly is not enough on its own — the plist must also stop respawning on a successful exit. Hence (1) + (2).
SuccessfulExit = false is the canonical launchd policy for “run, and if there’s nothing to do, exit and stay down until next login/setup,” while still self-healing on a real crash.
- The
metrics-sampler daemon shares this plist generation. It runs an infinite loop and never exits 0 on its own, so under SuccessfulExit = false its behavior is unchanged (still restarts only on crash).
- Failing setup fast (3) is preferable to installing a dead agent: the daemon does not auto-start on a later login (a clean exit is never respawned), so silently no-op’ing would leave the user thinking caching is on when it isn’t.
Behavior after this change
setup cache while logged out → fails immediately with “You must be authenticated…”, installs nothing.
- Logged out while the daemon is already running → the daemon stays up (per-request auth fails gracefully per [
CacheClientAuthenticationMiddleware] + the gRPC handlers catch per request); logging back in recovers automatically, because the token is read fresh from the credentials store on every request. No restart needed.
- Authenticated / CI (
TUIST_TOKEN) → unaffected; the env-token path satisfies the up-front check.
Root cause
The session directory is created for every CLI invocation before the command runs, and cache-start only failed after that, deep in the cache-endpoint lookup — so an unauthenticated respawn-loop left one empty session dir per 10s tick. The daemon had no early, clean “not authenticated” path, the LaunchAgent treated a permanent auth failure the same as a transient crash, and setup cache never checked auth at all.
User impact
Running tuist setup cache while logged out no longer (a) pins a background process that wakes every ~10s and accumulates session directories, nor (b) silently reports success while installing a daemon that can’t run. The user gets a clear instruction to authenticate and re-run.
How to test locally
- Log out (remove server credentials; ensure
TUIST_TOKEN is unset).
- Run
tuist setup cache in a project with a fullHandle → it now fails fast with “You must be authenticated to set up the cache. Run tuist auth login …” and installs no LaunchAgent (~/Library/LaunchAgents/tuist.cache.*.plist is not created; launchctl list | grep tuist.cache is empty).
tuist auth login, then re-run tuist setup cache → the agent installs and cache-start serves normally.
- (Daemon resilience) With the daemon running, remove credentials, trigger a cache request → requests fail gracefully and the daemon stays up; log back in → caching resumes without restarting the daemon.
Automated coverage:
CacheStartCommandServiceTests.run_exitsCleanlyWithoutStartingServer_whenNotAuthenticated
SetupCacheCommandServiceTests.setupCache_notAuthenticated (asserts no LaunchAgent is installed)
LaunchAgentServiceTests.setupLaunchAgent_createsDirectoryAndPlist (asserts the SuccessfulExit KeepAlive form)
🤖 Generated with Claude Code