421b802467
- Corrected the backup path in the `_in_backup` function to reflect the new structure under `mackup/`. - Improved error handling in `ssr::mackup::configure` to skip restore if the configuration file is not updated, providing user feedback. - Updated `ssr::sync` to conditionally execute backup based on configuration validity, enhancing robustness. - Revised documentation in configuration examples to clarify the new Mackup directory structure and its implications for users.
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/mackup/$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
|