#!/usr/bin/env bash # ssr-mackup — Manage auto-generated Mackup app definitions for dot-dirs. # # Subcommands: # scan Dry-run: show which ssr-*.cfg files would be created / updated, # then ask for confirmation before writing anything. # list List existing ~/.mackup/ssr-*.cfg files with the path they cover. # clean Remove all generated ssr-*.cfg files (prompts first). 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/mackup.sh source "$SSR_ROOT/lib/mackup.sh" ssr::require_macos ssr::load_config usage() { cat < Manage auto-generated Mackup app definitions for dot-dirs not covered by Mackup's built-in app definitions. Subcommands: scan Show suggested ~/.mackup/ssr-*.cfg files and confirm before writing. list List existing generated files. clean Remove all generated ssr-*.cfg files. Run 'ssr mackup scan' once after install (or whenever new dot-dirs appear) and commit the generated files alongside your dotfiles. EOF } # --------------------------------------------------------------------------- # scan — dry-run then confirm # --------------------------------------------------------------------------- cmd_scan() { local mackup_dir mackup_dir="$(ssr::cloud::mackup_dir 2>/dev/null)" \ || mackup_dir="" # allow offline use; _in_backup will just never match local cfg_dir="$HOME/.mackup" mkdir -p "$cfg_dir" local skip_set=" ${_SSR_MACKUP_SKIP[*]} " _in_backup() { [[ -n "$mackup_dir" && -e "$mackup_dir/$1" ]]; } _is_declared() { printf '%s\n' "$declared" | grep -qxF "$1"; } local declared declared="$(_ssr_mackup_declared_paths)" # Collect candidate items (same logic as generate_custom_cfg). local -a items=() local item for item in "${_SSR_MACKUP_CURATED[@]}" ${SSR_MACKUP_EXTRA_PATHS:-}; do [[ -e "$HOME/$item" ]] || continue _is_declared "$item" && continue _in_backup "$item" && continue items+=("$item") done local name while IFS= read -r dotdir; do name="${dotdir##*/}" [[ "$skip_set" == *" $name "* ]] && continue _is_declared "$name" && continue _in_backup "$name" && continue local dup=false for item in "${items[@]:-}"; do [[ "$item" == "$name" ]] && dup=true && break; done $dup && continue items+=("$name") done < <(find "$HOME" -maxdepth 1 -mindepth 1 -name '.*' -type d 2>/dev/null | sort) # Check existing stale duplicates. local -a to_remove=() local stale_cfg stale_path for stale_cfg in "$cfg_dir"/ssr-*.cfg; do [[ -f "$stale_cfg" ]] || continue stale_path="$(awk '/^\[configuration_files\]/{f=1;next}/^\[/{f=0}f&&/^[^#[:space:]]/{print $1;exit}' "$stale_cfg")" _is_declared "$stale_path" && to_remove+=("$stale_cfg") done # Only suggest items that have NO cfg file yet. # Existing files are user-owned — edit them directly in ~/.mackup/. local -a to_write=() local -a cfg_paths=() local it app_name cfg_file for it in "${items[@]:-}"; do app_name="$(_ssr_mackup_cfg_name "$it")" cfg_file="$cfg_dir/${app_name}.cfg" [[ -f "$cfg_file" ]] && continue # user owns it — skip to_write+=("$it") cfg_paths+=("$cfg_file") done # Nothing to do? if [[ ${#to_write[@]} -eq 0 && ${#to_remove[@]} -eq 0 ]]; then ssr::ok "All ~/.mackup/ssr-*.cfg files are up-to-date — nothing to do." return 0 fi # Show plan. if [[ ${#to_write[@]} -gt 0 ]]; then printf '\nSuggested Mackup app definitions:\n' local i for i in "${!to_write[@]}"; do local path="${cfg_paths[$i]##*/}" # Show resolved paths (selective default or whole dir). local resolved; resolved="$(_ssr_mackup_resolve_paths "${to_write[$i]}")" local line_count; line_count="$(printf '%s\n' "$resolved" | wc -l | tr -d ' ')" local paths_hint if [[ "$line_count" -eq 1 && "$resolved" == "${to_write[$i]}" ]]; then paths_hint="${to_write[$i]}/" else paths_hint="$line_count sub-path(s) of ${to_write[$i]}/" fi printf " \033[32mNEW\033[0m %-38s %s\n" \ "~/.mackup/$path" "$paths_hint" done printf '\n\033[2mEdit the generated files directly in ~/.mackup/ to customise paths.\033[0m\n' fi if [[ ${#to_remove[@]} -gt 0 ]]; then printf '\nStale duplicates to remove (now covered by built-in):\n' local f for f in "${to_remove[@]}"; do printf " \033[31mDEL\033[0m %s\n" "~/.mackup/${f##*/}" done fi printf '\n' # Confirm. local total=$(( ${#to_write[@]} + ${#to_remove[@]} )) ssr::confirm "Apply $total change(s)?" 60 || { printf 'Aborted.\n'; return 0; } # Write new files (never overwrites existing ones). for it in "${to_write[@]:-}"; do _ssr_mackup_write_cfg "$cfg_dir" "$it" done # Remove stale duplicates. for f in "${to_remove[@]:-}"; do rm -f "$f" ssr::log "Removed stale: ${f##*/}" done ssr::ok "Done — ${#to_write[@]} written, ${#to_remove[@]} removed." printf '\nEdit ~/.mackup/ssr-*.cfg directly to customise paths.\n' printf 'Run \033[1mssr sync\033[0m to back up with the new definitions.\n\n' } # --------------------------------------------------------------------------- # list — show existing generated files # --------------------------------------------------------------------------- cmd_list() { local cfg_dir="$HOME/.mackup" local found=0 cfg path for cfg in "$cfg_dir"/ssr-*.cfg; do [[ -f "$cfg" ]] || continue path="$(awk '/^\[configuration_files\]/{f=1;next}/^\[/{f=0}f&&/^[^#[:space:]]/{print $1;exit}' "$cfg")" printf ' %-42s %s\n' "~/.mackup/${cfg##*/}" "$path" found=$((found + 1)) done if [[ $found -eq 0 ]]; then printf 'No generated ssr-*.cfg files found in ~/.mackup/\n' printf 'Run: ssr mackup scan\n' else printf '\n%d file(s) total.\n' "$found" fi } # --------------------------------------------------------------------------- # clean — remove all generated files # --------------------------------------------------------------------------- cmd_clean() { local cfg_dir="$HOME/.mackup" local -a found=() local cfg for cfg in "$cfg_dir"/ssr-*.cfg; do [[ -f "$cfg" ]] && found+=("$cfg") done if [[ ${#found[@]} -eq 0 ]]; then ssr::ok "No generated ssr-*.cfg files to remove." return 0 fi printf 'Will remove %d file(s):\n' "${#found[@]}" for cfg in "${found[@]}"; do printf ' %s\n' "~/.mackup/${cfg##*/}" done printf '\n' ssr::confirm "Remove all generated ssr-*.cfg files?" 60 || { printf 'Aborted.\n'; return 0; } for cfg in "${found[@]}"; do rm -f "$cfg"; done ssr::ok "Removed ${#found[@]} file(s)." } # --------------------------------------------------------------------------- # Dispatch # --------------------------------------------------------------------------- [[ $# -gt 0 ]] || { usage; exit 1; } case "$1" in scan) cmd_scan ;; list) cmd_list ;; clean) cmd_clean ;; -h|--help) usage ;; *) ssr::err "Unknown subcommand: $1"; usage; exit 1 ;; esac