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:
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# macOS noise
|
||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
|
||||||
|
# User-local state (never commit)
|
||||||
|
.local/
|
||||||
|
.config/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.cursor/
|
||||||
|
|
||||||
|
# User-rendered launchd plist (template lives in launchd/)
|
||||||
|
*.plist.local
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 ssr contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -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 (0–23, 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).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssr — System Sync & Restore CLI dispatcher.
|
||||||
|
# Resolves the repository root, sources libs, and forwards to a subcommand.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Resolve script directory even when invoked via a symlink (e.g. /usr/local/bin/ssr).
|
||||||
|
SOURCE="${BASH_SOURCE[0]}"
|
||||||
|
while [[ -L "$SOURCE" ]]; do
|
||||||
|
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
||||||
|
SOURCE="$(readlink "$SOURCE")"
|
||||||
|
[[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
|
||||||
|
done
|
||||||
|
SSR_BIN_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
||||||
|
SSR_ROOT="$(cd "$SSR_BIN_DIR/.." && pwd)"
|
||||||
|
export SSR_ROOT SSR_BIN_DIR
|
||||||
|
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "$SSR_ROOT/lib/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
ssr — System Sync & Restore for macOS
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ssr <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
install First-time install: links binary, seeds config, schedules sync.
|
||||||
|
sync Run a backup: brew bundle dump + mackup backup → cloud.
|
||||||
|
restore [Brewfile] Restore a machine: brew bundle + mackup restore + macOS defaults.
|
||||||
|
schedule on|off Enable/disable the scheduled launchd sync job.
|
||||||
|
config show|edit Print or edit the active config file.
|
||||||
|
status Show last sync time, cloud target, and scheduled state.
|
||||||
|
version Print version.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c, --config PATH Use a specific config file (default: ~/.config/ssr/ssr.conf).
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
SSR_CONFIG Same as --config.
|
||||||
|
|
||||||
|
Docs: see README.md in the repository root.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse global flags.
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-c|--config) export SSR_CONFIG="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) ssr::die "Unknown global flag: $1 (use -h for help)" ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ $# -gt 0 ]] || { usage; exit 1; }
|
||||||
|
|
||||||
|
cmd="$1"; shift
|
||||||
|
case "$cmd" in
|
||||||
|
install|sync|restore|schedule|config|status)
|
||||||
|
target="$SSR_BIN_DIR/ssr-$cmd"
|
||||||
|
[[ -x "$target" ]] || ssr::die "Subcommand not executable: $target"
|
||||||
|
exec "$target" "$@"
|
||||||
|
;;
|
||||||
|
version|-v|--version)
|
||||||
|
cat "$SSR_ROOT/VERSION" 2>/dev/null || echo "dev"
|
||||||
|
;;
|
||||||
|
help|-h|--help) usage ;;
|
||||||
|
*) ssr::err "Unknown command: $cmd"; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssr-config — show or edit the active configuration file.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "$SSR_ROOT/lib/common.sh"
|
||||||
|
|
||||||
|
cfg="${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}"
|
||||||
|
|
||||||
|
case "${1:-show}" in
|
||||||
|
show)
|
||||||
|
if [[ -f "$cfg" ]]; then
|
||||||
|
echo "# $cfg"
|
||||||
|
cat "$cfg"
|
||||||
|
else
|
||||||
|
ssr::warn "No config found at $cfg"
|
||||||
|
echo "Run: ssr install to seed one from the example."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
edit)
|
||||||
|
ssr::ensure_dir "$(dirname "$cfg")"
|
||||||
|
if [[ ! -f "$cfg" ]]; then
|
||||||
|
cp "$SSR_ROOT/config/ssr.conf.example" "$cfg"
|
||||||
|
chmod 600 "$cfg"
|
||||||
|
fi
|
||||||
|
"${EDITOR:-vi}" "$cfg"
|
||||||
|
;;
|
||||||
|
path)
|
||||||
|
echo "$cfg"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ssr::die "Usage: ssr config [show|edit|path]"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssr-install — local install: symlink CLI, seed config, optionally schedule.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "$SSR_ROOT/lib/common.sh"
|
||||||
|
|
||||||
|
ssr::require_macos
|
||||||
|
|
||||||
|
PREFIX="${SSR_PREFIX:-$HOME/.local/bin}"
|
||||||
|
CONFIG_DIR="${SSR_CONFIG_DIR:-$HOME/.config/ssr}"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/ssr.conf"
|
||||||
|
|
||||||
|
ssr::log "Installing ssr"
|
||||||
|
ssr::log " prefix: $PREFIX"
|
||||||
|
ssr::log " config: $CONFIG_FILE"
|
||||||
|
|
||||||
|
ssr::ensure_dir "$PREFIX"
|
||||||
|
ssr::ensure_dir "$CONFIG_DIR"
|
||||||
|
|
||||||
|
# Symlink the main CLI so `ssr` works from anywhere.
|
||||||
|
ln -sfn "$SSR_ROOT/bin/ssr" "$PREFIX/ssr"
|
||||||
|
ssr::ok "Linked $PREFIX/ssr → $SSR_ROOT/bin/ssr"
|
||||||
|
|
||||||
|
# Seed config from the example (do not overwrite an existing user config).
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
cp "$SSR_ROOT/config/ssr.conf.example" "$CONFIG_FILE"
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
ssr::ok "Seeded $CONFIG_FILE (chmod 600)"
|
||||||
|
else
|
||||||
|
ssr::log "Config already exists, leaving untouched: $CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Path hint if PREFIX is not on PATH.
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$PREFIX:"*) ;;
|
||||||
|
*) ssr::warn "$PREFIX is not on your PATH — add it to your shell rc:"
|
||||||
|
echo " export PATH=\"$PREFIX:\$PATH\"" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Offer to enable the scheduled sync.
|
||||||
|
if ssr::confirm "Enable daily scheduled sync (launchd)?" 30; then
|
||||||
|
"$SSR_ROOT/bin/ssr-schedule" on
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssr::ok "Install complete. Run: ssr status"
|
||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssr-restore — restore brew packages, mackup data, macOS defaults.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "$SSR_ROOT/lib/common.sh"
|
||||||
|
# shellcheck source=../lib/cloud.sh
|
||||||
|
source "$SSR_ROOT/lib/cloud.sh"
|
||||||
|
# shellcheck source=../lib/brew.sh
|
||||||
|
source "$SSR_ROOT/lib/brew.sh"
|
||||||
|
# shellcheck source=../lib/mackup.sh
|
||||||
|
source "$SSR_ROOT/lib/mackup.sh"
|
||||||
|
# shellcheck source=../lib/macos.sh
|
||||||
|
source "$SSR_ROOT/lib/macos.sh"
|
||||||
|
|
||||||
|
ssr::require_macos
|
||||||
|
ssr::load_config
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ssr restore [BREWFILE]
|
||||||
|
|
||||||
|
If BREWFILE is omitted, the latest cloud backup is used:
|
||||||
|
<cloud>/BACKUP/<host> - <uuid>/Brewfile
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && { usage; exit 0; }
|
||||||
|
|
||||||
|
ssr::log "ssr restore starting"
|
||||||
|
ssr::cloud::ensure_available
|
||||||
|
backup_dir="$(ssr::cloud::backup_dir)"
|
||||||
|
mackup_dir="$(ssr::cloud::mackup_dir)"
|
||||||
|
|
||||||
|
brewfile="${1:-$backup_dir/Brewfile}"
|
||||||
|
[[ -f "$brewfile" ]] || ssr::die "Brewfile not found: $brewfile"
|
||||||
|
ssr::log "Using Brewfile: $brewfile"
|
||||||
|
|
||||||
|
ssr::confirm "Configure iCloud / cloud drive and proceed with restore?" 60 \
|
||||||
|
|| ssr::die "Aborted by user"
|
||||||
|
|
||||||
|
ssr::macos::disable_gatekeeper
|
||||||
|
ssr::macos::install_rosetta
|
||||||
|
|
||||||
|
ssr::brew::ensure_installed
|
||||||
|
ssr::brew::update
|
||||||
|
ssr::brew::restore "$brewfile"
|
||||||
|
|
||||||
|
ssr::mackup::ensure_installed
|
||||||
|
ssr::mackup::configure "$mackup_dir"
|
||||||
|
ssr::mackup::restore
|
||||||
|
|
||||||
|
ssr::macos::open_installed_casks
|
||||||
|
ssr::macos::apply_defaults
|
||||||
|
|
||||||
|
ssr::ok "ssr restore completed"
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssr-schedule — manage the launchd scheduled sync job.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "$SSR_ROOT/lib/common.sh"
|
||||||
|
|
||||||
|
ssr::require_macos
|
||||||
|
ssr::load_config
|
||||||
|
|
||||||
|
LABEL="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
||||||
|
PLIST_SRC="$SSR_ROOT/launchd/${LABEL}.plist"
|
||||||
|
PLIST_DST="$HOME/Library/LaunchAgents/${LABEL}.plist"
|
||||||
|
LOG_DIR="${SSR_LOG_DIR:-$HOME/.local/state/ssr/logs}"
|
||||||
|
INTERVAL_HOUR="${SSR_SCHEDULE_HOUR:-9}"
|
||||||
|
INTERVAL_MIN="${SSR_SCHEDULE_MINUTE:-0}"
|
||||||
|
SSR_BIN="$SSR_ROOT/bin/ssr"
|
||||||
|
|
||||||
|
render_plist() {
|
||||||
|
ssr::ensure_dir "$LOG_DIR"
|
||||||
|
ssr::ensure_dir "$(dirname "$PLIST_DST")"
|
||||||
|
# Substitute placeholders from the template into the user's LaunchAgents dir.
|
||||||
|
sed \
|
||||||
|
-e "s|@@LABEL@@|${LABEL}|g" \
|
||||||
|
-e "s|@@SSR_BIN@@|${SSR_BIN}|g" \
|
||||||
|
-e "s|@@LOG_DIR@@|${LOG_DIR}|g" \
|
||||||
|
-e "s|@@HOUR@@|${INTERVAL_HOUR}|g" \
|
||||||
|
-e "s|@@MINUTE@@|${INTERVAL_MIN}|g" \
|
||||||
|
"$PLIST_SRC" > "$PLIST_DST"
|
||||||
|
chmod 600 "$PLIST_DST"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_on() {
|
||||||
|
[[ -f "$PLIST_SRC" ]] || ssr::die "launchd template missing: $PLIST_SRC"
|
||||||
|
render_plist
|
||||||
|
# Reload to pick up changes if it was already loaded.
|
||||||
|
launchctl unload "$PLIST_DST" 2>/dev/null || true
|
||||||
|
launchctl load -w "$PLIST_DST"
|
||||||
|
ssr::ok "Scheduled sync enabled (${INTERVAL_HOUR}:$(printf '%02d' "$INTERVAL_MIN") daily) → $PLIST_DST"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_off() {
|
||||||
|
if [[ -f "$PLIST_DST" ]]; then
|
||||||
|
launchctl unload "$PLIST_DST" 2>/dev/null || true
|
||||||
|
rm -f "$PLIST_DST"
|
||||||
|
ssr::ok "Scheduled sync disabled"
|
||||||
|
else
|
||||||
|
ssr::log "Scheduled sync was not installed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
if [[ -f "$PLIST_DST" ]] && launchctl list | grep -q "$LABEL"; then
|
||||||
|
ssr::ok "Scheduled sync: active (label=$LABEL)"
|
||||||
|
else
|
||||||
|
ssr::log "Scheduled sync: inactive"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
on|enable) cmd_on ;;
|
||||||
|
off|disable) cmd_off ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
*) ssr::die "Usage: ssr schedule [on|off|status]" ;;
|
||||||
|
esac
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssr-status — print current state: config, cloud target, last sync, schedule.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "$SSR_ROOT/lib/common.sh"
|
||||||
|
# shellcheck source=../lib/cloud.sh
|
||||||
|
source "$SSR_ROOT/lib/cloud.sh"
|
||||||
|
|
||||||
|
ssr::load_config
|
||||||
|
|
||||||
|
cfg="${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}"
|
||||||
|
provider="${SSR_CLOUD_PROVIDER:-icloud}"
|
||||||
|
|
||||||
|
echo "ssr status"
|
||||||
|
echo " repo: $SSR_ROOT"
|
||||||
|
echo " config: $cfg $([[ -f "$cfg" ]] && echo '(present)' || echo '(missing)')"
|
||||||
|
echo " provider: $provider"
|
||||||
|
|
||||||
|
if cloud_dir="$(ssr::cloud::resolve_dir 2>/dev/null)"; then
|
||||||
|
echo " cloud dir: $cloud_dir $([[ -d "$cloud_dir" ]] && echo '(ok)' || echo '(unreachable)')"
|
||||||
|
echo " backup dir: $(ssr::cloud::backup_dir 2>/dev/null || echo 'n/a')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
state_dir="${SSR_STATE_DIR:-$HOME/.local/state/ssr}"
|
||||||
|
if [[ -f "$state_dir/last-sync" ]]; then
|
||||||
|
echo " last sync: $(cat "$state_dir/last-sync")"
|
||||||
|
else
|
||||||
|
echo " last sync: never"
|
||||||
|
fi
|
||||||
|
|
||||||
|
label="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
||||||
|
if launchctl list 2>/dev/null | grep -q "$label"; then
|
||||||
|
echo " schedule: active ($label)"
|
||||||
|
else
|
||||||
|
echo " schedule: inactive"
|
||||||
|
fi
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssr-sync — back up Homebrew + Mackup state to the configured cloud dir.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "$SSR_ROOT/lib/common.sh"
|
||||||
|
# shellcheck source=../lib/cloud.sh
|
||||||
|
source "$SSR_ROOT/lib/cloud.sh"
|
||||||
|
# shellcheck source=../lib/brew.sh
|
||||||
|
source "$SSR_ROOT/lib/brew.sh"
|
||||||
|
# shellcheck source=../lib/mackup.sh
|
||||||
|
source "$SSR_ROOT/lib/mackup.sh"
|
||||||
|
|
||||||
|
ssr::require_macos
|
||||||
|
ssr::load_config
|
||||||
|
|
||||||
|
ssr::log "ssr sync starting"
|
||||||
|
ssr::syslog "sync starting"
|
||||||
|
|
||||||
|
ssr::cloud::ensure_available
|
||||||
|
backup_dir="$(ssr::cloud::backup_dir)"
|
||||||
|
mackup_dir="$(ssr::cloud::mackup_dir)"
|
||||||
|
ssr::ensure_dir "$backup_dir"
|
||||||
|
ssr::ensure_dir "$mackup_dir"
|
||||||
|
|
||||||
|
ssr::brew::ensure_installed
|
||||||
|
ssr::mackup::ensure_installed
|
||||||
|
ssr::mackup::configure "$mackup_dir"
|
||||||
|
|
||||||
|
ssr::brew::update
|
||||||
|
ssr::brew::upgrade
|
||||||
|
ssr::mackup::backup
|
||||||
|
ssr::brew::dump "$backup_dir"
|
||||||
|
ssr::brew::cleanup
|
||||||
|
|
||||||
|
# Record sync time so `ssr status` can report freshness.
|
||||||
|
state_dir="${SSR_STATE_DIR:-$HOME/.local/state/ssr}"
|
||||||
|
ssr::ensure_dir "$state_dir"
|
||||||
|
date '+%Y-%m-%dT%H:%M:%S%z' > "$state_dir/last-sync"
|
||||||
|
|
||||||
|
ssr::ok "ssr sync completed → $backup_dir"
|
||||||
|
ssr::syslog "sync completed"
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# macos-defaults.sh — opinionated macOS preferences applied during restore.
|
||||||
|
# Override by setting SSR_MACOS_DEFAULTS in your config to your own script.
|
||||||
|
# Each block is independent; failures are logged but do not abort the script.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if ! "$@"; then
|
||||||
|
printf '[macos-defaults] failed: %s\n' "$*" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dock: position + appearance
|
||||||
|
run defaults write com.apple.dock pinning -string middle
|
||||||
|
run defaults write com.apple.dock mouse-over-hilte-stack -bool true
|
||||||
|
|
||||||
|
# Dock: recent applications stack
|
||||||
|
run defaults write com.apple.dock persistent-others -array-add \
|
||||||
|
'{ "tile-data" = { "list-type" = 1; }; "tile-type" = "recents-tile"; }'
|
||||||
|
|
||||||
|
# Login window message (requires sudo; skipped without)
|
||||||
|
if sudo -n true 2>/dev/null; then
|
||||||
|
run sudo defaults write /Library/Preferences/com.apple.loginwindow LoginwindowText "Welcome back..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Time Machine on unsupported network volumes
|
||||||
|
run defaults write com.apple.systempreferences TMShowUnsupportedNetworkVolumes -bool true
|
||||||
|
|
||||||
|
# Mail: prefer plain text
|
||||||
|
run defaults write com.apple.mail PreferPlainText -bool true
|
||||||
|
|
||||||
|
# Finder: show hidden files + path bar
|
||||||
|
run defaults write com.apple.finder AppleShowAllFiles -bool true
|
||||||
|
run defaults write com.apple.finder ShowPathbar -bool true
|
||||||
|
|
||||||
|
# Screenshots: save to ~/Pictures/Screenshots as PNG
|
||||||
|
mkdir -p "$HOME/Pictures/Screenshots"
|
||||||
|
run defaults write com.apple.screencapture location -string "$HOME/Pictures/Screenshots"
|
||||||
|
run defaults write com.apple.screencapture type -string png
|
||||||
|
|
||||||
|
# Reload affected services
|
||||||
|
killall Dock 2>/dev/null || true
|
||||||
|
killall Finder 2>/dev/null || true
|
||||||
|
killall SystemUIServer 2>/dev/null || true
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# ssr.conf — System Sync & Restore configuration.
|
||||||
|
# Format: simple KEY=VALUE pairs. Comments (# …) and blank lines allowed.
|
||||||
|
# This file is sourced by bash; only the keys below are recognized.
|
||||||
|
# Anything else will cause ssr to abort.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cloud storage
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Provider: icloud | dropbox | googledrive | onedrive | custom
|
||||||
|
SSR_CLOUD_PROVIDER=icloud
|
||||||
|
|
||||||
|
# Only used when SSR_CLOUD_PROVIDER=custom. Absolute path.
|
||||||
|
# SSR_CLOUD_PATH=/Volumes/MyNAS/macos-sync
|
||||||
|
|
||||||
|
# Subfolders created inside the cloud root.
|
||||||
|
SSR_BACKUP_SUBDIR=BACKUP
|
||||||
|
SSR_MACKUP_SUBDIR=Mackup
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Homebrew behavior during `ssr sync`
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SSR_BREW_UPGRADE=true
|
||||||
|
SSR_BREW_CLEANUP=true
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# macOS restore behavior
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Disabling Gatekeeper allows apps from anywhere. Off by default.
|
||||||
|
SSR_DISABLE_GATEKEEPER=false
|
||||||
|
|
||||||
|
# Open every installed cask once after restore (for first-run config).
|
||||||
|
SSR_OPEN_CASKS=false
|
||||||
|
|
||||||
|
# Path to the macOS defaults script applied at the end of `ssr restore`.
|
||||||
|
# Leave commented to use the repo default (config/macos-defaults.sh).
|
||||||
|
# SSR_MACOS_DEFAULTS=/path/to/your/macos-defaults.sh
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Scheduling
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SSR_LAUNCHD_LABEL=de.surke.ssr.sync
|
||||||
|
SSR_SCHEDULE_HOUR=9
|
||||||
|
SSR_SCHEDULE_MINUTE=0
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Paths
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SSR_STATE_DIR=$HOME/.local/state/ssr
|
||||||
|
# SSR_LOG_DIR=$HOME/.local/state/ssr/logs
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# install.sh — top-level bootstrap.
|
||||||
|
# Delegates to bin/ssr-install so a fresh clone can run a single command:
|
||||||
|
# ./install.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec "$DIR/bin/ssr-install" "$@"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>@@LABEL@@</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>@@SSR_BIN@@</string>
|
||||||
|
<string>sync</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>StartCalendarInterval</key>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>@@HOUR@@</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>@@MINUTE@@</integer>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>@@LOG_DIR@@/sync.out.log</string>
|
||||||
|
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>@@LOG_DIR@@/sync.err.log</string>
|
||||||
|
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PATH</key>
|
||||||
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>ProcessType</key>
|
||||||
|
<string>Background</string>
|
||||||
|
|
||||||
|
<key>LowPriorityIO</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# legacy/
|
||||||
|
|
||||||
|
Original `system_sync.sh` / `system_restore.sh` kept for reference and
|
||||||
|
historical diffing. **Do not run these in production** — they have known
|
||||||
|
issues (no `set -euo pipefail`, broken `MACOS = ".macos"` assignment with
|
||||||
|
spaces, `` zero-width character before the `ln` call, deprecated
|
||||||
|
`spctl --master-disable`, hard-coded iCloud paths, no error handling).
|
||||||
|
|
||||||
|
Use the top-level `ssr` CLI instead. See [../README.md](../README.md).
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Setup script for setting up a new macos machine
|
||||||
|
echo "Starting setup"
|
||||||
|
|
||||||
|
declare -A nonPckgApps
|
||||||
|
nonPckgApps[PhraseExpress]="https://www.phraseexpress.com/de/mac/download/"
|
||||||
|
nonPckgApps[AusweisApp2]="https://www.ausweisapp.bund.de/download/windows-und-mac/"
|
||||||
|
#nonPckgApps[]=""
|
||||||
|
|
||||||
|
BREWFILE="$1"
|
||||||
|
LOGGER_BIN="/usr/bin/logger -ist '$0'"
|
||||||
|
# Check for Homebrew to be present, install if it's missing
|
||||||
|
if test ! $(which brew); then
|
||||||
|
echo "Installing homebrew..."
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||||
|
fi
|
||||||
|
BREW_BIN=$(which brew)
|
||||||
|
BREW_PREFX=$(brew --prefix)
|
||||||
|
CASK_DIR=$(brew --caskroom)
|
||||||
|
# Update homebrew recipes
|
||||||
|
$BREW_BIN update
|
||||||
|
$BREW_BIN install mackup
|
||||||
|
MACKUP_DIR="$HOME/Library/Mobile Documents/com~apple~CloudDocs/Mackup"
|
||||||
|
ln -s $MACKUP_DIR/.mackup.cfg $HOME/.mackup.cfg
|
||||||
|
MACKUP_BIN=$(which mackup)
|
||||||
|
|
||||||
|
|
||||||
|
read -t 60 -n 1 -p "configure iCloud Drive and confirm to continue with Brewfile '$BREWFILE'? [y/N]: " answer
|
||||||
|
if [ $? == 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "allow Apps downloaded from anywhere…"
|
||||||
|
sudo spctl --master-disable
|
||||||
|
#open /System/Library/PreferencePanes/Security.prefPane
|
||||||
|
echo
|
||||||
|
echo "Make sure Rosetta is installed..."
|
||||||
|
sudo softwareupdate --install-rosetta
|
||||||
|
|
||||||
|
$BREW_BIN bundle --verbose --file="$BREWFILE" | $LOGGER_BIN
|
||||||
|
$MACKUP_BIN restore
|
||||||
|
|
||||||
|
#echo "this script is unable to install the following apps..."
|
||||||
|
#for key val in ${(kv)nonPckgApps}; do
|
||||||
|
# echo "$key"
|
||||||
|
# open "$val"
|
||||||
|
#done
|
||||||
|
|
||||||
|
read -n 1 -p "Hit return to continue: " answer
|
||||||
|
echo "these apps will need manual config and opened in the background..."
|
||||||
|
find "$CASK_DIR" -iname "*.app" | while read APP;
|
||||||
|
do
|
||||||
|
open "$APP";
|
||||||
|
done
|
||||||
|
|
||||||
|
read -n 1 -p "Hit return to continue: " answer
|
||||||
|
echo "Configuring OS... (may need sudo)"
|
||||||
|
MACOS = ".macos"
|
||||||
|
source "$MACOS"
|
||||||
|
# Pin The Dock To The Left(start), Bottom(middle), Right(end)
|
||||||
|
defaults write com.apple.dock pinning -string middle
|
||||||
|
# Set dock to a flat 2D version
|
||||||
|
defaults write com.apple.dock no-glass -boolean YES && killall Dock;
|
||||||
|
# Recent Applications Stack in Dock
|
||||||
|
defaults write com.apple.dock persistent-others -array-add '{ "tile-data" = { "list-type" = 1; }; "tile-type" = "recents-tile"; }' && killall Dock;
|
||||||
|
# put a small gradient behind an icon
|
||||||
|
defaults write com.apple.dock mouse-over-hilte-stack -boolean YES && killall Dock;
|
||||||
|
# add a message of your choice to the Login window
|
||||||
|
sudo defaults write /Library/Preferences/com.apple.loginwindow LoginwindowText "Welcome back..."
|
||||||
|
# Enable Time Machine On Unsupported Drives
|
||||||
|
defaults write com.apple.systempreferences TMShowUnsupportedNetworkVolumes 1
|
||||||
|
# Force Mail To Display In Plain Text
|
||||||
|
defaults write com.apple.mail PreferPlainText -bool TRUE
|
||||||
|
# widegts on the dashboard
|
||||||
|
defaults write com.apple.dashboard devmode YES
|
||||||
|
echo "Macbook setup completed!"
|
||||||
|
else
|
||||||
|
echo "Can't wait anymore!"
|
||||||
|
fi
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
LOGGER_BIN="/usr/bin/logger -ist '$0'"
|
||||||
|
UUID=$(system_profiler SPHardwareDataType | awk '/UUID/ { print $3; }')
|
||||||
|
HOSTNAME=$(hostname -s)
|
||||||
|
BACKUPDIR="$HOME/Library/Mobile Documents/com~apple~CloudDocs/BACKUP/$HOSTNAME - $UUID"
|
||||||
|
|
||||||
|
# Check for Homebrew to be present, install if it's missing
|
||||||
|
if test ! $(which brew); then
|
||||||
|
echo "Installing homebrew..."
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||||
|
fi
|
||||||
|
BREW_BIN=$(which brew)
|
||||||
|
BREW_PREFX=$(brew --prefix)
|
||||||
|
CASK_DIR=$(brew --caskroom)
|
||||||
|
|
||||||
|
# Update homebrew recipes
|
||||||
|
$BREW_BIN update
|
||||||
|
MACKUP_DIR="$HOME/Library/Mobile Documents/com~apple~CloudDocs/Mackup"
|
||||||
|
MACKUP_BIN=$(which mackup)
|
||||||
|
$MACKUP_BIN -f backup
|
||||||
|
|
||||||
|
mkdir -vp "$BACKUPDIR"
|
||||||
|
pushd "$BACKUPDIR" >/dev/null
|
||||||
|
cp -vf Brewfile{,.bak}
|
||||||
|
$BREW_BIN bundle dump --describe --quiet --force | $LOGGER_BIN
|
||||||
|
popd >/dev/null
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/brew.sh — Homebrew install + Brewfile dump/restore.
|
||||||
|
|
||||||
|
ssr::brew::ensure_installed() {
|
||||||
|
if command -v brew >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ssr::log "Homebrew not found — installing (non-interactive)"
|
||||||
|
NONINTERACTIVE=1 /bin/bash -c \
|
||||||
|
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
|
# Ensure brew is on PATH for both Apple Silicon and Intel layouts.
|
||||||
|
if [[ -x /opt/homebrew/bin/brew ]]; then
|
||||||
|
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||||
|
elif [[ -x /usr/local/bin/brew ]]; then
|
||||||
|
eval "$(/usr/local/bin/brew shellenv)"
|
||||||
|
fi
|
||||||
|
command -v brew >/dev/null 2>&1 || ssr::die "brew still not on PATH after install"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::brew::update() {
|
||||||
|
ssr::log "brew update"
|
||||||
|
brew update --quiet
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::brew::upgrade() {
|
||||||
|
[[ "${SSR_BREW_UPGRADE:-true}" == "true" ]] || return 0
|
||||||
|
ssr::log "brew upgrade"
|
||||||
|
brew upgrade --quiet || ssr::warn "brew upgrade reported errors (continuing)"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::brew::cleanup() {
|
||||||
|
[[ "${SSR_BREW_CLEANUP:-true}" == "true" ]] || return 0
|
||||||
|
ssr::log "brew cleanup"
|
||||||
|
brew cleanup --quiet || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dumps Brewfile to the cloud backup dir with a timestamped rotation copy.
|
||||||
|
ssr::brew::dump() {
|
||||||
|
local backup_dir="$1" brewfile="$1/Brewfile"
|
||||||
|
ssr::ensure_dir "$backup_dir"
|
||||||
|
if [[ -f "$brewfile" ]]; then
|
||||||
|
cp -f "$brewfile" "$brewfile.bak"
|
||||||
|
fi
|
||||||
|
ssr::log "brew bundle dump → $brewfile"
|
||||||
|
(
|
||||||
|
cd "$backup_dir"
|
||||||
|
brew bundle dump --describe --quiet --force --file=Brewfile
|
||||||
|
)
|
||||||
|
ssr::ok "Brewfile written ($(wc -l < "$brewfile" | tr -d ' ') lines)"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::brew::restore() {
|
||||||
|
local brewfile="$1"
|
||||||
|
[[ -f "$brewfile" ]] || ssr::die "Brewfile not found: $brewfile"
|
||||||
|
ssr::log "brew bundle install ← $brewfile"
|
||||||
|
brew bundle install --file="$brewfile" --no-lock --verbose
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/cloud.sh — cloud storage provider abstraction.
|
||||||
|
# Resolves SSR_CLOUD_DIR based on SSR_CLOUD_PROVIDER from config.
|
||||||
|
|
||||||
|
ssr::cloud::resolve_dir() {
|
||||||
|
local provider="${SSR_CLOUD_PROVIDER:-icloud}" custom="${SSR_CLOUD_PATH:-}"
|
||||||
|
|
||||||
|
case "$provider" in
|
||||||
|
icloud)
|
||||||
|
printf '%s\n' "$HOME/Library/Mobile Documents/com~apple~CloudDocs"
|
||||||
|
;;
|
||||||
|
dropbox)
|
||||||
|
if [[ -d "$HOME/Library/CloudStorage" ]]; then
|
||||||
|
find "$HOME/Library/CloudStorage" -maxdepth 1 -type d -name 'Dropbox*' -print -quit 2>/dev/null \
|
||||||
|
|| printf '%s\n' "$HOME/Dropbox"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$HOME/Dropbox"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
googledrive)
|
||||||
|
find "$HOME/Library/CloudStorage" -maxdepth 1 -type d -name 'GoogleDrive-*' -print -quit 2>/dev/null \
|
||||||
|
|| ssr::die "Google Drive folder not found under ~/Library/CloudStorage"
|
||||||
|
;;
|
||||||
|
onedrive)
|
||||||
|
find "$HOME/Library/CloudStorage" -maxdepth 1 -type d -name 'OneDrive-*' -print -quit 2>/dev/null \
|
||||||
|
|| ssr::die "OneDrive folder not found under ~/Library/CloudStorage"
|
||||||
|
;;
|
||||||
|
custom)
|
||||||
|
[[ -n "$custom" ]] || ssr::die "SSR_CLOUD_PROVIDER=custom requires SSR_CLOUD_PATH"
|
||||||
|
printf '%s\n' "$custom"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ssr::die "Unknown SSR_CLOUD_PROVIDER: $provider (use: icloud|dropbox|googledrive|onedrive|custom)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the per-host backup directory under the cloud root.
|
||||||
|
ssr::cloud::backup_dir() {
|
||||||
|
local cloud host uuid
|
||||||
|
cloud="$(ssr::cloud::resolve_dir)"
|
||||||
|
host="$(hostname -s)"
|
||||||
|
uuid="$(/usr/sbin/system_profiler SPHardwareDataType 2>/dev/null \
|
||||||
|
| awk '/Hardware UUID/ { print $3 }')"
|
||||||
|
[[ -n "$uuid" ]] || uuid="no-uuid"
|
||||||
|
local subdir="${SSR_BACKUP_SUBDIR:-BACKUP}"
|
||||||
|
printf '%s/%s/%s - %s\n' "$cloud" "$subdir" "$host" "$uuid"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mackup data lives at the cloud root in a fixed folder. Mackup itself
|
||||||
|
# resolves its engine via ~/.mackup.cfg which the install routine seeds.
|
||||||
|
ssr::cloud::mackup_dir() {
|
||||||
|
local cloud
|
||||||
|
cloud="$(ssr::cloud::resolve_dir)"
|
||||||
|
printf '%s/%s\n' "$cloud" "${SSR_MACKUP_SUBDIR:-Mackup}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::cloud::ensure_available() {
|
||||||
|
local cloud
|
||||||
|
cloud="$(ssr::cloud::resolve_dir)"
|
||||||
|
[[ -d "$cloud" ]] || ssr::die "Cloud directory not available: $cloud"
|
||||||
|
ssr::ok "Cloud provider: ${SSR_CLOUD_PROVIDER:-icloud} → $cloud"
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/common.sh — shared helpers (logging, errors, validation).
|
||||||
|
# Sourced by all ssr commands. Never executed directly.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Resolve repo root from any sourced location.
|
||||||
|
SSR_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SSR_ROOT="$(cd "${SSR_LIB_DIR}/.." && pwd)"
|
||||||
|
export SSR_ROOT SSR_LIB_DIR
|
||||||
|
|
||||||
|
# Colors only when stdout is a TTY (no ANSI in launchd logs).
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
SSR_C_RED=$'\033[31m'; SSR_C_GRN=$'\033[32m'
|
||||||
|
SSR_C_YLW=$'\033[33m'; SSR_C_BLU=$'\033[34m'
|
||||||
|
SSR_C_DIM=$'\033[2m'; SSR_C_RST=$'\033[0m'
|
||||||
|
else
|
||||||
|
SSR_C_RED=""; SSR_C_GRN=""; SSR_C_YLW=""; SSR_C_BLU=""; SSR_C_DIM=""; SSR_C_RST=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssr::ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
||||||
|
|
||||||
|
ssr::log() { printf '%s %s[INFO]%s %s\n' "$(ssr::ts)" "$SSR_C_BLU" "$SSR_C_RST" "$*"; }
|
||||||
|
ssr::ok() { printf '%s %s[OK]%s %s\n' "$(ssr::ts)" "$SSR_C_GRN" "$SSR_C_RST" "$*"; }
|
||||||
|
ssr::warn() { printf '%s %s[WARN]%s %s\n' "$(ssr::ts)" "$SSR_C_YLW" "$SSR_C_RST" "$*" >&2; }
|
||||||
|
ssr::err() { printf '%s %s[ERR]%s %s\n' "$(ssr::ts)" "$SSR_C_RED" "$SSR_C_RST" "$*" >&2; }
|
||||||
|
ssr::die() { ssr::err "$@"; exit 1; }
|
||||||
|
|
||||||
|
# Mirror to syslog if available so launchd runs are findable via `log show`.
|
||||||
|
ssr::syslog() {
|
||||||
|
[[ -x /usr/bin/logger ]] || return 0
|
||||||
|
/usr/bin/logger -t "ssr" -- "$*" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::require_macos() {
|
||||||
|
[[ "$(uname -s)" == "Darwin" ]] || ssr::die "ssr requires macOS (Darwin). Detected: $(uname -s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || ssr::die "Required command missing: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Confirm prompt with optional timeout (returns 0 = yes).
|
||||||
|
ssr::confirm() {
|
||||||
|
local prompt="${1:-Continue?}" timeout="${2:-0}" reply
|
||||||
|
if [[ "$timeout" -gt 0 ]]; then
|
||||||
|
read -r -t "$timeout" -n 1 -p "${prompt} [y/N]: " reply || reply=""
|
||||||
|
else
|
||||||
|
read -r -n 1 -p "${prompt} [y/N]: " reply || reply=""
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
[[ "${reply,,}" == "y" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate a path is safe (no traversal, no shell metas). Used before any FS op.
|
||||||
|
ssr::validate_path() {
|
||||||
|
local p="$1"
|
||||||
|
[[ -n "$p" ]] || ssr::die "Empty path"
|
||||||
|
[[ "$p" != *$'\n'* && "$p" != *$'\0'* ]] || ssr::die "Path contains control chars"
|
||||||
|
case "$p" in
|
||||||
|
*..*) ssr::die "Path traversal rejected: $p" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::ensure_dir() {
|
||||||
|
local d="$1"
|
||||||
|
ssr::validate_path "$d"
|
||||||
|
[[ -d "$d" ]] || mkdir -p "$d"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Source config file if it exists. Validates that only known KEY=VALUE lines
|
||||||
|
# are present to avoid arbitrary code execution from a tampered config.
|
||||||
|
ssr::load_config() {
|
||||||
|
local cfg="${1:-${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}}"
|
||||||
|
export SSR_CONFIG="$cfg"
|
||||||
|
[[ -f "$cfg" ]] || return 0
|
||||||
|
if grep -Eq -v '^[[:space:]]*(#|$|[A-Z_][A-Z0-9_]*=)' "$cfg"; then
|
||||||
|
ssr::die "Config $cfg contains invalid lines (only KEY=VALUE allowed)"
|
||||||
|
fi
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$cfg"
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/mackup.sh — Mackup install + backup/restore.
|
||||||
|
# Note: upstream mackup is in low-maintenance mode; we keep it because it
|
||||||
|
# remains the simplest way to sync dotfiles for a wide range of apps. The
|
||||||
|
# user can swap engines via ~/.mackup.cfg without changing this script.
|
||||||
|
|
||||||
|
ssr::mackup::ensure_installed() {
|
||||||
|
if command -v mackup >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ssr::log "Installing mackup via brew"
|
||||||
|
brew install mackup
|
||||||
|
}
|
||||||
|
|
||||||
|
# Writes ~/.mackup.cfg pointing the engine at the cloud directory.
|
||||||
|
# Idempotent: rewrites only if content differs.
|
||||||
|
ssr::mackup::configure() {
|
||||||
|
local mackup_dir="$1"
|
||||||
|
local cfg="$HOME/.mackup.cfg"
|
||||||
|
local desired
|
||||||
|
desired="$(printf '[storage]\nengine = file_system\npath = %s\ndirectory = mackup\n' "$mackup_dir")"
|
||||||
|
|
||||||
|
if [[ -f "$cfg" ]] && [[ "$(cat "$cfg")" == "$desired" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -L "$cfg" || -f "$cfg" ]]; then
|
||||||
|
cp -f "$cfg" "$cfg.bak.$(date +%s)" 2>/dev/null || true
|
||||||
|
rm -f "$cfg"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$desired" > "$cfg"
|
||||||
|
ssr::ok "Wrote $cfg"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::mackup::backup() {
|
||||||
|
ssr::log "mackup backup"
|
||||||
|
mackup -f backup
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::mackup::restore() {
|
||||||
|
ssr::log "mackup restore"
|
||||||
|
mackup restore
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/macos.sh — macOS system preferences.
|
||||||
|
# Sourced by `ssr restore`. Each setting is idempotent.
|
||||||
|
|
||||||
|
ssr::macos::apply_defaults() {
|
||||||
|
local script="${1:-${SSR_MACOS_DEFAULTS:-$SSR_ROOT/config/macos-defaults.sh}}"
|
||||||
|
if [[ ! -f "$script" ]]; then
|
||||||
|
ssr::warn "macOS defaults script missing: $script (skipping)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ssr::log "Applying macOS defaults from $script"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
bash "$script"
|
||||||
|
ssr::ok "macOS defaults applied"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::macos::install_rosetta() {
|
||||||
|
if /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$(uname -m)" != "arm64" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ssr::log "Installing Rosetta 2"
|
||||||
|
sudo softwareupdate --install-rosetta --agree-to-license
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allows non-App-Store apps to run. Requires sudo. Skipped unless explicitly
|
||||||
|
# enabled, because disabling Gatekeeper has security implications.
|
||||||
|
ssr::macos::disable_gatekeeper() {
|
||||||
|
[[ "${SSR_DISABLE_GATEKEEPER:-false}" == "true" ]] || return 0
|
||||||
|
ssr::warn "Disabling Gatekeeper (SSR_DISABLE_GATEKEEPER=true)"
|
||||||
|
sudo spctl --global-disable
|
||||||
|
}
|
||||||
|
|
||||||
|
ssr::macos::open_installed_casks() {
|
||||||
|
[[ "${SSR_OPEN_CASKS:-false}" == "true" ]] || return 0
|
||||||
|
local cask_dir
|
||||||
|
cask_dir="$(brew --caskroom 2>/dev/null)" || return 0
|
||||||
|
[[ -d "$cask_dir" ]] || return 0
|
||||||
|
ssr::log "Opening installed cask apps for first-run config"
|
||||||
|
find "$cask_dir" -maxdepth 3 -iname '*.app' -print0 \
|
||||||
|
| while IFS= read -r -d '' app; do
|
||||||
|
open "$app" || true
|
||||||
|
done
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user