#!/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 — show each candidate and confirm per-item # --------------------------------------------------------------------------- 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[*]} " local declared declared="$(_ssr_mackup_declared_paths)" # Returns true when is declared exactly OR when any built-in declares # a sub-path of (e.g. .byobu/profile covers .byobu, .claude/settings # covers .claude, etc.). # Uses `command grep` (bypasses colour aliases) and a bash glob for the # prefix check (avoids regex escaping issues with dots in paths). _is_declared() { local p="$1" decl command grep -qxF "$p" <<< "$declared" && return 0 while IFS= read -r decl; do [[ "$decl" == "$p/"* ]] && return 0 done <<< "$declared" return 1 } _in_backup() { [[ -n "$mackup_dir" && -e "$mackup_dir/$1" ]]; } # ----------------------------------------------------------------------- # 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) # ----------------------------------------------------------------------- # Stale ssr-*.cfg files: exist but now covered by a built-in definition. # ----------------------------------------------------------------------- local -a stale_cfgs=() 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" && stale_cfgs+=("$stale_cfg") done # ----------------------------------------------------------------------- # Filter to NEW items only (user owns existing files). # ----------------------------------------------------------------------- local -a new_items=() local -a new_cfgs=() 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 new_items+=("$it") new_cfgs+=("$cfg_file") done if [[ ${#new_items[@]} -eq 0 && ${#stale_cfgs[@]} -eq 0 ]]; then ssr::ok "All ~/.mackup/ssr-*.cfg files are up-to-date — nothing to do." return 0 fi printf '\n' local written=0 removed=0 # ----------------------------------------------------------------------- # New files — confirm each individually. # ----------------------------------------------------------------------- if [[ ${#new_items[@]} -gt 0 ]]; then printf '\033[1mNew Mackup app definitions\033[0m (%d candidate(s))\n\n' "${#new_items[@]}" local i for i in "${!new_items[@]}"; do it="${new_items[$i]}" cfg_file="${new_cfgs[$i]}" local cfg_name="${cfg_file##*/}" # Resolved paths preview. local resolved; resolved="$(_ssr_mackup_resolve_paths "$it")" local line_count; line_count="$(printf '%s\n' "$resolved" | wc -l | tr -d ' ')" # Header for this item. printf ' \033[32m+\033[0m \033[1m~/.mackup/%s\033[0m\n' "$cfg_name" if [[ "$line_count" -eq 1 && "$resolved" == "$it" ]]; then printf ' backs up: ~/%s/ (whole directory)\n' "$it" else printf ' backs up: %d path(s) inside ~/%s/\n' "$line_count" "$it" printf '%s\n' "$resolved" | while IFS= read -r p; do printf ' · ~/%s\n' "$p" done fi printf '\n' if ssr::confirm " Create ~/.mackup/$cfg_name?" 30; then _ssr_mackup_write_cfg "$cfg_dir" "$it" written=$((written + 1)) printf ' \033[32m✓ written\033[0m\n\n' else printf ' skipped\n\n' fi done fi # ----------------------------------------------------------------------- # Stale files — confirm each removal individually. # ----------------------------------------------------------------------- if [[ ${#stale_cfgs[@]} -gt 0 ]]; then printf '\033[1mStale duplicates\033[0m (now covered by a Mackup built-in)\n\n' local f for f in "${stale_cfgs[@]}"; do printf ' \033[31m-\033[0m \033[1m%s\033[0m\n' "~/.mackup/${f##*/}" # Show which built-in covers it. stale_path="$(awk '/^\[configuration_files\]/{f=1;next}/^\[/{f=0}f&&/^[^#[:space:]]/{print $1;exit}' "$f")" printf ' path \033[2m%s\033[0m is now declared by a built-in Mackup app\n\n' "$stale_path" if ssr::confirm " Remove ${f##*/}?" 30; then rm -f "$f" removed=$((removed + 1)) printf ' \033[31m✓ removed\033[0m\n\n' else printf ' skipped\n\n' fi done fi # ----------------------------------------------------------------------- # Summary. # ----------------------------------------------------------------------- if [[ $written -gt 0 || $removed -gt 0 ]]; then ssr::ok "Done — $written written, $removed removed." [[ $written -gt 0 ]] && printf '\nEdit ~/.mackup/ssr-*.cfg directly to adjust paths.\n' printf 'Run \033[1mssr sync\033[0m to back up with the updated definitions.\n\n' else printf 'Nothing applied.\n' fi } # --------------------------------------------------------------------------- # 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