Initial ssr repository
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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# ssr — System Sync & Restore CLI dispatcher.
|
||||
# Resolves the repository root, sources libs, and forwards to a subcommand.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve script directory even when invoked via a symlink (e.g. /usr/local/bin/ssr).
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [[ -L "$SOURCE" ]]; do
|
||||
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
|
||||
done
|
||||
SSR_BIN_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
||||
SSR_ROOT="$(cd "$SSR_BIN_DIR/.." && pwd)"
|
||||
export SSR_ROOT SSR_BIN_DIR
|
||||
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SSR_ROOT/lib/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
ssr — System Sync & Restore for macOS
|
||||
|
||||
Usage:
|
||||
ssr <command> [options]
|
||||
|
||||
Commands:
|
||||
install First-time install: links binary, seeds config, schedules sync.
|
||||
sync Run a backup: brew bundle dump + mackup backup → cloud.
|
||||
restore [Brewfile] Restore a machine: brew bundle + mackup restore + macOS defaults.
|
||||
schedule on|off Enable/disable the scheduled launchd sync job.
|
||||
config show|edit Print or edit the active config file.
|
||||
status Show last sync time, cloud target, and scheduled state.
|
||||
version Print version.
|
||||
|
||||
Options:
|
||||
-c, --config PATH Use a specific config file (default: ~/.config/ssr/ssr.conf).
|
||||
-h, --help Show this help.
|
||||
|
||||
Environment:
|
||||
SSR_CONFIG Same as --config.
|
||||
|
||||
Docs: see README.md in the repository root.
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse global flags.
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c|--config) export SSR_CONFIG="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--) shift; break ;;
|
||||
-*) ssr::die "Unknown global flag: $1 (use -h for help)" ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $# -gt 0 ]] || { usage; exit 1; }
|
||||
|
||||
cmd="$1"; shift
|
||||
case "$cmd" in
|
||||
install|sync|restore|schedule|config|status)
|
||||
target="$SSR_BIN_DIR/ssr-$cmd"
|
||||
[[ -x "$target" ]] || ssr::die "Subcommand not executable: $target"
|
||||
exec "$target" "$@"
|
||||
;;
|
||||
version|-v|--version)
|
||||
cat "$SSR_ROOT/VERSION" 2>/dev/null || echo "dev"
|
||||
;;
|
||||
help|-h|--help) usage ;;
|
||||
*) ssr::err "Unknown command: $cmd"; usage; exit 1 ;;
|
||||
esac
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# ssr-config — show or edit the active configuration file.
|
||||
|
||||
set -euo pipefail
|
||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SSR_ROOT/lib/common.sh"
|
||||
|
||||
cfg="${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}"
|
||||
|
||||
case "${1:-show}" in
|
||||
show)
|
||||
if [[ -f "$cfg" ]]; then
|
||||
echo "# $cfg"
|
||||
cat "$cfg"
|
||||
else
|
||||
ssr::warn "No config found at $cfg"
|
||||
echo "Run: ssr install to seed one from the example."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
edit)
|
||||
ssr::ensure_dir "$(dirname "$cfg")"
|
||||
if [[ ! -f "$cfg" ]]; then
|
||||
cp "$SSR_ROOT/config/ssr.conf.example" "$cfg"
|
||||
chmod 600 "$cfg"
|
||||
fi
|
||||
"${EDITOR:-vi}" "$cfg"
|
||||
;;
|
||||
path)
|
||||
echo "$cfg"
|
||||
;;
|
||||
*)
|
||||
ssr::die "Usage: ssr config [show|edit|path]"
|
||||
;;
|
||||
esac
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# ssr-install — local install: symlink CLI, seed config, optionally schedule.
|
||||
|
||||
set -euo pipefail
|
||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SSR_ROOT/lib/common.sh"
|
||||
|
||||
ssr::require_macos
|
||||
|
||||
PREFIX="${SSR_PREFIX:-$HOME/.local/bin}"
|
||||
CONFIG_DIR="${SSR_CONFIG_DIR:-$HOME/.config/ssr}"
|
||||
CONFIG_FILE="$CONFIG_DIR/ssr.conf"
|
||||
|
||||
ssr::log "Installing ssr"
|
||||
ssr::log " prefix: $PREFIX"
|
||||
ssr::log " config: $CONFIG_FILE"
|
||||
|
||||
ssr::ensure_dir "$PREFIX"
|
||||
ssr::ensure_dir "$CONFIG_DIR"
|
||||
|
||||
# Symlink the main CLI so `ssr` works from anywhere.
|
||||
ln -sfn "$SSR_ROOT/bin/ssr" "$PREFIX/ssr"
|
||||
ssr::ok "Linked $PREFIX/ssr → $SSR_ROOT/bin/ssr"
|
||||
|
||||
# Seed config from the example (do not overwrite an existing user config).
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
cp "$SSR_ROOT/config/ssr.conf.example" "$CONFIG_FILE"
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
ssr::ok "Seeded $CONFIG_FILE (chmod 600)"
|
||||
else
|
||||
ssr::log "Config already exists, leaving untouched: $CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Path hint if PREFIX is not on PATH.
|
||||
case ":$PATH:" in
|
||||
*":$PREFIX:"*) ;;
|
||||
*) ssr::warn "$PREFIX is not on your PATH — add it to your shell rc:"
|
||||
echo " export PATH=\"$PREFIX:\$PATH\"" ;;
|
||||
esac
|
||||
|
||||
# Offer to enable the scheduled sync.
|
||||
if ssr::confirm "Enable daily scheduled sync (launchd)?" 30; then
|
||||
"$SSR_ROOT/bin/ssr-schedule" on
|
||||
fi
|
||||
|
||||
ssr::ok "Install complete. Run: ssr status"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# ssr-restore — restore brew packages, mackup data, macOS defaults.
|
||||
|
||||
set -euo pipefail
|
||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SSR_ROOT/lib/common.sh"
|
||||
# shellcheck source=../lib/cloud.sh
|
||||
source "$SSR_ROOT/lib/cloud.sh"
|
||||
# shellcheck source=../lib/brew.sh
|
||||
source "$SSR_ROOT/lib/brew.sh"
|
||||
# shellcheck source=../lib/mackup.sh
|
||||
source "$SSR_ROOT/lib/mackup.sh"
|
||||
# shellcheck source=../lib/macos.sh
|
||||
source "$SSR_ROOT/lib/macos.sh"
|
||||
|
||||
ssr::require_macos
|
||||
ssr::load_config
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: ssr restore [BREWFILE]
|
||||
|
||||
If BREWFILE is omitted, the latest cloud backup is used:
|
||||
<cloud>/BACKUP/<host> - <uuid>/Brewfile
|
||||
EOF
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && { usage; exit 0; }
|
||||
|
||||
ssr::log "ssr restore starting"
|
||||
ssr::cloud::ensure_available
|
||||
backup_dir="$(ssr::cloud::backup_dir)"
|
||||
mackup_dir="$(ssr::cloud::mackup_dir)"
|
||||
|
||||
brewfile="${1:-$backup_dir/Brewfile}"
|
||||
[[ -f "$brewfile" ]] || ssr::die "Brewfile not found: $brewfile"
|
||||
ssr::log "Using Brewfile: $brewfile"
|
||||
|
||||
ssr::confirm "Configure iCloud / cloud drive and proceed with restore?" 60 \
|
||||
|| ssr::die "Aborted by user"
|
||||
|
||||
ssr::macos::disable_gatekeeper
|
||||
ssr::macos::install_rosetta
|
||||
|
||||
ssr::brew::ensure_installed
|
||||
ssr::brew::update
|
||||
ssr::brew::restore "$brewfile"
|
||||
|
||||
ssr::mackup::ensure_installed
|
||||
ssr::mackup::configure "$mackup_dir"
|
||||
ssr::mackup::restore
|
||||
|
||||
ssr::macos::open_installed_casks
|
||||
ssr::macos::apply_defaults
|
||||
|
||||
ssr::ok "ssr restore completed"
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# ssr-schedule — manage the launchd scheduled sync job.
|
||||
|
||||
set -euo pipefail
|
||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SSR_ROOT/lib/common.sh"
|
||||
|
||||
ssr::require_macos
|
||||
ssr::load_config
|
||||
|
||||
LABEL="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
||||
PLIST_SRC="$SSR_ROOT/launchd/${LABEL}.plist"
|
||||
PLIST_DST="$HOME/Library/LaunchAgents/${LABEL}.plist"
|
||||
LOG_DIR="${SSR_LOG_DIR:-$HOME/.local/state/ssr/logs}"
|
||||
INTERVAL_HOUR="${SSR_SCHEDULE_HOUR:-9}"
|
||||
INTERVAL_MIN="${SSR_SCHEDULE_MINUTE:-0}"
|
||||
SSR_BIN="$SSR_ROOT/bin/ssr"
|
||||
|
||||
render_plist() {
|
||||
ssr::ensure_dir "$LOG_DIR"
|
||||
ssr::ensure_dir "$(dirname "$PLIST_DST")"
|
||||
# Substitute placeholders from the template into the user's LaunchAgents dir.
|
||||
sed \
|
||||
-e "s|@@LABEL@@|${LABEL}|g" \
|
||||
-e "s|@@SSR_BIN@@|${SSR_BIN}|g" \
|
||||
-e "s|@@LOG_DIR@@|${LOG_DIR}|g" \
|
||||
-e "s|@@HOUR@@|${INTERVAL_HOUR}|g" \
|
||||
-e "s|@@MINUTE@@|${INTERVAL_MIN}|g" \
|
||||
"$PLIST_SRC" > "$PLIST_DST"
|
||||
chmod 600 "$PLIST_DST"
|
||||
}
|
||||
|
||||
cmd_on() {
|
||||
[[ -f "$PLIST_SRC" ]] || ssr::die "launchd template missing: $PLIST_SRC"
|
||||
render_plist
|
||||
# Reload to pick up changes if it was already loaded.
|
||||
launchctl unload "$PLIST_DST" 2>/dev/null || true
|
||||
launchctl load -w "$PLIST_DST"
|
||||
ssr::ok "Scheduled sync enabled (${INTERVAL_HOUR}:$(printf '%02d' "$INTERVAL_MIN") daily) → $PLIST_DST"
|
||||
}
|
||||
|
||||
cmd_off() {
|
||||
if [[ -f "$PLIST_DST" ]]; then
|
||||
launchctl unload "$PLIST_DST" 2>/dev/null || true
|
||||
rm -f "$PLIST_DST"
|
||||
ssr::ok "Scheduled sync disabled"
|
||||
else
|
||||
ssr::log "Scheduled sync was not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
if [[ -f "$PLIST_DST" ]] && launchctl list | grep -q "$LABEL"; then
|
||||
ssr::ok "Scheduled sync: active (label=$LABEL)"
|
||||
else
|
||||
ssr::log "Scheduled sync: inactive"
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
on|enable) cmd_on ;;
|
||||
off|disable) cmd_off ;;
|
||||
status) cmd_status ;;
|
||||
*) ssr::die "Usage: ssr schedule [on|off|status]" ;;
|
||||
esac
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# ssr-status — print current state: config, cloud target, last sync, schedule.
|
||||
|
||||
set -euo pipefail
|
||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SSR_ROOT/lib/common.sh"
|
||||
# shellcheck source=../lib/cloud.sh
|
||||
source "$SSR_ROOT/lib/cloud.sh"
|
||||
|
||||
ssr::load_config
|
||||
|
||||
cfg="${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}"
|
||||
provider="${SSR_CLOUD_PROVIDER:-icloud}"
|
||||
|
||||
echo "ssr status"
|
||||
echo " repo: $SSR_ROOT"
|
||||
echo " config: $cfg $([[ -f "$cfg" ]] && echo '(present)' || echo '(missing)')"
|
||||
echo " provider: $provider"
|
||||
|
||||
if cloud_dir="$(ssr::cloud::resolve_dir 2>/dev/null)"; then
|
||||
echo " cloud dir: $cloud_dir $([[ -d "$cloud_dir" ]] && echo '(ok)' || echo '(unreachable)')"
|
||||
echo " backup dir: $(ssr::cloud::backup_dir 2>/dev/null || echo 'n/a')"
|
||||
fi
|
||||
|
||||
state_dir="${SSR_STATE_DIR:-$HOME/.local/state/ssr}"
|
||||
if [[ -f "$state_dir/last-sync" ]]; then
|
||||
echo " last sync: $(cat "$state_dir/last-sync")"
|
||||
else
|
||||
echo " last sync: never"
|
||||
fi
|
||||
|
||||
label="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
||||
if launchctl list 2>/dev/null | grep -q "$label"; then
|
||||
echo " schedule: active ($label)"
|
||||
else
|
||||
echo " schedule: inactive"
|
||||
fi
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# ssr-sync — back up Homebrew + Mackup state to the configured cloud dir.
|
||||
|
||||
set -euo pipefail
|
||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SSR_ROOT/lib/common.sh"
|
||||
# shellcheck source=../lib/cloud.sh
|
||||
source "$SSR_ROOT/lib/cloud.sh"
|
||||
# shellcheck source=../lib/brew.sh
|
||||
source "$SSR_ROOT/lib/brew.sh"
|
||||
# shellcheck source=../lib/mackup.sh
|
||||
source "$SSR_ROOT/lib/mackup.sh"
|
||||
|
||||
ssr::require_macos
|
||||
ssr::load_config
|
||||
|
||||
ssr::log "ssr sync starting"
|
||||
ssr::syslog "sync starting"
|
||||
|
||||
ssr::cloud::ensure_available
|
||||
backup_dir="$(ssr::cloud::backup_dir)"
|
||||
mackup_dir="$(ssr::cloud::mackup_dir)"
|
||||
ssr::ensure_dir "$backup_dir"
|
||||
ssr::ensure_dir "$mackup_dir"
|
||||
|
||||
ssr::brew::ensure_installed
|
||||
ssr::mackup::ensure_installed
|
||||
ssr::mackup::configure "$mackup_dir"
|
||||
|
||||
ssr::brew::update
|
||||
ssr::brew::upgrade
|
||||
ssr::mackup::backup
|
||||
ssr::brew::dump "$backup_dir"
|
||||
ssr::brew::cleanup
|
||||
|
||||
# Record sync time so `ssr status` can report freshness.
|
||||
state_dir="${SSR_STATE_DIR:-$HOME/.local/state/ssr}"
|
||||
ssr::ensure_dir "$state_dir"
|
||||
date '+%Y-%m-%dT%H:%M:%S%z' > "$state_dir/last-sync"
|
||||
|
||||
ssr::ok "ssr sync completed → $backup_dir"
|
||||
ssr::syslog "sync completed"
|
||||
Reference in New Issue
Block a user