f6b323e9f5
_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>
481 lines
20 KiB
Bash
481 lines
20 KiB
Bash
#!/usr/bin/env bash
|
|
# lib/mackup.sh — Mackup install + backup/restore.
|
|
# Note: upstream mackup is in low-maintenance mode; we keep it because it
|
|
# remains the simplest way to sync dotfiles for a wide range of apps. The
|
|
# user can swap engines via ~/.mackup.cfg without changing this script.
|
|
#
|
|
# Mackup 0.10.2 command mapping (importantly different from older versions):
|
|
# mackup backup → copy only; NO symlinks created in ~/
|
|
# mackup restore → copy only; NO symlinks created in ~/
|
|
# mackup link install → copy to cloud + create symlinks in ~/ ← SSR uses this
|
|
# mackup link → create symlinks in ~/ (cloud files must exist) ← SSR restore
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Custom Mackup app definition auto-generator
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# 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=(
|
|
# .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
|
|
)
|
|
|
|
# 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=(
|
|
# .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"
|
|
".factory=settings.json,droids"
|
|
# 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.
|
|
# Covers: caches, generated state, credentials, large build artefacts.
|
|
_SSR_MACKUP_SKIP=(
|
|
.Trash .localized .DS_Store .CFUserTextEncoding
|
|
.cache .npm .yarn .pnpm .bun
|
|
.gradle .m2 .ivy2 .sbt
|
|
# .docker is handled via _SSR_MACKUP_SELECTIVE (individual files only).
|
|
# Docker Desktop rejects ~/.docker being a symlink, so we never back up
|
|
# the whole directory — only config.json / daemon.json as file symlinks.
|
|
.vagrant .minikube .kube .helm
|
|
.local # runtime state, not config
|
|
.bash_history .zsh_history .zsh_sessions .sh_history
|
|
.lesshst .wget-hsts .python_history .node_repl_history
|
|
.ruby_history .irb_history .psql_history .mysql_history
|
|
.dbshell .viminfo .Xauthority
|
|
.Spotlight-V100 .fseventsd .DocumentRevisions-V100
|
|
# Security-sensitive — back up separately with encryption
|
|
.ssh .gnupg .aws .azure .gcloud .config/gcloud
|
|
)
|
|
|
|
# Derive a safe Mackup app-name and filename from a dot-path.
|
|
# Examples: .claude → ssr-claude .config/gh → ssr-config-gh
|
|
_ssr_mackup_cfg_name() {
|
|
local path="$1"
|
|
local name="${path#.}" # strip leading dot
|
|
name="${name//\//-}" # slashes → dashes
|
|
name="${name//_/-}" # underscores → dashes
|
|
name="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')"
|
|
printf 'ssr-%s' "$name"
|
|
}
|
|
|
|
# Resolve the initial [configuration_files] entries for a NEW item.
|
|
# Uses _SSR_MACKUP_SELECTIVE defaults; once a cfg file exists the user owns
|
|
# it and this function is never called for that item again.
|
|
#
|
|
# Outputs one path per line.
|
|
_ssr_mackup_resolve_paths() {
|
|
local item="$1"
|
|
local entry
|
|
for entry in "${_SSR_MACKUP_SELECTIVE[@]}"; do
|
|
local key="${entry%%=*}"
|
|
local val="${entry#*=}"
|
|
if [[ "$key" == "$item" ]]; then
|
|
local sub
|
|
local IFS=','
|
|
for sub in $val; do
|
|
IFS=' '
|
|
printf '%s\n' "$item/$sub"
|
|
done
|
|
return 0
|
|
fi
|
|
done
|
|
# No selective default — back up the whole directory.
|
|
printf '%s\n' "$item"
|
|
}
|
|
|
|
# Write a single ~/.mackup/<name>.cfg for a NEW item.
|
|
# Never called when the file already exists — existing files are user-owned.
|
|
_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"
|
|
|
|
# Never overwrite existing files — user may have customised them.
|
|
[[ -f "$cfg_file" ]] && return 0
|
|
|
|
local paths_block
|
|
paths_block="$(_ssr_mackup_resolve_paths "$item")"
|
|
|
|
printf '[application]\nname = %s\n\n[configuration_files]\n%s\n' \
|
|
"$app_name" "$paths_block" > "$cfg_file"
|
|
}
|
|
|
|
# Build a newline-separated list of all paths already declared in Mackup's
|
|
# 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() {
|
|
# 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
|
|
|
|
# Also include hand-written user definitions (not our generated ssr-*.cfg).
|
|
local user_cfg_dir="$HOME/.mackup"
|
|
|
|
{
|
|
[[ -d "$apps_dir" ]] \
|
|
&& grep -rh '' "$apps_dir"/ 2>/dev/null || true
|
|
[[ -d "$user_cfg_dir" ]] \
|
|
&& grep -rhl '' "$user_cfg_dir"/ 2>/dev/null \
|
|
| grep -v '/ssr-' \
|
|
| xargs grep -h '' 2>/dev/null || true
|
|
} | awk '
|
|
/^\[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
|
|
}
|
|
|
|
# Generate one ~/.mackup/ssr-<name>.cfg per uncovered dot-dir/file.
|
|
# 1. Curated entries that exist in $HOME and are not yet covered.
|
|
# 2. Auto-discovered dot-dirs in $HOME not in the skip list and not covered.
|
|
#
|
|
# "Covered" means: already declared in a built-in or hand-written Mackup app
|
|
# definition OR already materialised in the Mackup backup directory.
|
|
#
|
|
# Removes the old combined ssr-custom.cfg if found (migration).
|
|
# Each file is picked up automatically by `mackup backup/restore`.
|
|
ssr::mackup::generate_custom_cfg() {
|
|
local mackup_dir="$1"
|
|
local cfg_dir="$HOME/.mackup"
|
|
mkdir -p "$cfg_dir"
|
|
|
|
# Remove legacy combined file if present.
|
|
[[ -f "$cfg_dir/ssr-custom.cfg" ]] && rm -f "$cfg_dir/ssr-custom.cfg"
|
|
|
|
# Build declared-paths lookup early so we can also purge stale ssr-*.cfg
|
|
# files that now duplicate a built-in definition (e.g. ssr-claude.cfg after
|
|
# Mackup added a built-in "claude-code" app).
|
|
local declared
|
|
declared="$(_ssr_mackup_declared_paths)"
|
|
|
|
local stale_cfg stale_path
|
|
for stale_cfg in "$cfg_dir"/ssr-*.cfg; do
|
|
[[ -f "$stale_cfg" ]] || continue
|
|
# Extract the single path from [configuration_files].
|
|
stale_path="$(awk '/^\[configuration_files\]/{f=1;next}/^\[/{f=0}f&&/^[^#[:space:]]/{print $1;exit}' "$stale_cfg")"
|
|
if printf '%s\n' "$declared" | grep -qxF "$stale_path"; then
|
|
ssr::log "Removing stale duplicate: ${stale_cfg##*/} ($stale_path now covered by built-in)"
|
|
rm -f "$stale_cfg"
|
|
fi
|
|
done
|
|
|
|
local skip_set=" ${_SSR_MACKUP_SKIP[*]} "
|
|
|
|
# Build declared-paths lookup (newline-separated, sorted).
|
|
local declared
|
|
declared="$(_ssr_mackup_declared_paths)"
|
|
|
|
# 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() {
|
|
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" ]]; }
|
|
|
|
local -a items=()
|
|
|
|
# 1. Curated + user-defined extras.
|
|
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
|
|
|
|
# 2. Auto-discover uncovered dot-dirs.
|
|
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)
|
|
|
|
if [[ ${#items[@]} -eq 0 ]]; then
|
|
ssr::log "No uncovered dot-dirs — ~/.mackup/ssr-*.cfg unchanged."
|
|
return 0
|
|
fi
|
|
|
|
local written=0 it
|
|
for it in "${items[@]}"; do
|
|
_ssr_mackup_write_cfg "$cfg_dir" "$it"
|
|
written=$((written + 1))
|
|
done
|
|
|
|
ssr::ok "~/.mackup/ssr-*.cfg: ${written} definition(s) written"
|
|
ssr::log " → ${items[*]}"
|
|
}
|
|
|
|
ssr::mackup::ensure_installed() {
|
|
if command -v mackup >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
ssr::log "Installing mackup via brew"
|
|
brew install mackup
|
|
}
|
|
|
|
# Writes ~/.mackup.cfg pointing the engine at the cloud directory.
|
|
# Idempotent: rewrites only if content differs.
|
|
ssr::mackup::configure() {
|
|
local mackup_dir="$1"
|
|
local cfg="$HOME/.mackup.cfg"
|
|
local desired
|
|
desired="$(printf '[storage]\nengine = file_system\npath = %s\ndirectory = mackup\n' "$mackup_dir")"
|
|
|
|
if [[ -f "$cfg" ]] && [[ "$(cat "$cfg")" == "$desired" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if [[ -L "$cfg" || -f "$cfg" ]]; then
|
|
cp -f "$cfg" "$cfg.bak.$(date +%s)" 2>/dev/null || true
|
|
rm -f "$cfg"
|
|
fi
|
|
printf '%s\n' "$desired" > "$cfg"
|
|
ssr::ok "Wrote $cfg"
|
|
}
|
|
|
|
# Force iCloud Drive to materialize placeholder (.icloud) files under the
|
|
# Mackup storage directory before mackup walks them. The most common ssr sync
|
|
# crash is `OSError [Errno 66] Directory not empty` raised by shutil.rmtree
|
|
# inside mackup when iCloud lazily re-creates dataless placeholders between
|
|
# listdir() and rmdir(). Pre-downloading sidesteps the race.
|
|
ssr::mackup::_materialize_icloud() {
|
|
local mackup_root="$1"
|
|
[[ -d "$mackup_root" ]] || return 0
|
|
case "$mackup_root" in
|
|
*Mobile\ Documents*) ;; # only worth doing for iCloud
|
|
*) return 0 ;;
|
|
esac
|
|
# `brctl download` was removed in macOS Sonoma (remaining brctl subcommands:
|
|
# diagnose/log/dump/status/accounts/quota/monitor). The current way to
|
|
# trigger per-file iCloud download from the CLI is `open -g <real-path>`,
|
|
# which asks the iCloud daemon to fetch the file in the background.
|
|
find "$mackup_root" -name '*.icloud' -type f 2>/dev/null | while IFS= read -r stub; do
|
|
local dir base real
|
|
dir="$(dirname "$stub")"
|
|
base="$(basename "$stub")"
|
|
real="${base#.}" # strip leading dot
|
|
real="${real%.icloud}" # strip .icloud suffix
|
|
open -g "$dir/$real" 2>/dev/null || true
|
|
done
|
|
# Brief pause so the iCloud daemon can start fetching before mackup reads.
|
|
sleep 1
|
|
# Belt-and-braces: prune any stubs that couldn't be resolved.
|
|
find "$mackup_root" -name '*.icloud' -type f -delete 2>/dev/null || true
|
|
}
|
|
|
|
# Run `mackup -f link install` — this is the correct "backup with symlinks"
|
|
# command in Mackup 0.10.2. Unlike `mackup backup` (which only copies),
|
|
# `link install` moves each managed file to the cloud folder and replaces it
|
|
# with a symlink. On subsequent runs it detects already-linked files and
|
|
# skips them efficiently.
|
|
#
|
|
# Sockets and dangling symlinks are naturally skipped by `link install` because
|
|
# Python's os.path.isfile/isdir returns False for those types. We still run a
|
|
# light preclean for known transient directories so their contents don't appear
|
|
# as stale files in the cloud copy.
|
|
#
|
|
# We do NOT abort `ssr sync` on persistent failure — brew dump, cleanup, and
|
|
# the unmanaged-apps scan still run. Failure is surfaced via ssr::warn.
|
|
ssr::mackup::backup() {
|
|
ssr::log "mackup link install (backup + symlink)"
|
|
|
|
# Derive mackup root from ~/.mackup.cfg (path + directory).
|
|
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
|
|
if [[ -f "$cfg" ]]; then
|
|
path="$(awk -F= '/^path/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")"
|
|
dir="$( awk -F= '/^directory/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")"
|
|
[[ -n "$path" && -n "$dir" ]] && mackup_root="$path/$dir"
|
|
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.
|
|
# `link install` already skips sockets and dangling symlinks via its own
|
|
# 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
|
|
.vscode/logs # VS Code logs
|
|
.vscode-insiders/logs
|
|
.npm/_logs # npm debug logs
|
|
)
|
|
local extra d
|
|
for extra in ${SSR_MACKUP_PRECLEAN_PATHS:-}; do
|
|
transient_dirs+=("$extra")
|
|
done
|
|
for d in "${transient_dirs[@]}"; do
|
|
[[ -e "$HOME/$d" ]] && rm -rf "${HOME:?}/$d" 2>/dev/null || true
|
|
done
|
|
}
|
|
|
|
local attempt rc=0
|
|
for attempt in 1 2 3; do
|
|
_ssr_mackup_preclean "$mackup_root"
|
|
if mackup -f link install; then
|
|
ssr::ok "mackup link install complete — symlinks created in ~/"
|
|
return 0
|
|
fi
|
|
rc=$?
|
|
ssr::warn "mackup link install failed (attempt $attempt/3, rc=$rc)"
|
|
if [[ $attempt -lt 3 ]]; then
|
|
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
|
|
sleep 2
|
|
fi
|
|
done
|
|
|
|
ssr::warn "mackup link install did not complete after 3 attempts (rc=$rc)."
|
|
ssr::warn "Sync will continue without it; run 'mackup -f link install' manually,"
|
|
ssr::warn "or check ~/.mackup.cfg if this persists."
|
|
ssr::syslog "mackup link install failed after retries (rc=$rc)"
|
|
return 0 # soft-fail so the rest of ssr sync still runs
|
|
}
|
|
|
|
ssr::mackup::restore() {
|
|
ssr::log "mackup link (restore via symlinks)"
|
|
|
|
# Pre-materialize iCloud placeholders so mackup's isfile/isdir checks
|
|
# see real files rather than .icloud stubs (which read as non-existent).
|
|
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
|
|
if [[ -f "$cfg" ]]; then
|
|
path="$(awk -F= '/^path/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")"
|
|
dir="$( awk -F= '/^directory/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")"
|
|
[[ -n "$path" && -n "$dir" ]] && mackup_root="$path/$dir"
|
|
fi
|
|
if [[ -n "$mackup_root" ]]; then
|
|
ssr::log "Materializing iCloud files in $mackup_root (may take a moment)…"
|
|
ssr::mackup::_materialize_icloud "$mackup_root"
|
|
local stub_count
|
|
stub_count="$(find "$mackup_root" -name '*.icloud' -type f 2>/dev/null | wc -l | tr -d ' ')"
|
|
if [[ "$stub_count" -gt 0 ]]; then
|
|
ssr::warn "$stub_count placeholder file(s) still pending — waiting 5s for iCloud sync…"
|
|
sleep 5
|
|
fi
|
|
fi
|
|
|
|
local stub_total
|
|
stub_total="$(find "${mackup_root:-/dev/null}" -name '*.icloud' -type f 2>/dev/null | wc -l | tr -d ' ')"
|
|
if [[ "$stub_total" -gt 0 ]]; then
|
|
ssr::warn "$stub_total file(s) still need downloading — symlinks may be skipped for those."
|
|
fi
|
|
ssr::log "Running mackup link (long pauses normal while iCloud fetches files)…"
|
|
|
|
# Background watchdog: prints elapsed time every 15s.
|
|
local start_ts rc=0 done_flag watchdog_pid
|
|
start_ts="$(date +%s)"
|
|
done_flag="$(mktemp)"
|
|
(
|
|
while [[ -f "$done_flag" ]]; do
|
|
sleep 15
|
|
[[ -f "$done_flag" ]] || break
|
|
printf ' … mackup link still running (%ds elapsed — waiting for iCloud)\n' \
|
|
"$(( $(date +%s) - start_ts ))" >&2
|
|
done
|
|
) &
|
|
watchdog_pid=$!
|
|
|
|
# `mackup link` creates symlinks ~/file → cloud/file for all managed apps.
|
|
# Unlike `mackup restore` (which copies), this keeps files in iCloud and
|
|
# ensures ongoing sync. -f auto-confirms replacement of existing home files.
|
|
mackup -f link || rc=$?
|
|
|
|
rm -f "$done_flag"
|
|
kill "$watchdog_pid" 2>/dev/null || true
|
|
wait "$watchdog_pid" 2>/dev/null || true
|
|
|
|
local elapsed=$(( $(date +%s) - start_ts ))
|
|
if [[ $rc -ne 0 ]]; then
|
|
ssr::warn "mackup link finished with errors (rc=$rc, ${elapsed}s elapsed)"
|
|
else
|
|
ssr::ok "mackup link completed in ${elapsed}s — symlinks created in ~/"
|
|
fi
|
|
}
|