539a8fc28f
Convert the legacy system_sync.sh and system_restore.sh into a modular shareable repository: - bin/ssr CLI dispatcher with install/sync/restore/schedule/config/status - lib/ split: common, cloud, brew, mackup, macos - Cloud provider abstraction (icloud, dropbox, googledrive, onedrive, custom) - launchd template for scheduled daily sync (renderable via sed) - ssr.conf.example with whitelisted KEY=VALUE config loader - macos-defaults.sh extracted from inline block, overridable - install.sh bootstrap symlinks CLI into ~/.local/bin - README with usage, config table, migration map, security notes - Original scripts archived under legacy/ for reference Fixes vs originals: set -euo pipefail, quoted vars, find -print0, spctl --global-disable (was --master-disable), MACOS=".macos" bug, zero-width char before ln, Apple Silicon brew shellenv, config injection hardening. Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.8 KiB
Bash
83 lines
2.8 KiB
Bash
#!/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"
|
|
}
|