feat: selective sub-path backup for volatile dot-dirs

Mackup with engine=file_system already uses symlinks for all backed-up
apps (/Users/osurke/.docker → <cloud>/Mackup/mackup/.docker). The copy PHASE
fails when the source dir contains sockets or dangling symlinks.

For dirs like .claude that have a volatile debug/ subdir, the clean fix
is to list only the stable sub-paths in the Mackup cfg rather than the
whole directory — debug/ is never touched.

_SSR_MACKUP_SELECTIVE: built-in overrides for .claude, .cursor, .codex,
.vscode, .factory — lists only config files (settings.json, CLAUDE.md,
skills, plugins, agents, …), skipping logs/, debug/, etc.

_ssr_mackup_resolve_paths(): resolves the [configuration_files] entries
for a given item using the selective table, falling back to the whole dir.

ssr mackup scan now shows '6 sub-path(s) of .claude/' in the plan
instead of '.claude/' to make the selective behaviour visible.

Configurable via SSR_MACKUP_SELECTIVE_PATHS in ssr.conf.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-12 14:58:16 +02:00
parent 36d4941c95
commit 93c23c60a9
3 changed files with 78 additions and 7 deletions
+59 -3
View File
@@ -24,6 +24,23 @@ _SSR_MACKUP_CURATED=(
bin # personal ~/bin scripts
)
# Selective sub-path overrides.
# When a directory contains volatile/problematic files (sockets, debug
# symlinks), list only the STABLE sub-paths here instead of the whole dir.
# Format: "dir=subpath1,subpath2,subpath3" (paths relative to the dir)
#
# Example: .claude has a debug/ dir with dangling symlinks → list only the
# 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=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"
)
# Dot-dirs/files to never auto-include.
# Covers: caches, generated state, credentials, large build artefacts.
_SSR_MACKUP_SKIP=(
@@ -52,17 +69,56 @@ _ssr_mackup_cfg_name() {
printf 'ssr-%s' "$name"
}
# Resolve the list of [configuration_files] entries for an item.
# If a selective override exists for the item, expand sub-paths that are
# present on disk. Falls back to the item itself (whole directory).
#
# Outputs one path per line.
_ssr_mackup_resolve_paths() {
local item="$1"
local -a all_selective=("${_SSR_MACKUP_SELECTIVE[@]}")
# Merge user-defined selectives from ssr.conf.
local entry
for entry in ${SSR_MACKUP_SELECTIVE_PATHS:-}; do
all_selective+=("$entry")
done
for entry in "${all_selective[@]}"; do
local key="${entry%%=*}"
local val="${entry#*=}"
if [[ "$key" == "$item" ]]; then
# Split comma-separated sub-paths; only include those that exist.
local sub
local IFS=','
for sub in $val; do
IFS=' '
local full="$item/$sub"
# List the path even if it doesn't exist yet — it may be created later.
printf '%s\n' "$full"
done
return 0
fi
done
# No selective entry — back up the whole directory.
printf '%s\n' "$item"
}
# Write a single ~/.mackup/<name>.cfg for one dot-path.
# Skips writing if the file already exists and its content matches
# (idempotent on repeated syncs).
# Uses selective sub-paths when a matching entry exists in _SSR_MACKUP_SELECTIVE.
# Skips writing if content is unchanged (idempotent).
_ssr_mackup_write_cfg() {
local cfg_dir="$1" item="$2"
local app_name; app_name="$(_ssr_mackup_cfg_name "$item")"
local cfg_file="$cfg_dir/${app_name}.cfg"
# Build [configuration_files] block.
local paths_block
paths_block="$(_ssr_mackup_resolve_paths "$item")"
local desired
desired="$(printf '[application]\nname = %s\n\n[configuration_files]\n%s\n' \
"$app_name" "$item")"
"$app_name" "$paths_block")"
if [[ -f "$cfg_file" ]] && [[ "$(cat "$cfg_file")" == "$desired" ]]; then
return 0 # already up-to-date