diff --git a/Brewfile b/Brewfile index a896c50..3f447d1 100644 --- a/Brewfile +++ b/Brewfile @@ -70,6 +70,12 @@ cask "steam" cask "transmission" # BitTorrent client cask "whatsapp" +# Gateway-only (mac-mini): same hostname match as is_gateway_host in bootstrap.sh +require "socket" +if Socket.gethostname.split(".").first.match?(/\Amac-mini(-\d+)?\z/) + cask "jellyfin" # Media server (http://localhost:8096) +end + # Casks - Fonts cask "font-jetbrains-mono-nerd-font" diff --git a/CLAUDE.md b/CLAUDE.md index d7dcfa9..8b80aac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,10 +116,11 @@ Files in `home/` are symlinked to `~` by `bootstrap.sh`. This allows version con | agent-session-analytics | 8081 | `/agent-session-analytics` | `com.evansenter.agent-session-analytics` | | agent-memory-store | 8083 | `/agent-memory-store` | `com.evansenter.agent-memory-store` | | obsidian-mcp | 3010 | `/obsidian-mcp` | `com.evansenter.obsidian-mcp` | +| jellyfin | 8096 | — | `com.evansenter.jellyfin` | LaunchAgent plists live in `~/Library/LaunchAgents/`. The external service repos (event-bus, session-analytics, memory-store) have their own `make install-server`; obsidian-mcp is set up by *this* repo's bootstrap (`claude mcp add` + plist in `LaunchAgents/`, wrapper in `home/.bin/obsidian-mcp-start`, requires Obsidian with the Local REST API plugin running). Reload with `launchctl unload` + `launchctl load`. -Note: `agent-memory-store` is documented infra but is **not** provisioned by bootstrap (no `claude mcp add`, no `AGENT_MEMORY_STORE_URL` in `settings.local.json`) — it's registered out-of-band where used. The repo-managed LaunchAgents are: `com.evansenter.obsidian-mcp` (gateway only), `com.evansenter.sysload` (zellij CPU/RAM widget, if zellij installed), and `com.user.cargo-sweep` (periodic `cargo sweep`, if cargo installed). +Note: `agent-memory-store` is documented infra but is **not** provisioned by bootstrap (no `claude mcp add`, no `AGENT_MEMORY_STORE_URL` in `settings.local.json`) — it's registered out-of-band where used. The repo-managed LaunchAgents are: `com.evansenter.obsidian-mcp` (gateway only), `com.evansenter.jellyfin` (gateway only, runs the server binary from the `jellyfin` cask headless with `DOTNET_USE_POLLING_FILE_WATCHER=1` — on macOS 27 its FSEvents config watcher blocks startup forever; don't also launch Jellyfin.app, which would fight it for port 8096), `com.evansenter.sysload` (zellij CPU/RAM widget, if zellij installed), and `com.user.cargo-sweep` (periodic `cargo sweep`, if cargo installed). **Adding new LaunchAgents:** Plists go in the top-level `LaunchAgents/` directory (NOT `home/Library/LaunchAgents/`). Use `__HOME__` as a placeholder for the user's home directory — `install_launch_agent()` in bootstrap does `sed` substitution at install time. Logs go to `~/.local/log/`. Add an `install_launch_agent` call in `install_launch_agents()`, gated on the binary being present. diff --git a/LaunchAgents/com.evansenter.jellyfin.plist b/LaunchAgents/com.evansenter.jellyfin.plist new file mode 100644 index 0000000..9559371 --- /dev/null +++ b/LaunchAgents/com.evansenter.jellyfin.plist @@ -0,0 +1,53 @@ + + + + + Label + com.evansenter.jellyfin + + + ProgramArguments + + /Applications/Jellyfin.app/Contents/MacOS/jellyfin + --webdir + /Applications/Jellyfin.app/Contents/Resources/jellyfin-web + --ffmpeg + /Applications/Jellyfin.app/Contents/MacOS/ffmpeg + --datadir + __HOME__/Library/Application Support/jellyfin + + + + EnvironmentVariables + + DOTNET_USE_POLLING_FILE_WATCHER + 1 + + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + + + ThrottleInterval + 30 + + + StandardOutPath + /dev/null + + StandardErrorPath + __HOME__/.local/log/jellyfin.err + + + ProcessType + Standard + + diff --git a/bootstrap.sh b/bootstrap.sh index a06c436..750902b 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -393,6 +393,11 @@ install_launch_agents() { install_launch_agent "com.evansenter.obsidian-mcp.plist" fi + # Install Jellyfin server LaunchAgent (gateway host, from the jellyfin cask) + if is_gateway_host && [[ -x /Applications/Jellyfin.app/Contents/MacOS/jellyfin ]]; then + install_launch_agent "com.evansenter.jellyfin.plist" + fi + # Install sysload writer LaunchAgent (only if zellij is installed) # Caches `top` output to ~/.cache/sysload every 10s so the zjstatus # sysload widget can read it without spawning `top` per-tab-per-redraw. diff --git a/tests/test-bootstrap.sh b/tests/test-bootstrap.sh index 061ca40..a11e63a 100755 --- a/tests/test-bootstrap.sh +++ b/tests/test-bootstrap.sh @@ -704,6 +704,12 @@ test_launch_agent_substitutes_home() { return $ok } +test_jellyfin_agent_uses_polling_file_watcher() { + # Without it the server hangs before logging or binding 8096 on macOS 27 + grep -A1 'DOTNET_USE_POLLING_FILE_WATCHER' "$SCRIPT_DIR/../LaunchAgents/com.evansenter.jellyfin.plist" \ + | grep -q '1' +} + test_launch_agent_idempotent_on_unchanged_plist() { _load_install_launch_agent local root home stub; root=$(mktemp -d); home=$(mktemp -d); stub=$(mktemp -d) @@ -1030,6 +1036,7 @@ main() { run_test "sysload-writer tolerates top failure" "test_sysload_writer_tolerates_top_failure" run_test "launch agent substitutes __HOME__ and reloads" "test_launch_agent_substitutes_home" run_test "launch agent idempotent on unchanged plist" "test_launch_agent_idempotent_on_unchanged_plist" + run_test "jellyfin agent uses polling file watcher" "test_jellyfin_agent_uses_polling_file_watcher" echo "" echo "=== INSTALL_AI & INSTALL_TAILSCALE gating ==="