diff --git a/bin/ssr-mackup b/bin/ssr-mackup index 6a64f5e..09af2d4 100755 --- a/bin/ssr-mackup +++ b/bin/ssr-mackup @@ -90,11 +90,12 @@ cmd_scan() { local -a labels=() local -a cfg_paths=() - local it app_name desired existing cfg_file + local it app_name desired existing cfg_file paths_block 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")" + paths_block="$(_ssr_mackup_resolve_paths "$it")" + desired="$(printf '[application]\nname = %s\n\n[configuration_files]\n%s\n' "$app_name" "$paths_block")" if [[ -f "$cfg_file" ]]; then existing="$(cat "$cfg_file")" if [[ "$existing" == "$desired" ]]; then @@ -123,8 +124,17 @@ cmd_scan() { 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]}" + # Show resolved paths (selective or whole dir). + local resolved; resolved="$(_ssr_mackup_resolve_paths "${to_write[$i]}")" + local line_count; line_count="$(printf '%s\n' "$resolved" | wc -l | tr -d ' ')" + local paths_hint + if [[ "$line_count" -eq 1 && "$resolved" == "${to_write[$i]}" ]]; then + paths_hint="${to_write[$i]}/" # whole directory + else + paths_hint="$line_count sub-path(s) of ${to_write[$i]}/" + fi + printf " ${colour}%s\033[0m %-38s %s\n" \ + "$label" "~/.mackup/$path" "$paths_hint" done fi diff --git a/config/ssr.conf.example b/config/ssr.conf.example index 59249bb..f6af8f9 100644 --- a/config/ssr.conf.example +++ b/config/ssr.conf.example @@ -26,6 +26,11 @@ SSR_MACKUP_AUTODISCOVER=true # Built-in defaults: .claude/debug .cursor/logs .vscode/logs .npm/_logs # SSR_MACKUP_PRECLEAN_PATHS=".my-app/tmp .other-app/run" +# Selective sub-path overrides — back up only listed sub-paths for a dir +# instead of the whole directory (avoids sockets/debug symlinks in volatile dirs). +# Format: ".dir=subpath1,subpath2" Built-in defaults cover .claude, .cursor, etc. +# SSR_MACKUP_SELECTIVE_PATHS=".my-app=config.json,themes" + # Extra paths (relative to $HOME) to always include, space-separated. # Example: ".easymcp .my-tool .config/my-cli" # SSR_MACKUP_EXTRA_PATHS="" diff --git a/lib/mackup.sh b/lib/mackup.sh index 01584c7..5e77173 100644 --- a/lib/mackup.sh +++ b/lib/mackup.sh @@ -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/.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