Commit Graph

35 Commits

Author SHA1 Message Date
Oliver Surke 421b802467 fix(mackup): update backup path and enhance configuration handling
- 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.
2026-05-13 20:17:50 +02:00
Oliver Surke c31338dba9 refactor(mackup): transition to copy-based backup and restore
- 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>
2026-05-13 09:17:25 +02:00
Oliver Surke 51fb589ef2 fix(mackup): safer defaults + recovery after broken symlink sync
- 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>
2026-05-12 20:09:23 +02:00
Oliver Surke ccc1db1e93 chore(mackup): enhance skip list to prevent redundant configuration suggestions
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.
2026-05-12 20:06:43 +02:00
Oliver Surke c88e9db1c4 chore(mackup): expand skip list with installed runtimes and caches
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 17:44:16 +02:00
Oliver Surke f6b323e9f5 fix(mackup): correct built-in coverage detection and per-item scan confirm
_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>
2026-05-12 17:41:15 +02:00
Oliver Surke 953c213ded fix: use mackup link install/link instead of backup/restore
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>
2026-05-12 15:37:43 +02:00
Oliver Surke 61091aa6bf refactor: ssr-*.cfg files are user-owned after creation
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>
2026-05-12 15:06:43 +02:00
Oliver Surke be9b37010e fix: back up Docker Desktop config files only, not the whole ~/.docker dir
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>
2026-05-12 15:03:24 +02:00
Oliver Surke 93c23c60a9 feat: selective sub-path backup for volatile dot-dirs
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>
2026-05-12 14:58:16 +02:00
Oliver Surke 3e54f0ca20 fix: pre-clean transient dirs + repeat cleanup before every mackup attempt
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>
2026-05-12 12:17:44 +02:00
Oliver Surke 1d18dd6a48 fix: prevent brew bundle hanging on large cask installs
HOMEBREW_NO_QUARANTINE=1 — skips the post-install xattr quarantine walk
  that Gatekeeper runs on every binary in the app bundle. For large apps
  like WhatsApp (237 MB, many nested binaries) this walk blocks for several
  minutes. Apps will show the standard one-time Gatekeeper dialog instead.

HOMEBREW_NO_INSTALL_CLEANUP=1 — skips per-package cache cleanup after each
  install. A single brew cleanup already runs in ssr-sync afterwards.

mdimport — now runs in background (&) so Spotlight indexing never blocks
  the restore flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:52:24 +02:00
Oliver Surke d849400f63 fix: skip dot-paths already declared in Mackup built-in app definitions
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>
2026-05-12 11:49:04 +02:00
Oliver Surke b50ab61d5c fix: remove dangling symlinks before mackup backup
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>
2026-05-12 11:00:25 +02:00
Oliver Surke 894062adcd refactor: one ~/.mackup/ssr-<name>.cfg per dot-dir instead of combined file
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>
2026-05-12 10:50:42 +02:00
Oliver Surke c6ac73bc83 fix: remove Unix socket files before mackup backup
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>
2026-05-12 10:43:13 +02:00
Oliver Surke 72516b864c feat: download and install extra DMG/PKG/ZIP installers during restore
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>
2026-05-12 10:33:56 +02:00
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