fix(mackup): correct built-in coverage detection and per-item scan confirm

_ssr_mackup_declared_paths:
- Fix wrong subdir: apps/ → applications/ (coverage was completely broken)
- Use Mackup's bundled Python interpreter (not system python3)
- Track xdg_configuration_files sections separately and prefix paths
  with .config/ so paths like gh/config.yml → .config/gh/config.yml
  are properly recognized as covering .config/gh

_is_declared:
- Use `command grep` to bypass colour aliases
- Replace grep -E regex (dot-escaping issues) with bash glob: == "$p/"*
- Applied to both lib/mackup.sh and bin/ssr-mackup

_SSR_MACKUP_CURATED:
- Remove entries now correctly detected as built-in covered:
  .claude (claude-code.cfg), .codex, .docker, .config/gh (github-cli xdg)
- Keep .cursor (built-in covers Library/Application Support, not .cursor/)

_SSR_MACKUP_SELECTIVE:
- Remove entries for removed curated items

ssr mackup scan:
- Confirm each new item individually (y/n per candidate)
- Confirm each stale removal individually
- Show resolved sub-paths per item before asking

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-12 17:41:15 +02:00
parent 953c213ded
commit f6b323e9f5
2 changed files with 187 additions and 95 deletions
+85 -34
View File
@@ -14,20 +14,24 @@
# Custom Mackup app definition auto-generator
# ---------------------------------------------------------------------------
# Curated dot-paths that matter for developer workflows but are not in
# Mackup's built-in app definitions. Add your own via SSR_MACKUP_EXTRA_PATHS
# (space-separated) in ssr.conf.
# Curated dot-paths for developer workflows NOT covered by Mackup's built-in
# app definitions. Add your own via SSR_MACKUP_EXTRA_PATHS in ssr.conf.
#
# Built-ins that ALREADY cover common tools (do NOT add them here):
# claude-code.cfg → .claude/{plugins,settings,skills,agents,settings.json,CLAUDE.md}
# codex.cfg → .codex/{config.toml,auth.json}
# cursor.cfg → Library/Application Support/Cursor/User/... (not .cursor/)
# docker.cfg → .docker/{config.json,daemon.json}
# github-cli.cfg → .config/gh/config.yml (xdg_configuration_files)
# vscode.cfg → Library/Application Support/Code/User/... (not .vscode/)
# vscode-insiders.cfg → Library/Application Support/Code - Insiders/...
_SSR_MACKUP_CURATED=(
.claude # Claude AI CLI config, skills, agents
.cursor # Cursor IDE config + rules
.codex # OpenAI Codex config
.easymcp # EasyMCP config
.factory # Factory AI config
.docker # Docker Desktop — selective files only (see _SSR_MACKUP_SELECTIVE)
.config/claude
.config/cursor
.config/gh # GitHub CLI (if not already in mackup)
.config/op # 1Password CLI config
# .cursor/ rules and workspace-specific settings live here — the built-in
# cursor.cfg only covers Library/Application Support/Cursor (different path).
.cursor
.easymcp # EasyMCP config (no built-in)
.factory # Factory AI config (no built-in)
.config/op # 1Password CLI config (no built-in)
bin # personal ~/bin scripts
)
@@ -40,15 +44,13 @@ _SSR_MACKUP_CURATED=(
# config files we actually need. Extend via SSR_MACKUP_SELECTIVE_PATHS
# (same format, space-separated entries) in ssr.conf.
_SSR_MACKUP_SELECTIVE=(
".claude=settings.json,CLAUDE.md,skills,plugins,agents,commands"
# .cursor/ — only the stable config sub-paths; exclude logs/, extensions/,
# workspaceStorage/ etc. The built-in cursor.cfg covers Library/Application
# Support paths; this covers the dot-dir used for rules and workspace config.
".cursor=settings.json,keybindings.json,snippets,rules"
".codex=auth.json,config.toml"
".vscode=settings.json,keybindings.json,snippets"
".vscode-insiders=settings.json,keybindings.json,snippets"
".factory=settings.json,droids"
# Docker Desktop rejects ~/.docker as a symlink — back up individual files
# only so ~/.docker/ stays a real directory that Docker Desktop can manage.
".docker=config.json,daemon.json"
# Add selective overrides for any SSR_MACKUP_EXTRA_PATHS entries here if
# you need sub-path granularity instead of whole-directory backup.
)
# Dot-dirs/files to never auto-include.
@@ -128,12 +130,16 @@ _ssr_mackup_write_cfg() {
# built-in app definitions AND user-defined ~/.mackup/*.cfg files.
# Our own generated ssr-*.cfg files are excluded to avoid circular checks.
_ssr_mackup_declared_paths() {
local apps_dir=""
# Locate Mackup's built-in apps/ directory via Python.
if command -v python3 >/dev/null 2>&1; then
apps_dir="$(python3 -c \
'import os, mackup; print(os.path.join(os.path.dirname(mackup.__file__), "apps"))' \
# Locate Mackup's built-in applications/ directory.
# Mackup bundles its own Python interpreter — use it so the import works
# regardless of which system python3 is active.
local apps_dir="" mackup_bin
mackup_bin="$(command -v mackup 2>/dev/null)"
if [[ -n "$mackup_bin" ]]; then
local mackup_py
mackup_py="$(head -1 "$mackup_bin" | sed 's/^#!//')"
apps_dir="$("$mackup_py" -c \
'import os, mackup; print(os.path.join(os.path.dirname(mackup.__file__), "applications"))' \
2>/dev/null || true)"
fi
@@ -142,15 +148,17 @@ _ssr_mackup_declared_paths() {
{
[[ -d "$apps_dir" ]] \
&& grep -rh '' "$apps_dir"/*.cfg 2>/dev/null || true
&& grep -rh '' "$apps_dir"/ 2>/dev/null || true
[[ -d "$user_cfg_dir" ]] \
&& grep -rhl '' "$user_cfg_dir"/*.cfg 2>/dev/null \
&& grep -rhl '' "$user_cfg_dir"/ 2>/dev/null \
| grep -v '/ssr-' \
| xargs grep -h '' 2>/dev/null || true
} | awk '
/^\[configuration_files\]/ { in_section=1; next }
/^\[/ { in_section=0 }
in_section && /^[^#[:space:]]/ { print $1 }
/^\[configuration_files\]/ { in_cfg=1; in_xdg=0; next }
/^\[xdg_configuration_files\]/ { in_xdg=1; in_cfg=0; next }
/^\[/ { in_cfg=0; in_xdg=0 }
in_cfg && /^[^#[:space:]]/ { print $1 }
in_xdg && /^[^#[:space:]]/ { print ".config/" $1 }
' | sort -u
}
@@ -194,9 +202,20 @@ ssr::mackup::generate_custom_cfg() {
local declared
declared="$(_ssr_mackup_declared_paths)"
# Return true when <path> is already declared in any existing app definition.
# Return true when <path> is already declared, OR when any declared path
# starts with "<path>/" (i.e. individual files inside <path> are declared,
# meaning a built-in covers the directory via selective file entries).
# Example: built-in byobu declares .byobu/profile → .byobu is covered.
#
# Uses `command grep` (bypasses colour aliases) and a bash glob for the
# prefix check (avoids regex escaping issues with dots in paths).
_is_declared() {
printf '%s\n' "$declared" | grep -qxF "$1"
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
}
# Return true when <path> is already present in the Mackup cloud backup.
_in_backup() { [[ -e "$mackup_dir/$1" ]]; }
@@ -323,6 +342,21 @@ ssr::mackup::backup() {
fi
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
# ---------------------------------------------------------------------------
# Pre-symlink: paths managed by apps that recreate their directory almost
# instantly after deletion, making Mackup's delete→symlink sequence lose the
# race. We pre-create the symlink → cloud BEFORE running `link install`.
# Mackup detects the existing correct symlink and skips the path entirely.
#
# Format: home-relative path (same as in the Mackup app definition).
# ---------------------------------------------------------------------------
local -a _presymlink=(
# iTerm2 recreates DynamicProfiles within milliseconds of deletion.
# Pre-symlinking ensures Mackup skips it; iTerm2 reads/writes through
# the symlink normally (creating new profiles goes straight to iCloud).
.config/iterm2/AppSupport/DynamicProfiles
)
# ---------------------------------------------------------------------------
# Light preclean — remove transient directories so their ephemeral contents
# (debug symlinks, log files) are not included in the cloud copy.
@@ -330,6 +364,23 @@ ssr::mackup::backup() {
# os.path.isfile/isdir guard, so we only need to handle known directories.
# ---------------------------------------------------------------------------
_ssr_mackup_preclean() {
local mr="$1" # mackup root
# 1. Pre-symlink race-condition paths.
local rel home_p cloud_p
for rel in "${_presymlink[@]}"; do
home_p="$HOME/$rel"
cloud_p="$mr/$rel"
[[ -d "$cloud_p" ]] || continue # no cloud copy yet — skip
if [[ -L "$home_p" ]] && [[ "$(readlink "$home_p")" == "$cloud_p" ]]; then
continue # already correct symlink
fi
ssr::log " pre-symlink: $rel → cloud (prevents iTerm2-style race)"
rm -rf "$home_p" 2>/dev/null || true
ln -s "$cloud_p" "$home_p" 2>/dev/null || true
done
# 2. Transient directories whose contents should never go into cloud.
local -a transient_dirs=(
.claude/debug # Claude Code debug session links
.cursor/logs # Cursor IDE logs
@@ -348,7 +399,7 @@ ssr::mackup::backup() {
local attempt rc=0
for attempt in 1 2 3; do
_ssr_mackup_preclean
_ssr_mackup_preclean "$mackup_root"
if mackup -f link install; then
ssr::ok "mackup link install complete — symlinks created in ~/"
return 0