Commit Graph

18 Commits

Author SHA1 Message Date
Oliver Surke 371dea1851 feat: auto-detect and install missing shell frameworks after restore
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>
2026-05-12 10:19:32 +02:00
Oliver Surke 649449c628 feat: auto-generate ~/.mackup/ssr-custom.cfg for uncovered dot-dirs
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>
2026-05-12 10:05:16 +02:00
Oliver Surke 87aaf25235 fix: strip deprecated taps from Brewfile; restore Control Centre + menu bar
lib/brew.sh:
  Define _SSR_DEPRECATED_TAPS list (homebrew/core, /cask, /services,
  /test-bot, /bundle, /cask-fonts, /cask-drivers, /cask-versions).
  _ssr_brew_strip_deprecated_taps removes matching 'tap "…"' lines after
  dump so they never cause "Tapping X has failed!" errors on restore.

lib/macos-prefs.sh:
  Add com.apple.controlcenter, com.apple.systemuiserver and
  com.apple.notificationcenterui to SSR_MACOS_PREF_DOMAINS_DEFAULT so
  Control Centre module toggles, menu-bar extras order and Notification
  Centre widgets are included in the pref snapshot on every sync.
  Also add ControlCenter to the post-import service restart list.

lib/macos.sh:
  Add ControlCenter to ssr::macos::restart_ui so the menu bar and
  Control Centre panel pick up restored prefs immediately after mackup.
  Kill cfprefsd first in all restart sequences to flush the pref cache.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 09:55:29 +02:00
Oliver Surke c330f96f9c fix: restart Dock/Finder/SystemUIServer after mackup restore
Mackup restores com.apple.dock.plist and other UI preference files, but
the running Dock/Finder processes don't reload them automatically. Add
ssr::macos::restart_ui (killall cfprefsd Dock Finder SystemUIServer + 2s
settle) called immediately after the mackup restore step so the Dock
layout and Finder settings take effect without requiring a logout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 09:51:34 +02:00
Oliver Surke 488a9a2140 feat: pre-auth sudo once before brew bundle install with keepalive
Homebrew cask installers call sudo internally for each package that needs
admin privileges. Without pre-authentication, macOS prompts for a password
on every such package across a long brew bundle run.

_ssr_sudo_keepalive_start:
  Calls 'sudo -v' once (prompts user if not already cached), then spawns a
  background loop that calls 'sudo -n true' every 60s to prevent the
  default 5-minute credential cache from expiring mid-run.

_ssr_sudo_keepalive_stop:
  Kills the keepalive loop after brew bundle completes.

If sudo is unavailable (non-admin user), a warning is shown and the run
continues — individual cask installers that need sudo will prompt as before.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 09:49:34 +02:00
Oliver Surke 698cbe145a feat: async iCloud prefetch + keep-local for Mackup folder
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>
2026-05-12 09:46:12 +02:00
Oliver Surke 634487b9d8 feat: restore state/resume/report + single Spotlight index after mas installs
lib/brew.sh:
  Set MAS_NO_AUTO_INDEX=1 before brew bundle install to suppress the
  per-app mdimport call that mas otherwise runs after each App Store
  installation. Run one mdimport /Applications batch pass afterwards.

lib/restore-state.sh (new):
  Lightweight key=value state file (~/.local/state/ssr/restore-state).
  ssr::rs::init / ssr::rs::run / ssr::rs::set/get / ssr::rs::error /
  ssr::rs::report — tracks step outcomes and accumulated errors.

bin/ssr-restore:
  --resume flag: skip steps already marked ok in state file.
  Each step (brew_install, brew_restore, mackup_restore, macos_defaults,
  prefs_import) wrapped in ssr::rs::run; errors accumulate without abort.
  Formatted report printed at end of every run.

bin/ssr-status:
  ssr status restore — full restore step report + error log
  ssr status backup  — Brewfile lines, unmanaged apps, prefs snapshot
  ssr status         — general view now shows one-line last-restore
                       summary and hint to run 'ssr status restore'

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 09:39:48 +02:00
Oliver Surke cf7f61e95d feat: add progress watchdog and iCloud hints to mackup restore
- 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>
2026-05-11 22:07:42 +02:00
Oliver Surke d06a6e47d7 fix: soft-fail brew bundle install; update .gitignore
lib/brew.sh:
  brew bundle exits non-zero when any single package fails (network error,
  cask version mismatch, missing tap, etc.). Previously this killed the
  entire ssr restore via set -euo pipefail before mackup / defaults / prefs
  had a chance to run. Capture the exit code, emit a clear warning, and
  continue. The user can rerun the manual command shown in the warning.

.gitignore:
  Replace minimal gitignore with the comprehensive macOS + Cursor + VSCode
  template generated by toptal gitignore.io. Preserve the original ssr-
  specific custom rules (.local/, .config/, *.log, *.plist.local) in the
  custom section at the bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 21:22:27 +02:00
Oliver Surke 873cb3d51f fix: replace removed brctl download with open -g; add -f to mackup restore
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>
2026-05-11 21:06:29 +02:00
Oliver Surke aef5877641 fix: remove --no-lock from brew bundle install (flag removed in modern brew-bundle)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 20:57:38 +02:00
Oliver Surke 8489180d51 Fix ssr restore: skip iCloud prompt when available, bail early on missing sudo
lib/brew.sh:
  - Before running the Homebrew installer, check `sudo -n true`. If sudo
    is not available, print a clear actionable error message with the
    manual install command and exit instead of running NONINTERACTIVE=1
    and crashing with "Need sudo access ... Administrator!" mid-way.

bin/ssr-restore:
  - Drop the unconditional "Configure iCloud / cloud drive?" prompt.
    Instead check whether the cloud directory is already mounted; if it
    is, proceed silently (the common case on an already-configured Mac).
    The prompt only appears when the directory is missing, i.e. iCloud
    is not yet signed in or the drive is not mounted — with an
    appropriate message explaining what to do.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 20:51:18 +02:00
Oliver Surke 5ed6279b51 Snapshot and restore macOS preference domains
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>
2026-05-11 20:19:49 +02:00
Oliver Surke d681792148 Surface unmanaged .app bundles during sync as Markdown report
`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>
2026-05-11 18:04:30 +02:00
Oliver Surke a4465f37c6 Improve schedule and mackup reliability
- 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>
2026-05-11 18:02:31 +02:00
Oliver Surke 69541209dc Fix ssr::confirm using bash 4+ lowercase operator on bash 3.2
macOS ships /bin/bash 3.2.57 which has no `${var,,}` parameter expansion.
Replace with a portable case statement matching y/Y. Behaviorally
identical (read -n 1 already constrains input to a single char, and the
original code only accepted 'y' after lowercasing, so 'y' and 'Y' are
the only accepting inputs).

Repo-wide audit for other bash 4+ features (case modification, mapfile,
nameref locals, ${var@op} transforms, coproc) returned clean.

Repro before fix:
  $ ./install.sh
  Enable daily scheduled sync (launchd)? [y/N]: y
  lib/common.sh: line 52: ${reply,,}: bad substitution

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 15:42:20 +02:00
Oliver Surke 8bad3116c0 Fix path validator always failing on bash 3.2
In bash, strings cannot contain NUL bytes, so `$'\0'` expands to the empty
string. The pattern `*$'\0'*` collapsed to `**`, which matches every
non-empty value, so `validate_path` aborted on every real path with
"Path contains control chars".

- Drop the NUL check entirely (impossible to violate by construction).
- Use a single case statement for the newline and traversal checks; more
  predictable than chained [[ ... && ... ]] glob comparisons on bash 3.2.

Verified: paths with spaces, dots in basenames (~/.config, ~/.local), and
the iCloud bundle directory all pass; empty paths, "..", and embedded
newlines still reject.

Repro before fix:
  $ ./install.sh
  [ERR] Path contains control chars

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 15:40:43 +02:00
Oliver Surke 539a8fc28f Initial ssr repository
Convert the legacy system_sync.sh and system_restore.sh into a modular
shareable repository:

- bin/ssr CLI dispatcher with install/sync/restore/schedule/config/status
- lib/ split: common, cloud, brew, mackup, macos
- Cloud provider abstraction (icloud, dropbox, googledrive, onedrive, custom)
- launchd template for scheduled daily sync (renderable via sed)
- ssr.conf.example with whitelisted KEY=VALUE config loader
- macos-defaults.sh extracted from inline block, overridable
- install.sh bootstrap symlinks CLI into ~/.local/bin
- README with usage, config table, migration map, security notes
- Original scripts archived under legacy/ for reference

Fixes vs originals: set -euo pipefail, quoted vars, find -print0,
spctl --global-disable (was --master-disable), MACOS=".macos" bug,
zero-width char before ln, Apple Silicon brew shellenv, config injection
hardening.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 15:05:17 +02:00