- Corrected the backup path in the `_in_backup` function to reflect the new structure under `mackup/`.
- Improved error handling in `ssr::mackup::configure` to skip restore if the configuration file is not updated, providing user feedback.
- Updated `ssr::sync` to conditionally execute backup based on configuration validity, enhancing robustness.
- Revised documentation in configuration examples to clarify the new Mackup directory structure and its implications for users.
- 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>
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>
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>
Adds a simple config file (~/.config/ssr/extra-installers.txt) where
users can list installer URLs for apps not available on Homebrew or MAS
(e.g. EasyMCP Desktop, internal tools).
lib/installers.sh:
- Supports .dmg (mount → cp .app or run .pkg), .pkg, .zip
- Skips entries already present in /Applications
- Non-fatal: logs warnings and continues on failure
ssr sync: copies extra-installers.txt to <backup_dir> alongside Brewfile
ssr restore: new `extra_installers` resumable step runs after shell_deps
Format: optional-name <url> (# comments and blank lines ignored)
Example entry:
EasyMCP https://github.com/Adobe-AIFoundations/easymcp/.../EasyMCP-Desktop-3.0.3-arm64.dmg
Co-authored-by: Cursor <cursoragent@cursor.com>
After mackup restores dotfiles, ~/.zshrc may reference tools (Oh My Zsh,
nvm, pyenv, rustup, …) that haven't been installed yet, causing errors on
first shell login.
lib/shell.sh:
- Defines 13 built-in frameworks with detect + install commands
- Scans ~/.zshrc / ~/.bashrc etc. for references before installing
(avoids installing things the user doesn't actually use)
- SSR_SHELL_DEPS_EXTRA in ssr.conf for custom user additions
bin/ssr-restore:
- Adds `shell_deps` as a resumable step after mackup_restore
- Sources ~/.config/ssr/post-restore.sh if present (user hook)
config/post-restore.sh.example:
- Template with commented examples (OMZ plugins, npm globals, SSH keys)
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>
lib/icloud.sh (new):
ssr::icloud::prefetch <dir> — spawn background open -g requests for
all .icloud stubs; returns immediately so other work can run in parallel.
ssr::icloud::wait <dir> [sec] — poll until stubs are gone or timeout
(default 180s). Logs progress every 5s.
ssr::icloud::keep_local <dir> — after sync, removes
com.apple.icloud.evictable xattr from every real file + updates
access-time (LRU delay), preventing iCloud from re-creating stubs
before the next run. Best-effort (no public pin API on macOS CLI).
bin/ssr-sync:
- prefetch Mackup folder immediately after cloud is available (before
brew update/upgrade) so downloads proceed in the background.
- ssr::icloud::wait called just before mackup backup.
- ssr::icloud::keep_local called after sync completes.
- SSR_ICLOUD_WAIT_TIMEOUT (default 180) and SSR_ICLOUD_KEEP_LOCAL
(default true) config toggles added.
bin/ssr-restore:
- prefetch Mackup folder right after cloud is validated, before the
brew bundle install step, maximising download time available.
config/ssr.conf.example:
Added SSR_ICLOUD_WAIT_TIMEOUT and SSR_ICLOUD_KEEP_LOCAL keys.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a round-trip mechanism for macOS system preferences that are not
covered by Homebrew or Mackup (Dock layout, Finder options, global
keyboard shortcuts, input sources, Spaces config, menu-bar clock,
NSGlobalDomain).
sync: `ssr sync` exports each configured domain with `defaults export`
and writes one plist per domain to <backup_dir>/macos-prefs/. Export is
soft-fail per domain so one bad domain doesn't abort the sync. Gated by
SSR_SNAPSHOT_MACOS_PREFS=true (default on).
restore: `ssr restore` replays the snapshot with `defaults import` after
the declarative config/macos-defaults.sh runs, so the snapshot is the
last writer and wins for accepted domains. Before importing, the restore
detects domains that both the snapshot and macos-defaults.sh touch and
prompts interactively:
Domain `com.apple.dock`:
Snapshot will OVERRIDE values set by macos-defaults.sh.
[A]ccept snapshot / [r]eject (keep declarative)? [A/r]:
Non-TTY (launchd, CI) falls back to SSR_PREFS_CONFLICT_DEFAULT (default
"accept"). SSR_ASSUME_YES=true skips all prompts. After import, Dock,
Finder, SystemUIServer and cfprefsd are restarted once.
New config keys: SSR_SNAPSHOT_MACOS_PREFS, SSR_MACOS_PREF_DOMAINS,
SSR_MACOS_PREF_DOMAINS_EXTRA, SSR_PREFS_CONFLICT_DEFAULT, SSR_ASSUME_YES.
ssr-status surfaces the snapshotted-domain count.
README documents the snapshot flow, default domain table, conflict UX,
and the five new config keys.
Co-authored-by: Cursor <cursoragent@cursor.com>
`ssr sync` now scans /Applications (and ~/Applications, configurable via
SSR_APP_DIRS) for top-level .app bundles that are neither Mac App Store
installs (Contents/_MASReceipt/receipt) nor Homebrew Cask installs, and
writes <backup_dir>/unmanaged-apps.md alongside the Brewfile. This closes
a long-standing gap in the restore story: such apps need manual reinstall
on a fresh machine, and were previously invisible to the sync workflow.
Cask detection runs fully offline against the local Caskroom and unions
four complementary strategies:
1. Caskroom walk for .app symlinks/bundles under
$(brew --caskroom)/<cask>/<version>/.
2. `app "Foo.app"` declarations parsed from .rb formula receipts.
3. `name "Foo"` field from .rb receipts (handles pkg-based casks like
Microsoft Edge, Teams, Zoom, Google Drive, etc., that never drop
a .app under the caskroom).
4. `"name":["Foo"]` field from modern .json receipts (multi-line aware).
For each unmanaged app the report includes version and bundle id from the
Info.plist plus a best-effort installer link, in preference order:
1. Sparkle SUFeedURL → derive the vendor homepage from the URL host.
2. Bundle-id prefix match against a curated vendor map (Adobe,
Microsoft, Docker, Cursor, Anthropic, OpenAI, JetBrains,
GitKraken, Signal, Zoom, Telegram, Slack, Synology, Jabra, ...).
3. DuckDuckGo search fallback (`<App name> macOS download`).
Toggles in ssr.conf.example: SSR_SCAN_UNMANAGED_APPS=true (default),
SSR_APP_DIRS=/Applications:~/Applications.
ssr-status gains a one-line summary referencing the report file.
Co-authored-by: Cursor <cursoragent@cursor.com>