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>
This commit is contained in:
2026-05-11 15:05:17 +02:00
commit 539a8fc28f
23 changed files with 1171 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
# ssr — System Sync & Restore for macOS
Reproducible macOS setup in one command. `ssr` snapshots your Homebrew bundle and application configs to a cloud drive on a schedule, and can rebuild a fresh Mac from that snapshot.
- **Backup** (`ssr sync`): dumps `Brewfile` and runs `mackup backup` to the configured cloud directory.
- **Restore** (`ssr restore`): installs Homebrew, replays the `Brewfile`, restores `mackup` configs, applies macOS defaults.
- **Scheduled sync**: native macOS `launchd` agent (daily by default).
- **Cloud storage**: iCloud Drive, Dropbox, Google Drive, OneDrive, or any custom path — selectable via config.
- **No secrets**: all state lives in your cloud drive; the repo only contains code and config templates.
> Successor to the original `system_sync.sh` / `system_restore.sh`. Same intent, modernized tooling, modular, idempotent, scheduled by default.
---
## Requirements
- macOS (Apple Silicon or Intel)
- Bash 3.2+ (preinstalled)
- Network access for the initial Homebrew install
- A cloud drive folder you control (iCloud Drive is the default)
---
## Install
```bash
git clone https://git.surke.de/osurke/system-sync-restore.git ~/.ssr
cd ~/.ssr
./install.sh
```
`install.sh` will:
1. Symlink `bin/ssr` into `~/.local/bin` (override with `SSR_PREFIX`).
2. Seed `~/.config/ssr/ssr.conf` from `config/ssr.conf.example` (chmod 600).
3. Optionally enable the scheduled daily sync.
If `~/.local/bin` is not on your `PATH`, add it to your shell rc:
```bash
export PATH="$HOME/.local/bin:$PATH"
```
---
## Usage
```text
ssr install First-time install: link binary, seed config, schedule sync
ssr sync One-shot backup → cloud
ssr restore [Brewfile] Restore the machine; Brewfile defaults to the cloud copy
ssr schedule on|off Enable/disable the daily launchd job
ssr config show|edit Show or edit the active config
ssr status Show config, cloud target, last sync, schedule state
ssr version Print version
```
### First backup
```bash
ssr sync
```
Output structure in the cloud drive:
```
<cloud root>/
├── BACKUP/
│ └── <hostname> - <hardware-uuid>/
│ ├── Brewfile
│ └── Brewfile.bak
└── Mackup/
└── … (managed by mackup)
```
### Restore on a new machine
```bash
git clone https://git.surke.de/osurke/system-sync-restore.git ~/.ssr
cd ~/.ssr && ./install.sh
ssr restore
```
By default `ssr restore` picks the Brewfile from `<cloud>/BACKUP/<host> - <uuid>/Brewfile`. To restore from a different machine's snapshot, pass an explicit path:
```bash
ssr restore "$HOME/Library/Mobile Documents/com~apple~CloudDocs/BACKUP/oldhost - ABCD-1234/Brewfile"
```
---
## Configuration
Config lives in `~/.config/ssr/ssr.conf` (override with `--config` or `$SSR_CONFIG`). The file is sourced by bash but only recognised `KEY=VALUE` lines are accepted; anything else causes `ssr` to abort.
| Key | Default | Purpose |
|-----|---------|---------|
| `SSR_CLOUD_PROVIDER` | `icloud` | `icloud`\|`dropbox`\|`googledrive`\|`onedrive`\|`custom` |
| `SSR_CLOUD_PATH` | — | Absolute path; only used when provider is `custom` |
| `SSR_BACKUP_SUBDIR` | `BACKUP` | Subfolder for per-host backups |
| `SSR_MACKUP_SUBDIR` | `Mackup` | Subfolder used by mackup |
| `SSR_BREW_UPGRADE` | `true` | Run `brew upgrade` during sync |
| `SSR_BREW_CLEANUP` | `true` | Run `brew cleanup` after sync |
| `SSR_DISABLE_GATEKEEPER` | `false` | Run `spctl --global-disable` during restore (security trade-off) |
| `SSR_OPEN_CASKS` | `false` | Open every installed cask after restore for first-run config |
| `SSR_MACOS_DEFAULTS` | repo's `config/macos-defaults.sh` | Override path to your own defaults script |
| `SSR_LAUNCHD_LABEL` | `de.surke.ssr.sync` | launchd job label |
| `SSR_SCHEDULE_HOUR` | `9` | Daily run hour (023, local time) |
| `SSR_SCHEDULE_MINUTE` | `0` | Daily run minute |
| `SSR_PREFIX` | `$HOME/.local/bin` | Install prefix for the `ssr` symlink |
| `SSR_STATE_DIR` | `$HOME/.local/state/ssr` | Internal state directory |
| `SSR_LOG_DIR` | `$HOME/.local/state/ssr/logs` | launchd log directory |
### Cloud provider paths resolved automatically
| Provider | Resolved root |
|----------|---------------|
| `icloud` | `~/Library/Mobile Documents/com~apple~CloudDocs` |
| `dropbox` | `~/Library/CloudStorage/Dropbox*` (fallback `~/Dropbox`) |
| `googledrive` | `~/Library/CloudStorage/GoogleDrive-*` |
| `onedrive` | `~/Library/CloudStorage/OneDrive-*` |
| `custom` | Value of `SSR_CLOUD_PATH` |
---
## Scheduled sync
```bash
ssr schedule on # write & load ~/Library/LaunchAgents/<label>.plist
ssr schedule off # unload & remove
ssr schedule status # check
```
Logs land in `$SSR_LOG_DIR/sync.out.log` and `sync.err.log`. Tail them:
```bash
tail -f ~/.local/state/ssr/logs/sync.err.log
```
Show launchd-level details:
```bash
launchctl list | grep ssr
log show --predicate 'process == "ssr"' --last 1h
```
---
## Project layout
```
.
├── install.sh # Thin wrapper around bin/ssr-install
├── bin/
│ ├── ssr # CLI dispatcher
│ ├── ssr-install # Local install + scheduling
│ ├── ssr-sync # Backup to cloud
│ ├── ssr-restore # Rebuild a Mac from the backup
│ ├── ssr-schedule # launchd on/off/status
│ ├── ssr-config # show/edit/path
│ └── ssr-status # state summary
├── lib/
│ ├── common.sh # Logging, validation, config loader
│ ├── cloud.sh # Provider abstraction
│ ├── brew.sh # Homebrew install + Brewfile ops
│ ├── mackup.sh # Mackup install + backup/restore
│ └── macos.sh # macOS-specific actions (Rosetta, defaults, etc.)
├── config/
│ ├── ssr.conf.example # Reference user config
│ └── macos-defaults.sh # Default macOS preferences
├── launchd/
│ └── de.surke.ssr.sync.plist # Template for the scheduled agent
├── VERSION
├── LICENSE
└── README.md
```
All dependencies are inside the repository; the only external requirements are macOS itself and `curl` (for the one-time Homebrew bootstrap).
---
## Security
- No secrets in this repo. All cloud data lives in the user's cloud drive.
- The config file is sourced but **validated**: only `# comments` and `KEY=VALUE` lines are allowed. Foreign content aborts execution.
- `chmod 600` is applied to `ssr.conf` and the rendered launchd plist.
- `SSR_DISABLE_GATEKEEPER=true` is opt-in only; off by default.
- No remote pipes (`curl … | bash`) except Homebrew's own bootstrap during install.
---
## Uninstall
```bash
ssr schedule off
rm -f ~/.local/bin/ssr
rm -rf ~/.config/ssr ~/.local/state/ssr
```
The cloud data is left untouched.
---
## Migration from the legacy scripts
The old `system_sync.sh` / `system_restore.sh` map onto the new commands:
| Legacy | New |
|--------|-----|
| `system_sync.sh` | `ssr sync` |
| `system_restore.sh <Brewfile>` | `ssr restore <Brewfile>` |
| Manual cron / Lingon entry | `ssr schedule on` |
| Hard-coded iCloud path | `SSR_CLOUD_PROVIDER` in `ssr.conf` |
| Inline `.macos` block | `config/macos-defaults.sh` (overridable) |
---
## License
MIT. See [LICENSE](LICENSE).