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>
- 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>