#!/bin/bash # daemons - observe and control the homegrown launchd daemons: activity logs + lifecycle # # Usage: # daemons append [detail|--detail-stdin] Append one activity record # daemons status Summarize all daemons (loaded? last activity + counts) # daemons check Health+activity gate (returns nonzero on problem) # daemons log [name|-a|--all] [-f|--follow] Render the log (human-readable) # daemons query [name|-a|--all] [-e|--event E] [-s|--since T] [-j|--jq EXPR] Filter/extract JSONL records # daemons load [--plist PATH] [-n|--dry-run] Bootstrap (reload) a daemon into launchd; logs a load record # daemons unload [-n|--dry-run] Bootout a daemon from launchd; logs an unload record # daemons registry [--edit|--open] Print the registry path, or open it (--edit: $VISUAL/$EDITOR, --open: macOS open) # daemons -h | --help # # Environment: # DAEMONS_LOG_DIR Directory of .log files (default: $XDG_STATE_HOME/daemons or ~/.local/state/daemons) # DAEMONS_REGISTRY Registry TSV (default: $XDG_CONFIG_HOME/daemons/daemons.tsv or ~/.config/daemons/daemons.tsv) PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" # LaunchDaemons get a minimal PATH; need jq _daemons() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="daemons" ;; esac local LOG_DIR="${DAEMONS_LOG_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/daemons}" local REGISTRY="${DAEMONS_REGISTRY:-${XDG_CONFIG_HOME:-$HOME/.config}/daemons/daemons.tsv}" _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } _expand_short_opts() { # $1 = string of short-opt letters that take a value (e.g. "nXHd"); "" for flag-only scripts # $2..$N = "$@" # Populates _EXPANDED; caller does: set -- "${_EXPANDED[@]}"; unset _EXPANDED local value_opts="$1"; shift _EXPANDED=() local passthru="" local arg local rest local c for arg in "$@"; do if [ -n "$passthru" ]; then _EXPANDED+=("$arg"); continue; fi case "$arg" in --) passthru=1; _EXPANDED+=("$arg") ;; --*|-|"") _EXPANDED+=("$arg") ;; -[a-zA-Z]?*) rest="${arg#-}" while [ -n "$rest" ]; do c="${rest%"${rest#?}"}"; rest="${rest#?}" _EXPANDED+=("-$c") case "$value_opts" in *"$c"*) [ -n "$rest" ] && _EXPANDED+=("$rest") rest="" ;; esac done ;; *) _EXPANDED+=("$arg") ;; esac done } _show_help() { local s; [ -t 1 ] && s=$'\033[4m' local r; [ -t 1 ] && r=$'\033[24m' cat < <${s}event${r}> [${s}detail${r}|--detail-stdin] $SCRIPT_NAME status $SCRIPT_NAME check $SCRIPT_NAME log [${s}name${r}|-a|--all] [-f|--follow] $SCRIPT_NAME query [${s}name${r}|-a|--all] [-e|--event ${s}E${r}] [-s|--since ${s}T${r}] [-j|--jq ${s}EXPR${r}] $SCRIPT_NAME load <${s}name${r}> [--plist ${s}PATH${r}] [-n|--dry-run] $SCRIPT_NAME unload <${s}name${r}> [-n|--dry-run] $SCRIPT_NAME registry [--edit|--open] DESCRIPTION Writes and reads per-daemon activity logs (JSONL) under \$DAEMONS_LOG_DIR. Each record is {ts, daemon, event, detail}; event is trigger|noop|change|error|load|unload. 'append' is the write path daemons call; 'status'/'check'/'log'/'query' read; 'load'/'unload' control launchd and log their own record; 'registry' reveals the registry. SUBCOMMANDS append Append one activity record (creates the log dir on first write). Pass --detail-stdin in place of [detail] to read the detail from stdin status Per-daemon summary: loaded? + last activity (event + timestamp) + all-time event counts; "never fired" when no log check Health+activity gate; nonzero + loud alerts on any problem log Render the log human-readably (-a/--all merges; -f/--follow follows) query Filter/extract JSONL by -e/--event, -s/--since, -a/--all, -j/--jq -s/--since accepts NNN[smh] (e.g. 1h, 30m), a bare date YYYY-MM-DD (midnight UTC), or a full ISO-8601 UTC timestamp load Bootstrap a daemon into launchd (bootout-then-bootstrap, so it also reloads). Resolves the plist by --plist > registry column > convention, then verifies it exists. A system/ target elevates via sudo when not root. Logs a 'load' record unload Bootout a daemon from launchd (idempotent; "already unloaded" is not an error). Logs an 'unload' record registry Print the resolved \$DAEMONS_REGISTRY path; --edit opens it in \$VISUAL/\$EDITOR (default vi), --open uses macOS open. --edit and --open are mutually exclusive ENVIRONMENT DAEMONS_LOG_DIR Directory of .log files (default: \$XDG_STATE_HOME/daemons or ~/.local/state/daemons) DAEMONS_REGISTRY Registry TSV (default: \$XDG_CONFIG_HOME/daemons/daemons.tsv or ~/.config/daemons/daemons.tsv) FILES \$DAEMONS_REGISTRY Whitespace-separated table that 'status', 'check', 'load', and 'unload' read, and 'registry' prints (append/log/query don't need it). A header row, then one row per daemon with four required columns and an optional fifth: name log name; matches the passed to append domain launchd domain: gui/\$UID/ (per-user agent; \$UID is replaced with your numeric uid) or system/ label launchd label; state read via launchctl print