-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync.sh
More file actions
executable file
·277 lines (231 loc) · 10.8 KB
/
sync.sh
File metadata and controls
executable file
·277 lines (231 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/usr/bin/env bash
# sync.sh — Capture current machine state and update dotfiles repo.
#
# Run manually or via launchd (daily). Updates repo files from the
# current machine state but does NOT commit — review the diff first.
#
# Usage:
# ./sync.sh # Interactive — shows diff at the end
# ./sync.sh --quiet # Scripted/scheduled — only outputs if changes found
set -euo pipefail
DOTFILES="$(cd "$(dirname "$0")" && pwd)"
source "$DOTFILES/scripts/utils.sh"
QUIET=false
[[ "${1:-}" == "--quiet" ]] && QUIET=true
# =============================================================================
# Brewfile
# =============================================================================
info "Capturing Homebrew packages..."
brew bundle dump --file="$DOTFILES/Brewfile.new" --force
# Preserve the curated comments and organization from the existing Brewfile
# by only flagging if there are differences in the package list
if ! diff -q <(grep -v '^#' "$DOTFILES/Brewfile" | grep -v '^$' | sort) \
<(grep -v '^#' "$DOTFILES/Brewfile.new" | grep -v '^$' | sort) &>/dev/null; then
info "Brewfile has changed — review Brewfile.new and merge manually"
info " diff Brewfile Brewfile.new"
else
rm "$DOTFILES/Brewfile.new"
$QUIET || success "Brewfile up to date"
fi
# =============================================================================
# Dock layout
# =============================================================================
info "Capturing Dock layout..."
# Extract current Dock apps
current_apps=()
while IFS= read -r app; do
current_apps+=("$app")
done < <(defaults read com.apple.dock persistent-apps | grep '"_CFURLString"' | sed 's/.*"\(.*\.app\)\/\{0,1\}".*/\1/' | sed 's|^file://||' | python3 -c "import sys, urllib.parse; [print(urllib.parse.unquote(l.strip())) for l in sys.stdin]")
# Build the dock_apps array for the script
dock_block="dock_apps=("
for app in "${current_apps[@]}"; do
dock_block+=$'\n'" \"$app\""
done
dock_block+=$'\n'")"
# Check if the Dock layout has changed by comparing app lists
script_apps=$(grep '"/.*\.app"' "$DOTFILES/scripts/05-macos-defaults.sh" | sed 's/.*"\(\/.*\.app\)".*/\1/' | sort)
current_sorted=$(printf '%s\n' "${current_apps[@]}" | sort)
if [[ "$script_apps" != "$current_sorted" ]]; then
info "Dock layout has changed"
$QUIET || {
info "Current Dock apps:"
printf ' %s\n' "${current_apps[@]}"
info "Update scripts/05-macos-defaults.sh manually with the new layout"
}
else
$QUIET || success "Dock layout up to date"
fi
# =============================================================================
# macOS defaults — spot-check key settings
# =============================================================================
info "Checking macOS preferences..."
changes_found=false
check_default() {
local domain="$1" key="$2" expected="$3" label="$4"
local actual
actual=$(defaults read "$domain" "$key" 2>/dev/null || echo "__unset__")
# Normalize boolean representations (true/1, false/0)
local norm_actual="$actual" norm_expected="$expected"
[[ "$norm_actual" == "true" ]] && norm_actual="1"
[[ "$norm_actual" == "false" ]] && norm_actual="0"
[[ "$norm_expected" == "true" ]] && norm_expected="1"
[[ "$norm_expected" == "false" ]] && norm_expected="0"
if [[ "$norm_actual" != "$norm_expected" ]]; then
warn "$label: expected '$expected', got '$actual'"
changes_found=true
fi
}
check_default com.apple.dock tilesize "66" "Dock icon size"
check_default com.apple.dock autohide "1" "Dock autohide"
check_default com.apple.dock magnification "1" "Dock magnification"
check_default com.apple.dock largesize "89" "Dock magnified size"
check_default com.apple.dock orientation "bottom" "Dock position"
check_default com.apple.dock show-recents "0" "Dock show recents"
check_default com.apple.dock mru-spaces "0" "Spaces rearrange"
check_default com.apple.dock expose-group-apps "1" "Expose group apps"
check_default com.apple.finder FXPreferredViewStyle "Nlsv" "Finder view style"
check_default com.apple.finder _FXSortFoldersFirst "1" "Finder folders first"
check_default com.apple.finder AppleShowAllFiles "1" "Finder show hidden"
check_default NSGlobalDomain AppleAccentColor "6" "Accent color"
check_default com.apple.screencapture type "JPG" "Screenshot format"
check_default com.apple.AppleMultitouchTrackpad Clicking "1" "Tap to click"
check_default com.apple.AppleMultitouchTrackpad TrackpadThreeFingerDrag "1" "Three-finger drag"
if ! $changes_found; then
$QUIET || success "macOS preferences match"
fi
# =============================================================================
# VS Code extensions
# =============================================================================
info "Checking VS Code extensions..."
if command_exists code; then
installed=$(code --list-extensions | sort)
brewfile_exts=$(grep '^vscode ' "$DOTFILES/Brewfile" | sed 's/vscode "\(.*\)"/\1/' | sort)
new_exts=$(comm -23 <(echo "$installed") <(echo "$brewfile_exts"))
removed_exts=$(comm -13 <(echo "$installed") <(echo "$brewfile_exts"))
if [[ -n "$new_exts" ]]; then
info "Extensions installed but not in Brewfile:"
echo "$new_exts" | while read -r ext; do
info " + $ext"
done
fi
if [[ -n "$removed_exts" ]]; then
info "Extensions in Brewfile but not installed:"
echo "$removed_exts" | while read -r ext; do
info " - $ext"
done
fi
if [[ -z "$new_exts" ]] && [[ -z "$removed_exts" ]]; then
$QUIET || success "VS Code extensions match"
fi
else
$QUIET || warn "VS Code CLI (code) not found — skipping extension check"
fi
# =============================================================================
# Zen Browser
# =============================================================================
ZEN_DIR="$DOTFILES/Zen"
ZEN_PROFILES="$HOME/Library/Application Support/zen/Profiles"
ZEN_USERJS="$ZEN_DIR/user.js"
ZEN_EXTRA_FILES=(containers.json zen-themes.json zen-keyboard-shortcuts.json)
if [[ -d "$ZEN_DIR" && -d "$ZEN_PROFILES" ]]; then
info "Checking Zen Browser preferences..."
# Find the active profile (most recently modified prefs.js)
zen_profile=""
for profile in "$ZEN_PROFILES"/*/; do
if [[ -f "$profile/prefs.js" ]]; then
zen_profile="$profile"
fi
done
if [[ -n "$zen_profile" ]]; then
zen_drift=false
# Check user.js prefs against live prefs.js
if [[ -f "$ZEN_USERJS" ]]; then
zen_keys=$(grep '^user_pref(' "$ZEN_USERJS" | sed 's/user_pref("\([^"]*\)".*/\1/')
while IFS= read -r key; do
repo_val=$(grep "\"$key\"" "$ZEN_USERJS" | sed 's/.*,\s*//' | sed 's/);\s*$//')
live_val=$(grep "\"$key\"" "$zen_profile/prefs.js" | sed 's/.*,\s*//' | sed 's/);\s*$//')
if [[ -n "$live_val" && "$repo_val" != "$live_val" ]]; then
warn "Zen pref changed: $key"
warn " repo: $repo_val"
warn " live: $live_val"
zen_drift=true
changes_found=true
fi
done <<< "$zen_keys"
fi
# Check extra profile files for drift
for f in "${ZEN_EXTRA_FILES[@]}"; do
if [[ -f "$zen_profile/$f" && -f "$ZEN_DIR/$f" ]]; then
if ! diff -q "$ZEN_DIR/$f" "$zen_profile/$f" &>/dev/null; then
warn "Zen $f has changed — run: cp \"$zen_profile/$f\" \"$ZEN_DIR/$f\""
zen_drift=true
changes_found=true
fi
elif [[ -f "$zen_profile/$f" && ! -f "$ZEN_DIR/$f" ]]; then
warn "Zen $f exists in profile but not in repo"
zen_drift=true
changes_found=true
fi
done
if ! $zen_drift; then
$QUIET || success "Zen Browser preferences match"
fi
fi
fi
# =============================================================================
# Anki add-ons
# =============================================================================
ADDON_LIST="$DOTFILES/Anki/addons.txt"
ADDON_CONFIGS="$DOTFILES/Anki/addon-configs"
ADDON_DIR="$HOME/Library/Application Support/Anki2/addons21"
if [[ -d "$ADDON_DIR" && -f "$ADDON_LIST" ]]; then
info "Checking Anki add-ons..."
# Check for new or removed add-ons
repo_ids=$(grep -v '^#' "$ADDON_LIST" | sed 's/#.*//' | tr -d '[:space:]' | grep -v '^$' | sort)
installed_ids=$(find "$ADDON_DIR" -maxdepth 1 -type d ! -name addons21 ! -name __pycache__ -exec basename {} \; | sort)
new_addons=$(comm -13 <(echo "$repo_ids") <(echo "$installed_ids"))
removed_addons=$(comm -23 <(echo "$repo_ids") <(echo "$installed_ids"))
if [[ -n "$new_addons" ]]; then
info "Add-ons installed but not in addons.txt:"
echo "$new_addons" | while read -r id; do info " + $id"; done
changes_found=true
fi
if [[ -n "$removed_addons" ]]; then
info "Add-ons in addons.txt but not installed:"
echo "$removed_addons" | while read -r id; do info " - $id"; done
changes_found=true
fi
# Check for config changes (user config is in meta.json under "config" key)
for dir in "$ADDON_DIR"/*/; do
id="$(basename "$dir")"
[[ "$id" == "__pycache__" ]] && continue
[[ -f "$dir/meta.json" ]] || continue
has_config=$(python3 -c "import json; print('yes' if 'config' in json.load(open('$dir/meta.json')) else 'no')" 2>/dev/null)
[[ "$has_config" == "yes" ]] || continue
if [[ -f "$ADDON_CONFIGS/$id/meta.json" ]]; then
# Compare just the config key, not mod times etc.
live_config=$(python3 -c "import json; print(json.dumps(json.load(open('$dir/meta.json')).get('config',{}), sort_keys=True))" 2>/dev/null)
saved_config=$(python3 -c "import json; print(json.dumps(json.load(open('$ADDON_CONFIGS/$id/meta.json')).get('config',{}), sort_keys=True))" 2>/dev/null)
if [[ "$live_config" != "$saved_config" ]]; then
info "Anki add-on $id config has changed — run ./scripts/anki-sync.sh to update"
changes_found=true
fi
else
info "Anki add-on $id has config but not backed up"
changes_found=true
fi
done
if ! $changes_found; then
$QUIET || success "Anki add-ons match"
fi
fi
# =============================================================================
# Summary
# =============================================================================
echo ""
if [[ -f "$DOTFILES/Brewfile.new" ]] || $changes_found || [[ -n "${new_exts:-}" ]] || [[ -n "${removed_exts:-}" ]]; then
info "Changes detected — review above and update repo files as needed"
info "Then commit: cd $DOTFILES && git add -p && git commit"
else
$QUIET || success "Everything in sync!"
fi