Skip to content

feat(init): macOS launchd + shellrc fallback + --start-daemon#19

Merged
jiunbae merged 1 commit into
mainfrom
feat/init-launchd-shellrc
Apr 30, 2026
Merged

feat(init): macOS launchd + shellrc fallback + --start-daemon#19
jiunbae merged 1 commit into
mainfrom
feat/init-launchd-shellrc

Conversation

@jiunbae
Copy link
Copy Markdown
Member

@jiunbae jiunbae commented Apr 30, 2026

Summary

Closes the v0.4.1 UX gap where muxa init finished cleanly on macOS but muxa status failed because nothing had ever started muxad. Three additive pieces, no breaking changes:

  • muxad-launchd — macOS analogue of muxad-systemd. muxa init writes ~/Library/LaunchAgents/dev.open330.muxad.plist, runs launchctl bootstrap gui/<uid> + kickstart -k. Uninstall does the inverse. Same outcome / dry-run / marker semantics as the systemd path.
  • muxad-shellrc — cross-platform fallback for hosts with no service manager (containers, BSDs, WSL1, minimal Linux, CI). Marker block in ~/.zshrc / ~/.bashrc / fish config / fallback ~/.profile, lazy-starting muxad from the next interactive shell. Shell pick is auto from $SHELL.
  • --start-daemon flag (default true) — final action that spawns muxad detached if pgrep -x muxad doesn't find it. Belt-and-suspenders alongside the manager components: if systemd's enable --now or launchd's bootstrap silently no-ops, the explicit start still leaves muxad responsive when the wizard finishes.

Preset::Standard / Preset::Full and the wizard's multi-select pick exactly one of the three managers from std::env::consts::OS. Detection::recommended_daemon_manager further degrades to shellrc when systemctl/launchctl is missing on a host that'd normally support them — the "Standard preset = working install" invariant holds even in stripped-down envs.

Why now

Reported by a user on macOS (after the v0.4.1 fix landed): muxa init succeeded but muxa status returned daemon not reachable at /tmp/muxa-501.sock. Pre-flight pgrep matched a stale process; IPC socket wasn't bound. muxad-launchd is the macOS-native fix; muxad-shellrc + --start-daemon close the gap on every other systemd-less platform too.

Module layout

File Role
files/launchd.rs (new, ~120 LOC) macOS plist content + launchctl bootstrap/bootout driver
files/shellrc.rs (new, ~120 LOC) shell rc marker block, $SHELL-aware path selection
components.rs MuxadLaunchd / MuxadShellrc variants + recommended_daemon_manager() + applicable_here() filter
detect.rs launchctl_available field + recommended_daemon_manager() host degrader
plan.rs EnableLaunchdUnit { plist_path } / DisableLaunchdUnit / StartDaemonIfNeeded actions; plan_systemd / plan_launchd / plan_shellrc helpers
apply.rs New action handlers + detached muxad spawner; refactored apply_one into named helpers (was at clippy's line cap)
mod.rs --start-daemon flag + appender; new step lines
ui.rs Multi-select filtered to applicable_here() only

Test plan

  • cargo test --workspace — 365 / 365 pass (was 361 + 4 new)
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • Linux dry-run: muxa init --dry-run --preset standard --yes → systemd, no launchd, no shellrc; start muxad if not running line appears
  • macOS smoke: muxa init --preset standard --yes on a real macOS host — confirms plist written, agent loaded, muxa status responsive without manual intervention
  • Container / WSL1 smoke: muxa init --preset standard --yes → falls back to muxad-shellrc, daemon starts
  • Round-trip: muxa init --preset standard --yes then muxa init --uninstall leaves a clean state

Release

Ship as v0.4.2 (minor — additive new feature).

🤖 Generated with Claude Code

Closes the v0.4.1 UX gap where `muxa init` would finish cleanly on
macOS but `muxa status` would fail because nothing had ever started
muxad. The Linux `muxad-systemd` component had no equivalent on any
other platform.

Three pieces:

1. `muxad-launchd` — macOS analogue of `muxad-systemd`. Writes
   `~/Library/LaunchAgents/dev.open330.muxad.plist`, runs
   `launchctl bootstrap gui/<uid>` + `kickstart -k`. Uninstall does
   the inverse. Same outcome / dry-run / marker semantics as the
   systemd path.

2. `muxad-shellrc` — cross-platform fallback for hosts with no
   service manager (containers, BSDs, WSL1, minimal Linux, CI). One
   marker block in `~/.zshrc` / `~/.bashrc` / fish config / fallback
   `~/.profile` that lazy-starts muxad from the next interactive
   shell. Shell pick is auto from `$SHELL`.

3. `--start-daemon` flag (default true) — appended action at the
   end of every install run that spawns muxad detached if
   `pgrep -x muxad` doesn't find it. Belt-and-suspenders alongside
   the manager components: if systemd's enable-now or launchd's
   bootstrap silently no-ops, the explicit start guarantees the
   wizard leaves muxad responsive when it finishes.

Smart selection in Preset::Standard / Preset::Full + the wizard's
multi-select: exactly one of the three managers shows up, picked
from `std::env::consts::OS`, with `Detection::recommended_daemon_manager`
further degrading to shellrc when systemctl/launchctl is missing on
a host that would normally support them. The "Standard preset = a
working install" invariant holds even in stripped-down envs.

Internals:

- `files/launchd.rs` (~120 LOC) mirrors `files/systemd.rs`'s shape:
  pure plist content layer + thin shell-out driver. New
  `EnableLaunchdUnit { plist_path }` / `DisableLaunchdUnit` actions.
- `files/shellrc.rs` (~120 LOC) reuses the existing `marker::upsert`
  / `marker::remove` so removal is surgical the same way every other
  marker-block file works.
- `apply.rs::apply_one` was getting too long (clippy 116/100); split
  EditFile / DeleteFile / SourceTmuxConf / StartDaemon out as named
  helpers.

Tests: launchd plist content + outcome decision; shellrc round-trip
+ shell→rc mapping; updated default_selection assertion (now picks
the OS-fallback manager). 361 → 365 workspace tests, all green.
Clippy clean with `-D warnings`, rustfmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jiunbae jiunbae merged commit 2c7a2fb into main Apr 30, 2026
6 checks passed
@jiunbae jiunbae deleted the feat/init-launchd-shellrc branch April 30, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant