#!/usr/bin/env bash # lib/common.sh — shared helpers (logging, errors, validation). # Sourced by all ssr commands. Never executed directly. set -euo pipefail # Resolve repo root from any sourced location. SSR_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SSR_ROOT="$(cd "${SSR_LIB_DIR}/.." && pwd)" export SSR_ROOT SSR_LIB_DIR # Colors only when stdout is a TTY (no ANSI in launchd logs). if [[ -t 1 ]]; then SSR_C_RED=$'\033[31m'; SSR_C_GRN=$'\033[32m' SSR_C_YLW=$'\033[33m'; SSR_C_BLU=$'\033[34m' SSR_C_DIM=$'\033[2m'; SSR_C_RST=$'\033[0m' else SSR_C_RED=""; SSR_C_GRN=""; SSR_C_YLW=""; SSR_C_BLU=""; SSR_C_DIM=""; SSR_C_RST="" fi ssr::ts() { date '+%Y-%m-%dT%H:%M:%S%z'; } ssr::log() { printf '%s %s[INFO]%s %s\n' "$(ssr::ts)" "$SSR_C_BLU" "$SSR_C_RST" "$*"; } ssr::ok() { printf '%s %s[OK]%s %s\n' "$(ssr::ts)" "$SSR_C_GRN" "$SSR_C_RST" "$*"; } ssr::warn() { printf '%s %s[WARN]%s %s\n' "$(ssr::ts)" "$SSR_C_YLW" "$SSR_C_RST" "$*" >&2; } ssr::err() { printf '%s %s[ERR]%s %s\n' "$(ssr::ts)" "$SSR_C_RED" "$SSR_C_RST" "$*" >&2; } ssr::die() { ssr::err "$@"; exit 1; } # Mirror to syslog if available so launchd runs are findable via `log show`. ssr::syslog() { [[ -x /usr/bin/logger ]] || return 0 /usr/bin/logger -t "ssr" -- "$*" || true } ssr::require_macos() { [[ "$(uname -s)" == "Darwin" ]] || ssr::die "ssr requires macOS (Darwin). Detected: $(uname -s)" } ssr::require_cmd() { command -v "$1" >/dev/null 2>&1 || ssr::die "Required command missing: $1" } # Confirm prompt with optional timeout (returns 0 = yes). ssr::confirm() { local prompt="${1:-Continue?}" timeout="${2:-0}" reply if [[ "$timeout" -gt 0 ]]; then read -r -t "$timeout" -n 1 -p "${prompt} [y/N]: " reply || reply="" else read -r -n 1 -p "${prompt} [y/N]: " reply || reply="" fi echo [[ "${reply,,}" == "y" ]] } # Validate a path is safe (no traversal, no shell metas). Used before any FS op. ssr::validate_path() { local p="$1" [[ -n "$p" ]] || ssr::die "Empty path" [[ "$p" != *$'\n'* && "$p" != *$'\0'* ]] || ssr::die "Path contains control chars" case "$p" in *..*) ssr::die "Path traversal rejected: $p" ;; esac } ssr::ensure_dir() { local d="$1" ssr::validate_path "$d" [[ -d "$d" ]] || mkdir -p "$d" } # Source config file if it exists. Validates that only known KEY=VALUE lines # are present to avoid arbitrary code execution from a tampered config. ssr::load_config() { local cfg="${1:-${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}}" export SSR_CONFIG="$cfg" [[ -f "$cfg" ]] || return 0 if grep -Eq -v '^[[:space:]]*(#|$|[A-Z_][A-Z0-9_]*=)' "$cfg"; then ssr::die "Config $cfg contains invalid lines (only KEY=VALUE allowed)" fi # shellcheck disable=SC1090 source "$cfg" }