c31338dba9
- Updated `ssr sync` and `ssr restore` commands to use `mackup backup` and `mackup restore`, replacing the previous symlink-based approach. - Added guidance for users with legacy symlink setups to run `ssr mackup unlink` for recovery. - Revised documentation in README, completions, and configuration examples to reflect the new copy-based methodology for improved reliability on newer macOS versions. - Enhanced comments in the codebase to clarify the changes and their implications for users. Co-authored-by: Cursor <cursoragent@cursor.com>
286 lines
11 KiB
Bash
Executable File
286 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ssr-mackup — Manage auto-generated Mackup app definitions for dot-dirs.
|
|
#
|
|
# Subcommands:
|
|
# scan Per-item prompts for new ~/.mackup/ssr-*.cfg files.
|
|
# list List existing ~/.mackup/ssr-*.cfg files with the path they cover.
|
|
# clean Remove all generated ssr-*.cfg files (prompts first).
|
|
# unlink Run `mackup link uninstall` — only for undoing symlink-based Mackup
|
|
# (legacy SSR or manual `mackup link`); copies from storage and removes symlinks.
|
|
|
|
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 <<EOF
|
|
Usage: ssr mackup <subcommand>
|
|
|
|
Manage auto-generated Mackup app definitions for dot-dirs not covered
|
|
by Mackup's built-in app definitions.
|
|
|
|
Subcommands:
|
|
scan Suggest new ~/.mackup/ssr-*.cfg files (confirm each item).
|
|
list List existing generated files.
|
|
clean Remove all generated ssr-*.cfg files.
|
|
unlink Undo symlink-based Mackup only (`mackup link uninstall`).
|
|
|
|
Run 'ssr mackup scan' when new dot-dirs appear; commit ~/.mackup/ssr-*.cfg
|
|
with your dotfiles if you use them.
|
|
|
|
If you previously used symlink-based Mackup and apps break, run:
|
|
ssr mackup unlink
|
|
Current SSR uses copy-based `mackup backup` / `restore` instead.
|
|
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 <path> is declared exactly OR when any built-in declares
|
|
# a sub-path of <path> (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)."
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# unlink — mackup link uninstall (recovery)
|
|
# ---------------------------------------------------------------------------
|
|
cmd_unlink() {
|
|
cat <<'EOF'
|
|
This runs: mackup -f link uninstall
|
|
|
|
Mackup copies each managed path from your Mackup storage folder back into
|
|
$HOME and removes the symlinks. Use only if you still have symlink-based
|
|
Mackup from an older SSR or from manual `mackup link` / `link install`.
|
|
|
|
After unlink, apps use normal local files again. Ongoing `ssr sync` uses
|
|
`mackup backup` (copy to cloud), not symlinks.
|
|
EOF
|
|
printf '\n'
|
|
ssr::confirm "Run mackup link uninstall now?" 120 || { printf 'Aborted.\n'; return 0; }
|
|
ssr::mackup::link_uninstall && ssr::ok "mackup link uninstall finished."
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch
|
|
# ---------------------------------------------------------------------------
|
|
[[ $# -gt 0 ]] || { usage; exit 1; }
|
|
|
|
case "$1" in
|
|
scan) cmd_scan ;;
|
|
list) cmd_list ;;
|
|
clean) cmd_clean ;;
|
|
unlink) cmd_unlink ;;
|
|
-h|--help) usage ;;
|
|
*) ssr::err "Unknown subcommand: $1"; usage; exit 1 ;;
|
|
esac
|