- Updated `ssr sync` and `ssr restore` commands to use `mackup backup` and `mackup restore`, replacing the previous symlink-based approach.
- Added guidance for users with legacy symlink setups to run `ssr mackup unlink` for recovery.
- Revised documentation in README, completions, and configuration examples to reflect the new copy-based methodology for improved reliability on newer macOS versions.
- Enhanced comments in the codebase to clarify the changes and their implications for users.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Default-off SSR_MACKUP_PRESYMLINK for DynamicProfiles pre-symlink (was
risking empty/wrong iCloud trees and breaking iTerm2).
- Remove incorrect skip entries: built-in cfgs use different paths than
~/.zsh, ~/.vscode*, ~/.iterm2 — skipping hid real overlap from scan only
but the presymlink change was the main footgun.
- Add `ssr mackup unlink` → `mackup -f link uninstall` to copy configs back
from Mackup storage and drop symlinks (official undo).
- Docs: ssr.conf.example, README sync/restore wording, completions.
Co-authored-by: Cursor <cursoragent@cursor.com>
Added additional dot-directories and sync-service data directories to the skip list, ensuring that paths already covered by Mackup built-ins do not generate duplicate configuration suggestions. This improves the efficiency of the backup process and reduces user confusion.
_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>
In Mackup 0.10.2, `mackup backup` only copies files to cloud — it never
creates symlinks. Symlinks require `mackup link install` (sync) or
`mackup link` (restore). This is why no symlinks appeared in ~/ after
running ssr sync.
Changes:
- ssr::mackup::backup() → now calls `mackup -f link install`
Moves managed files to cloud and creates symlinks in ~/
Naturally skips sockets and dangling symlinks via isfile/isdir guard,
so the pre-cleanup is now a light transient-dir purge only.
- ssr::mackup::restore() → now calls `mackup -f link`
Creates symlinks ~/file → cloud/file (files must already be in cloud).
Keeps files in iCloud for ongoing sync, unlike the copy-based restore.
- Added Mackup 0.10.2 command-mapping comment to file header.
Co-authored-by: Cursor <cursoragent@cursor.com>
ssr mackup scan now only suggests NEW files — it never overwrites an
existing ssr-*.cfg. Once created, the file belongs to the user who
edits it directly in ~/.mackup/ to customise which paths are backed up.
- _ssr_mackup_write_cfg: returns early if file already exists
- scan: drops UPD logic; only shows NEW suggestions
- _ssr_mackup_resolve_paths: removed SSR_MACKUP_SELECTIVE_PATHS support
(ssr.conf config variable removed — edit the cfg files instead)
- Scan output: hints to edit files directly after creation
Co-authored-by: Cursor <cursoragent@cursor.com>
Docker Desktop checks that ~/.docker is a real directory and rejects it
if it is a symlink (Mackup's default when backing up a whole dir).
Solution: remove .docker from the skip list, add it to curated + selective.
Mackup will symlink only ~/.docker/config.json and ~/.docker/daemon.json —
the directory itself stays a real dir that Docker Desktop can manage.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Root cause: Claude Code recreates ~/.claude/debug/latest (a dangling
symlink to the current debug session) almost immediately after it is
deleted. The old cleanup ran once before the retry loop, so any retry
after a 2s sleep would hit a freshly recreated symlink.
Fix: move _ssr_mackup_preclean() inside the retry loop so it runs
immediately before each mackup attempt, leaving no window for apps to
recreate problematic files.
Also remove known transient subdirectories by path before the generic
socket/dangling-link sweep:
.claude/debug .cursor/logs .vscode/logs .npm/_logs
Configurable via SSR_MACKUP_PRECLEAN_PATHS (space-separated, $HOME-relative).
Co-authored-by: Cursor <cursoragent@cursor.com>
Previously ssr-custom cfg generation only checked whether the path was
already in the cloud backup directory. It did not consult Mackup's
~400 built-in app definitions, causing duplicates like ssr-claude.cfg
when Mackup already has a 'claude-code' entry covering .claude.
Changes:
- _ssr_mackup_declared_paths(): parses all built-in *.cfg files from
Mackup's Python package apps/ dir + hand-written ~/.mackup/*.cfg
(excluding our own ssr-*.cfg) into a sorted path set
- generate_custom_cfg now skips any path already in that set
- Stale ssr-*.cfg files that now duplicate a built-in are removed
automatically on the next sync run
Co-authored-by: Cursor <cursoragent@cursor.com>
shutil.copy fails on dangling symlinks with [Errno 2] No such file or
directory (e.g. ~/.claude/debug/latest → missing target).
Pre-backup cleanup now handles two cases:
- Unix sockets (-type s) → Errno 102
- Dangling links (-type l !-e) → Errno 2
Both are ephemeral runtime artefacts that cannot be meaningfully backed up.
Co-authored-by: Cursor <cursoragent@cursor.com>
Each discovered dot-dir now gets its own Mackup app definition:
.claude → ~/.mackup/ssr-claude.cfg
.cursor → ~/.mackup/ssr-cursor.cfg
.config/gh → ~/.mackup/ssr-config-gh.cfg
…
This matches Mackup's own convention, keeps definitions independent,
and makes it easy to add/remove individual apps. The old combined
ssr-custom.cfg is removed automatically on the next sync run.
Files are written idempotently (skipped when content is unchanged).
Co-authored-by: Cursor <cursoragent@cursor.com>
Mackup fails with shutil.Error [Errno 102] when it tries to copy Unix
socket files (e.g. iTerm2's iterm2-daemon-1.socket, sockets/secrets).
Sockets are ephemeral runtime artefacts — they cannot be meaningfully
backed up and are always recreated by the owning app on startup.
Purge all -type s files from $HOME before calling mackup backup.
Co-authored-by: Cursor <cursoragent@cursor.com>
Before each `ssr sync`, scan $HOME for dot-dirs/files not yet present
in the Mackup backup directory and write them into
~/.mackup/ssr-custom.cfg. This covers tools like .claude, .cursor,
.easymcp, .codex, .factory, and any future dot-dir without requiring
the user to write manual Mackup app definitions.
Curated list handles common developer tools; auto-discovery adds the
rest while skipping caches, credential dirs (.ssh, .gnupg, .aws,
.azure) and other volatile state.
Configurable via:
SSR_MACKUP_AUTODISCOVER=true (opt-out with false)
SSR_MACKUP_EXTRA_PATHS="..." (space-separated $HOME-relative paths)
Co-authored-by: Cursor <cursoragent@cursor.com>
- Pre-materialize iCloud stubs before restore (same as backup)
- Report stub count so user knows why it might be slow
- Background watchdog prints '… still running (Ns elapsed)' every 15s
so long iCloud download pauses don't look like a hang
- Report total elapsed time on completion
Co-authored-by: Cursor <cursoragent@cursor.com>
brctl download was removed in macOS Sonoma — the remaining subcommands are
diagnose/log/dump/status/accounts/quota/monitor. Replace the dead call with
a find loop that uses 'open -g <real-path>' to request per-file iCloud
download from the daemon, followed by a 1-second settle pause and the
existing .icloud stub deletion.
Also add -f to mackup restore for consistency with the backup call.
Co-authored-by: Cursor <cursoragent@cursor.com>
- bin/ssr-schedule: use `launchctl bootstrap`/`bootout` (modern API) with
legacy `load -w` fallback. The previous `load -w`-only flow silently
no-op'd on macOS 10.15+ in some configurations, leaving `ssr status`
reporting `inactive` after a successful `ssr schedule on`.
- bin/ssr-status: query the service via `launchctl print`/`list <label>`
for consistent active/inactive detection.
- lib/mackup.sh: pre-materialize iCloud placeholders via `brctl download`
and retry `mackup -f backup` up to three times to dodge the recurring
`OSError [Errno 66] Directory not empty` race between mackup's rmtree
and iCloud's filesystem provider. Soft-fails so the rest of `ssr sync`
still runs even if mackup never settles; logs actionable hints.
Co-authored-by: Cursor <cursoragent@cursor.com>