diff --git a/bin/ssr b/bin/ssr index 5f8b18f..6614f5d 100755 --- a/bin/ssr +++ b/bin/ssr @@ -34,6 +34,7 @@ Commands: config show|edit Print or edit the active config file. completions Install or remove shell completions (zsh/bash/fish). status Show last sync time, cloud target, and scheduled state. + mackup scan|list|clean Manage auto-generated Mackup app definitions. version Print version. Options: @@ -62,7 +63,7 @@ done cmd="$1"; shift case "$cmd" in - install|sync|restore|update|schedule|config|completions|status) + install|sync|restore|update|schedule|config|completions|status|mackup) target="$SSR_BIN_DIR/ssr-$cmd" [[ -x "$target" ]] || ssr::die "Subcommand not executable: $target" exec "$target" "$@" diff --git a/bin/ssr-mackup b/bin/ssr-mackup new file mode 100755 index 0000000..6a64f5e --- /dev/null +++ b/bin/ssr-mackup @@ -0,0 +1,219 @@ +#!/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 + + # Classify each candidate: NEW or UPD (content differs from existing). + local -a to_write=() + local -a labels=() + local -a cfg_paths=() + + local it app_name desired existing cfg_file + for it in "${items[@]:-}"; do + app_name="$(_ssr_mackup_cfg_name "$it")" + cfg_file="$cfg_dir/${app_name}.cfg" + desired="$(printf '[application]\nname = %s\n\n[configuration_files]\n%s\n' "$app_name" "$it")" + if [[ -f "$cfg_file" ]]; then + existing="$(cat "$cfg_file")" + if [[ "$existing" == "$desired" ]]; then + continue # already up-to-date, skip + fi + labels+=("UPD") + else + labels+=("NEW") + fi + 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 label="${labels[$i]}" + local path="${cfg_paths[$i]##*/}" + local colour + [[ "$label" == "NEW" ]] && colour="\033[32m" || colour="\033[33m" + printf " ${colour}%s\033[0m %-38s (%s)\n" \ + "$label" "~/.mackup/$path" "${to_write[$i]}" + done + 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/updated files. + 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 '\nRun \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 diff --git a/bin/ssr-sync b/bin/ssr-sync index fa5ed4d..c3da91b 100755 --- a/bin/ssr-sync +++ b/bin/ssr-sync @@ -48,12 +48,6 @@ ssr::brew::upgrade # Timeout (seconds) is configurable; default 180s. ssr::icloud::wait "$mackup_dir" "${SSR_ICLOUD_WAIT_TIMEOUT:-180}" -# Auto-generate ~/.mackup/ssr-custom.cfg for dot-dirs not yet covered by -# Mackup's built-in definitions (e.g. .claude, .cursor, .easymcp, …). -if [[ "${SSR_MACKUP_AUTODISCOVER:-true}" == "true" ]]; then - ssr::mackup::generate_custom_cfg "$mackup_dir" -fi - ssr::mackup::backup ssr::brew::dump "$backup_dir" ssr::brew::cleanup diff --git a/completions/_ssr b/completions/_ssr index 608b082..f019e65 100644 --- a/completions/_ssr +++ b/completions/_ssr @@ -15,6 +15,7 @@ _ssr() { 'config:Show or edit the active config file' 'completions:Install or remove shell completions' 'status:Show last sync time, cloud target, schedule state' + 'mackup:Manage auto-generated Mackup app definitions' 'version:Print version' 'help:Show top-level help' ) @@ -59,6 +60,12 @@ _ssr() { 'uninstall[Remove shell completions]' \ 'status[Show installed completions]' ;; + mackup) + _values 'mackup action' \ + 'scan[Suggest and confirm ssr-*.cfg files]' \ + 'list[List generated ssr-*.cfg files]' \ + 'clean[Remove all generated ssr-*.cfg files]' + ;; esac ;; esac diff --git a/completions/ssr.bash b/completions/ssr.bash index e6f98d4..59537fa 100644 --- a/completions/ssr.bash +++ b/completions/ssr.bash @@ -13,7 +13,7 @@ _ssr() { cword=$COMP_CWORD fi - local commands="install sync restore update schedule config completions status version help" + local commands="install sync restore update schedule config completions status mackup version help" local global_flags="-c --config -h --help" # Detect command (first non-flag word after `ssr`). @@ -60,6 +60,9 @@ _ssr() { completions) COMPREPLY=( $(compgen -W "install uninstall status" -- "$cur") ) ;; + mackup) + COMPREPLY=( $(compgen -W "scan list clean" -- "$cur") ) + ;; *) COMPREPLY=() ;; diff --git a/completions/ssr.fish b/completions/ssr.fish index b9ac255..963619d 100644 --- a/completions/ssr.fish +++ b/completions/ssr.fish @@ -1,7 +1,7 @@ # fish completion for ssr — System Sync & Restore. # Install: drop into ~/.config/fish/completions/ssr.fish -set -l ssr_cmds install sync restore update schedule config completions status version help +set -l ssr_cmds install sync restore update schedule config completions status mackup version help # True when no subcommand has been picked yet (only flags / option-args seen so far). function __ssr_needs_subcmd @@ -69,3 +69,5 @@ complete -c ssr -n '__ssr_using_subcmd update' -s n -l dry-run -d 'Preview with complete -c ssr -n '__ssr_using_subcmd schedule' -a 'on off status' complete -c ssr -n '__ssr_using_subcmd config' -a 'show edit path' complete -c ssr -n '__ssr_using_subcmd completions' -a 'install uninstall status' +complete -c ssr -n __ssr_needs_subcmd -a mackup -d 'Manage Mackup app definitions' +complete -c ssr -n '__ssr_using_subcmd mackup' -a 'scan list clean'