Skip to content

fix(docs+menubar): platform-aware fallback messages and macOS-only guard#1075

Open
MHoroszowski wants to merge 3 commits intodanielmiessler:mainfrom
MHoroszowski:fix/platform-aware-docs-and-menubar-guard
Open

fix(docs+menubar): platform-aware fallback messages and macOS-only guard#1075
MHoroszowski wants to merge 3 commits intodanielmiessler:mainfrom
MHoroszowski:fix/platform-aware-docs-and-menubar-guard

Conversation

@MHoroszowski
Copy link
Copy Markdown

Summary

Four small cross-platform cosmetic fixes that close out the PAI audit
for Linux/WSL users:

  • VoiceServer/install.sh and VoiceServer/status.sh told
    Linux/WSL users the voice server "will use macOS 'say'" as a
    fallback when no ElevenLabs API key was configured. On non-Darwin
    hosts say doesn't exist and the server has no built-in TTS
    fallback. Both scripts now branch on pai_is_darwin (from
    lib/platform.sh) and print an honest message on Linux/WSL. The
    Darwin strings are preserved byte-identical.

  • VoiceServer/menubar/install-menubar.sh now has an early
    platform guard. SwiftBar and BitBar are macOS-only; on Linux/WSL
    the script prints an explanation pointing at getNotificationCmd()
    in server.ts and exits 0 cleanly so CI and cross-platform
    bootstrap flows can invoke it unconditionally. The guard is a
    single uname -s check; on Darwin it is a no-op and execution
    continues through the unchanged SwiftBar/BitBar detection logic
    byte-identically.

  • Packs/Utilities/INSTALL.md's troubleshooting section for the
    AudioEditor sub-skill only listed brew install ffmpeg. Added
    collapsible <details> blocks with macOS and Linux/WSL install
    commands. Other entries (wrangler/npm, bun/curl, fabric) are
    already cross-platform and were left alone.

Stacking

This PR stacks on:

PR #1072 introduces lib/platform.sh with pai_is_darwin, which PR
4 reuses. The diff against main will show both parents' content
until they merge. Rebase onto main after they land.

Test plan

  • bash -n on all three modified shell scripts
  • install-menubar.sh run on real Linux host: guard fires,
    prints explanation, exits 0
  • install-menubar.sh with mocked uname -s = Darwin: guard is
    a no-op, script continues through to the unchanged SwiftBar /
    BitBar detection branch
  • status.sh Voice Configuration branch, all three env-file
    states (missing / placeholder / real key) on Linux — each hits
    the non-Darwin branch and prints the truthful message
  • install.sh fallback note branches on Linux — both no-file
    and placeholder-key print the truthful message
  • Packs/Utilities/INSTALL.md collapsible blocks are
    well-formed (blank line after <summary> for GitHub's
    markdown parser)
  • Darwin reviewer spot-check: please verify that on macOS
    the three literal strings Voice server will use macOS 'say' command as fallback, ! Using macOS 'say' (no API key), and
    ! Using macOS 'say' (no configuration) still print, and that
    install-menubar.sh runs through SwiftBar/BitBar detection
    exactly as it did before. Author runs PAI on WSL2 only.

Scope note

Packs/Research/INSTALL.md also has two brew install fabric
references but the internal plan scoped PR 4 to
Packs/Utilities/INSTALL.md specifically. Left for a follow-up.

playAudio() hardcoded /usr/bin/afplay, which is macOS-only. On Linux,
every TTS notification fails with ENOENT and the voice server appears
to work but produces no audio (the failure is swallowed by the
fire-and-forget curl pattern used at the call sites).

Extract player resolution into getAudioPlayer():
- darwin           → afplay  (unchanged)
- linux + ffplay   → ffplay -nodisp -autoexit -volume 0..100
- linux + mpg123   → mpg123 -f 0..32768 (PCM scale)
- neither          → throw with an actionable install hint

ffplay is preferred because ffmpeg is widely preinstalled; mpg123 is
the lightweight fallback. Both route through PulseAudio, so this works
on native Linux and on Windows via WSL2 + WSLg out of the box.

Verified on Ubuntu 24.04 / WSL2 (Windows 11): TTS audio plays through
WSLg PulseAudio to Windows speakers with no additional configuration.

Addresses the audio-playback half of danielmiessler#855. Complementary to danielmiessler#1030,
which covers the desktop-notification half (osascript → notify-send)
without overlap.
… port check

Extends the getAudioPlayer() pattern from danielmiessler#1061 to cover three more macOS-
only assumptions that fail silently or visibly on Linux and WSL2:

1. sendNotification() hardcoded /usr/bin/osascript with an AppleScript
   `display notification` call. On Linux this is ENOENT; on WSL2 it is
   also ENOENT and the user loses every desktop banner.
2. The hardcoded ~/Library/Logs/pai-voice-server.log path appears in six
   different scripts (install/uninstall/start/stop/status and the menubar
   BitBar indicator). On Linux it writes into a non-standard location
   inside $HOME that XDG-aware tools never discover.
3. status.sh and stop.sh/uninstall.sh gate all port-check logic on `lsof`,
   which is not installed by default on many Linux and container images.
   When lsof is absent the "is port 8888 in use?" check silently returns
   no, even when pai-voice is actively listening.

Changes:

- server.ts: add three helpers next to getAudioPlayer().
  * isWSL()              — single source of truth for WSL1/WSL2 detection
                           via /proc/version, short-circuits on non-Linux.
  * getLogPath()         — darwin unchanged; linux/wsl uses
                           ${XDG_DATA_HOME:-$HOME/.local/share}/pai/logs/…
  * getNotificationCmd() — mirrors getAudioPlayer() shape.
      darwin → /usr/bin/osascript (literal AppleScript preserved
               byte-identically so macOS behavior is unchanged).
      wsl2   → wsl-notify-send if present, else powershell.exe with
               BurntToast (if the module is importable) or a bare
               [Windows.UI.Notifications.ToastNotificationManager]
               one-liner as the final fallback.
      linux  → /usr/bin/notify-send.
  Call site inside sendNotification() routes through the new helper. The
  darwin branch produces argv identical to the pre-refactor literal, so
  macOS is an obvious-by-inspection no-op.

- lib/platform.sh (new): shared shell helpers sourced by every script.
  * pai_is_wsl           — matches isWSL() in TS (single detector).
  * pai_log_path         — matches getLogPath() in TS.
  * pai_port_pids PORT   — cascades lsof → ss → netstat, printing one
                           PID per line; returns 1 if nothing listens.
  POSIX-leaning bash, side-effect free on source, no mkdir, no exit.

- install.sh, uninstall.sh, status.sh, start.sh, stop.sh,
  menubar/pai-voice.5s.sh: source lib/platform.sh, replace literal
  LOG_PATH assignments with "$(pai_log_path)", and swap lsof-only port
  checks for pai_port_pids so the scripts work when lsof is unavailable.
  Darwin launchctl logic is untouched; only the log-path string and the
  port-check call site change on the macOS flow.

Verified on Ubuntu 24.04 / WSL2:
- bun bundles server.ts cleanly.
- bash -n passes on every modified script.
- pai_is_wsl returns 0 inside WSL2 and stays false on pure Linux
  (no /proc/version microsoft match).
- pai_log_path resolves to /home/$USER/.local/share/pai/logs/pai-voice-server.log.
- pai_port_pids 8888 returns the live PID via lsof, ss (lsof masked),
  and netstat (lsof + ss masked) — all three branches confirmed against
  a running pai-voice.service.
- getNotificationCmd() on WSL2 selects the powershell.exe branch when
  wsl-notify-send is absent; powershell.exe returns exit 0 and fires a
  toast via BurntToast/WinRT.

Darwin paths are preserved byte-identically and were not exercised on
hardware (author runs PAI on WSL2 only). Please review the darwin
branches carefully — they are intentionally line-for-line equal to the
pre-refactor literals.

Stacked on top of danielmiessler#1061 (cross-platform audio playback). Should be
merged after danielmiessler#1061, or rebased onto main if danielmiessler#1061 lands first.
Four small cross-platform cosmetic fixes that close out the PAI audit:

- VoiceServer/install.sh and status.sh told Linux/WSL users the
  voice server "will use macOS 'say'" as a fallback when no
  ElevenLabs API key was configured. On non-Darwin hosts `say` does
  not exist and the server has no built-in TTS fallback. Both
  scripts now branch on pai_is_darwin (from lib/platform.sh, added
  in the cross-platform helpers PR) and print an honest message on
  Linux/WSL. The Darwin strings are preserved byte-identical.

- VoiceServer/menubar/install-menubar.sh now has an early
  platform guard. SwiftBar and BitBar are macOS-only; on Linux/WSL
  the script prints an explanatory message pointing at
  getNotificationCmd() in server.ts and exits 0 so CI and
  cross-platform bootstrap flows can invoke it unconditionally.
  The guard is a single `uname -s` check; on Darwin the script
  continues through to the unchanged SwiftBar/BitBar detection
  logic byte-identically.

- Packs/Utilities/INSTALL.md's troubleshooting section for the
  AudioEditor sub-skill only listed `brew install ffmpeg`. Added
  collapsible <details> blocks with macOS and Linux/WSL install
  commands. Other entries (wrangler/npm, bun/curl, fabric) are
  already cross-platform and were left alone.

Stacked on top of the cross-platform helpers PR (adds lib/platform.sh
with pai_is_darwin) and the cross-platform audio PR. Rebase onto main
after those land.

Scope note: Packs/Research/INSTALL.md also has two `brew install
fabric` references but the plan scoped PR 4 to Utilities/INSTALL.md
specifically. Left for a follow-up.
MHoroszowski added a commit to MHoroszowski/Personal_AI_Infrastructure that referenced this pull request Apr 15, 2026
VoiceServer's install/start/stop/status/uninstall scripts previously
assumed macOS/launchctl exclusively. Linux and WSL2 users had no
supported path to run the voice server as a supervised service. This
adds a systemd --user branch to each script, selected at runtime via
the pai_is_darwin helper from lib/platform.sh. The Darwin launchctl
flow is preserved byte-identical.

### install.sh

- New systemd_unit_* configuration constants alongside the existing
  PLIST_PATH.
- "Existing installation" check branches on pai_is_darwin. On
  Linux/WSL it probes systemctl --user list-unit-files and the on-
  disk unit file, prompts the user with the same y/n reinstall UX
  as the macOS path, and on decline exits 0 without touching the
  live unit.
- Linux/WSL prechecks that systemctl is present and that
  systemctl --user list-units --no-pager is reachable. On WSL2 it
  prints the /etc/wsl.conf [boot] systemd=true hint if the user
  session is unavailable.
- New unit generator writes ~/.config/systemd/user/pai-voice.service
  templated on the reference unit that ships with PAI on WSL2:
    [Unit]  Description / After=default.target
    [Service]  Type=simple  WorkingDirectory=${SCRIPT_DIR}
               ExecStart=${BUN_BIN} run server.ts
               Restart=on-failure RestartSec=3
               StandardOutput/StandardError=append:${LOG_PATH}
               Environment=HOME/PATH
    [Install]  WantedBy=default.target
  LOG_PATH comes from pai_log_path (XDG on Linux, Library/Logs on
  macOS). BUN_BIN is resolved with command -v bun at install time.
  HOME and PATH are set explicitly so the child process can find
  the user's ~/.env and runtime helpers like mpg123. The unit
  passes systemd-analyze verify with no warnings.
- daemon-reload + enable --now starts and persists the service.
  Failure prints the systemctl/journalctl commands to diagnose.
- Post-install summary branches by service manager ("launchd" vs
  "systemd --user") and the stale "macOS Say (fallback)" voice
  string is now Darwin-only, matching the honest message PR danielmiessler#1075
  introduced elsewhere.

### start.sh

- Darwin path preserved byte-identical (LaunchAgent existence check,
  launchctl list, launchctl load, START_RC capture).
- Linux/WSL branch checks $SYSTEMD_UNIT_PATH for existence,
  systemctl --user is-active --quiet for the already-running fast
  path, and systemctl --user start otherwise. The "already running"
  hint on Linux points at `systemctl --user restart` instead of the
  macOS-only ./restart.sh.

### stop.sh

- Darwin path preserved byte-identical.
- Linux/WSL branch: systemctl --user is-active --quiet → stop → ok.
  The existing pai_port_pids-based port-8888 cleanup at the tail of
  the script stays common to both platforms (unchanged from danielmiessler#1072).

### status.sh

- Service Status block branches on pai_is_darwin. Linux/WSL reads
  systemctl --user is-active + MainPID, falling back to
  list-unit-files for the installed-but-inactive state, and prints
  "not installed" if neither.
- The Voice Configuration block (Darwin "Using macOS 'say'" vs
  Linux "No TTS fallback") from PR danielmiessler#1075 is untouched.

### uninstall.sh

- Confirmation banner branches so Linux/WSL says "Remove the
  systemd --user unit" instead of "Remove the LaunchAgent".
- Stop-and-remove block branches on pai_is_darwin. Linux/WSL path
  stops the unit, disables it, removes the file, and daemon-reloads.
- The optional log-file cleanup and post-uninstall notes are
  platform-agnostic and unchanged.

### Reference unit

Templated on the working pai-voice.service unit that ships with PAI
on WSL2 (Description, After, Type, Restart, StandardOutput/Error
format, WantedBy). Differences from the reference:

- WorkingDirectory uses ${SCRIPT_DIR} instead of %h/.claude/VoiceServer
  so fork checkouts at any path work correctly.
- ExecStart uses $(command -v bun) instead of %h/.bun/bin/bun so
  non-default bun install locations work.
- Log path uses pai_log_path (XDG on Linux) instead of
  %h/.claude/VoiceServer/voice-server.log so logs land in the
  XDG-compliant location introduced by danielmiessler#1072.
- Explicit Environment=HOME and Environment=PATH so the service can
  locate ~/.env and runtime helpers (mpg123, ffplay, etc.)
  regardless of how the systemd --user session was launched.

### Idempotency

On a machine with an already-active pai-voice.service unit, re-
running install.sh prompts for reinstall (y/n). Declining exits
cleanly without touching the live unit file or the running process
(verified on the author's machine: PID and unit mtime unchanged
across a full install.sh run with 'n' answer). Accepting will stop,
disable, rewrite, daemon-reload, enable, and start, matching the
exact UX of the macOS reinstall path.

### Verification

On Ubuntu 24.04 / WSL2 with systemd --user:

- bash -n passes on all five modified scripts.
- systemd-analyze verify on the generated unit: clean exit.
- Generated plist content (Darwin branch) is byte-identical to the
  pre-refactor heredoc — confirmed by running the unaltered heredoc
  body with identical stubs and diffing. The Darwin branch only
  gains an enclosing `if pai_is_darwin; then ... fi` wrapper; the
  heredoc body lines are at their original columns so the plist
  written to disk is byte-identical.
- install.sh with an existing unit file + 'n' answer: hits the
  "Installation cancelled" path, exit 0, live unit mtime and the
  running service PID both unchanged.
- start.sh against a live running unit: hits "already running",
  exit 0, live PID unchanged.
- start.sh with a missing unit (fake HOME): hits "Service not
  installed", exit 1, no systemctl invocation.
- status.sh against a live running unit: reports "OK Service is
  active (PID: ...)" with the real MainPID.
- status.sh with a bogus unit name: hits "Service is not installed".
- stop.sh sliced with a bogus unit name: hits "not running" branch,
  no side effects.
- uninstall.sh with 'n' answer: prints Linux-specific confirmation
  banner, "Uninstall cancelled", live state untouched.

Darwin path not exercised on hardware (author has no Mac). The
launchctl/plist code paths are wrapped in `if pai_is_darwin; then`
with the pre-refactor content preserved verbatim, and the generated
plist is confirmed byte-identical. Darwin reviewers please spot-
check the wrapped launchctl flow for any regressions.

Stacked on danielmiessler#1061, danielmiessler#1072, danielmiessler#1075. Rebase onto main after those land.
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