-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmac-install.sh
More file actions
executable file
·341 lines (304 loc) · 11.9 KB
/
Copy pathmac-install.sh
File metadata and controls
executable file
·341 lines (304 loc) · 11.9 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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
#!/usr/bin/env bash
# Bootstrap a fresh Mac from this dotfiles repo.
#
# Design notes (same conventions as pkg-install.sh):
# - No `set -e`. Every step is wrapped so one failure doesn't abort the rest —
# a half-installed machine you can inspect beats a script that died on step 3.
# - Idempotent: safe to re-run. Every step checks for "already done" first.
# - Nothing that needs a secret is automated. Those are printed at the end.
#
# Usage:
# ./mac-install.sh # do it
# ./mac-install.sh --dry-run # print every command, change nothing
# ./mac-install.sh --skip-brew # Homebrew already set up
# ./mac-install.sh --skip-casks # skip GUI apps and fonts (the slow half)
set -u
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DRY_RUN=0
SKIP_BREW=0
SKIP_CASKS=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--skip-brew) SKIP_BREW=1 ;;
--skip-casks) SKIP_CASKS=1 ;;
--help|-h) sed -n '2,15p' "$0"; exit 0 ;;
*) echo "unknown flag: $arg (try --help)" >&2; exit 2 ;;
esac
done
if [ "$(uname -s)" != "Darwin" ]; then
echo "ERROR: this script targets macOS. On Debian/Ubuntu use ./pkg-install.sh." >&2
exit 1
fi
ok=()
failed=()
skipped=()
# run <label> <command...> — the try/except wrapper everything goes through.
run() {
local label="$1"; shift
if [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] $label: $*"
ok+=("$label")
return 0
fi
local out
if out=$("$@" 2>&1); then
echo " [ ok ] $label"
ok+=("$label")
else
echo " [fail] $label"
sed 's/^/ /' <<<"$out" >&2
failed+=("$label")
fi
}
# run_tty <label> <command...> — same bookkeeping as run(), but the command
# inherits the terminal instead of having its output captured. Use this for
# anything slow or interactive: a captured command that asks for a password
# shows no prompt and no progress, so the script looks hung when it is simply
# waiting on you. Homebrew casks need sudo, so this matters.
run_tty() {
local label="$1"; shift
if [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] $label: $*"
ok+=("$label")
return 0
fi
echo " --- $label: live output below ---"
if "$@"; then
echo " [ ok ] $label"
ok+=("$label")
else
echo " [fail] $label"
failed+=("$label")
fi
}
have() { command -v "$1" >/dev/null 2>&1; }
note_skip() { echo " [have] $1"; skipped+=("$1"); }
# ---- 1. Xcode command line tools --------------------------------------------
echo "==> Xcode command line tools"
if xcode-select -p >/dev/null 2>&1; then
note_skip "xcode-clt"
elif [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] xcode-select --install"
else
# This opens a GUI dialog and returns immediately, so we poll rather than
# trusting the exit code. Bounded: if the user dismisses the dialog the loop
# would otherwise hang forever with no output.
xcode-select --install >/dev/null 2>&1
echo " Accept the Xcode command line tools dialog. Waiting up to 30 min..."
waited=0
until xcode-select -p >/dev/null 2>&1 || [ "$waited" -ge 1800 ]; do
sleep 10
waited=$((waited + 10))
[ $((waited % 120)) -eq 0 ] && echo " still waiting (${waited}s)..."
done
if xcode-select -p >/dev/null 2>&1; then
echo " [ ok ] xcode-clt"; ok+=("xcode-clt")
else
echo " [fail] xcode-clt — dialog not completed. Run 'xcode-select --install'" >&2
echo " by hand, then re-run this script." >&2
failed+=("xcode-clt")
fi
fi
# ---- 2. Homebrew -------------------------------------------------------------
echo
echo "==> Homebrew"
# Apple silicon installs to /opt/homebrew, Intel to /usr/local.
if [ -x /opt/homebrew/bin/brew ]; then
BREW=/opt/homebrew/bin/brew
elif [ -x /usr/local/bin/brew ]; then
BREW=/usr/local/bin/brew
else
BREW=""
fi
if [ "$SKIP_BREW" -eq 1 ]; then
note_skip "homebrew (--skip-brew)"
elif [ -n "$BREW" ]; then
note_skip "homebrew"
elif [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] install homebrew via the official script"
BREW=/opt/homebrew/bin/brew
else
# The official installer is interactive: it prints what it will do, waits for
# RETURN, then asks for your password. Say so, because an unexpected prompt
# scrolled off the top reads as a hang.
echo " The Homebrew installer will ask you to press RETURN, then for your password."
if /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; then
BREW=$([ -x /opt/homebrew/bin/brew ] && echo /opt/homebrew/bin/brew || echo /usr/local/bin/brew)
echo " [ ok ] homebrew"
ok+=("homebrew")
else
echo " [fail] homebrew — nothing after this will work" >&2
failed+=("homebrew")
fi
fi
if [ -n "$BREW" ] && [ "$DRY_RUN" -eq 0 ]; then
eval "$("$BREW" shellenv)"
fi
# ---- 3. Brewfile -------------------------------------------------------------
echo
echo "==> Brewfile"
if [ -z "$BREW" ]; then
echo " [skip] no brew on PATH"
skipped+=("brewfile")
else
# Formulae first: fast, no sudo, and everything the shell actually needs.
# HOMEBREW_NO_AUTO_UPDATE stops brew re-checking for updates before each
# install. --verbose names each package as it starts, so a stall is
# attributable to a specific entry instead of looking like a dead terminal.
export HOMEBREW_NO_AUTO_UPDATE=1
run_tty "brewfile" "$BREW" bundle --verbose --file="$DOTFILES_DIR/Brewfile"
# Casks second, and separately: hundreds of MB each, privileged installers,
# password prompts. A stall here no longer costs you the toolchain above.
if [ "$SKIP_CASKS" -eq 1 ]; then
echo " [skip] casks (--skip-casks)"
skipped+=("casks")
else
echo
echo " Casks next: GUI apps and fonts, ~2GB. Some will ask for your password."
echo " Ctrl-C is safe here — rerun with --skip-casks to get everything else,"
echo " then: brew bundle --verbose --file=~/dotfiles/Brewfile.casks"
run_tty "casks" "$BREW" bundle --verbose --file="$DOTFILES_DIR/Brewfile.casks"
fi
fi
# ---- 4. Stow the configs, oh-my-zsh and its plugins --------------------------
# install.sh owns all three: it stows first, then installs oh-my-zsh (so the
# installer finds ~/.zshrc already symlinked and leaves it alone), then clones
# the custom plugins. Don't duplicate that sequence here.
echo
echo "==> Stow dotfiles + oh-my-zsh"
# ~/.ssh must be 700 before anything lands in it, and stow won't set that.
[ "$DRY_RUN" -eq 0 ] && { mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"; }
run_tty "stow" "$DOTFILES_DIR/install.sh"
# .wezterm.lua sits at the repo root rather than in a stow package (the WSL
# wezsync helper reads it from there), so link it by hand. On macOS WezTerm
# reads ~/.wezterm.lua directly — no Windows-side copy needed.
if [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] ln -s $DOTFILES_DIR/.wezterm.lua ~/.wezterm.lua"
elif [ -L "$HOME/.wezterm.lua" ]; then
note_skip "wezterm-config"
else
[ -e "$HOME/.wezterm.lua" ] && mv "$HOME/.wezterm.lua" "$HOME/.wezterm.lua.bak"
run "wezterm-config" ln -s "$DOTFILES_DIR/.wezterm.lua" "$HOME/.wezterm.lua"
fi
# Hand-written skills live once in ~/.agents/skills (the agents package) and are
# surfaced to Claude Code by symlink from ~/.claude/skills. Stow only restores
# the store, not those links, so recreate any that are missing.
if [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] link ~/.agents/skills/* into ~/.claude/skills/"
elif [ -d "$HOME/.agents/skills" ]; then
mkdir -p "$HOME/.claude/skills"
linked=0
for s in "$HOME"/.agents/skills/*/; do
n="$(basename "$s")"
if [ ! -e "$HOME/.claude/skills/$n" ]; then
ln -s "../../.agents/skills/$n" "$HOME/.claude/skills/$n" && linked=$((linked + 1))
fi
done
echo " [ ok ] agent-skill-links ($linked new)"
ok+=("agent-skill-links")
fi
# ---- 5. Installers that aren't in any package manager ------------------------
echo
echo "==> curl installers"
curl_install() {
local name="$1" probe="$2" url="$3" shell="${4:-bash}"
if have "$probe" || [ -d "$HOME/.$name" ]; then
note_skip "$name"
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] curl -fsSL $url | $shell"
ok+=("$name")
return 0
fi
local out
if out=$(curl -fsSL "$url" | "$shell" 2>&1); then
echo " [ ok ] $name"; ok+=("$name")
else
echo " [fail] $name"; sed 's/^/ /' <<<"$out" >&2; failed+=("$name")
fi
}
curl_install bun bun https://bun.com/install
curl_install claude claude https://claude.ai/install.sh
# ~/.composio is provisioned by the Composio MCP integration inside Claude Code
# (`claude mcp add --transport http composio https://connect.composio.dev/mcp`),
# not by a standalone installer — nothing to run here.
# Supabase CLI has a real tap on macOS — no curl-piping needed.
if [ -n "$BREW" ] && ! have supabase; then
run_tty "supabase" "$BREW" install supabase/tap/supabase
else
have supabase && note_skip "supabase"
fi
# ---- 6. Language-manager packages --------------------------------------------
echo
echo "==> npm / pipx / uv / nvm packages"
if have npm; then run_tty "npm-globals" npm install -g docx pnpm; else skipped+=("npm-globals"); fi
if have pipx; then run "pipx-graphifyy" pipx install graphifyy; else skipped+=("pipx-graphifyy"); fi
if have uv; then run "uv-nano-pdf" uv tool install nano-pdf; else skipped+=("uv-nano-pdf"); fi
# nvm is a shell function, not a binary — source it before use.
NVM_SH="$(brew --prefix nvm 2>/dev/null)/nvm.sh"
if [ -s "$NVM_SH" ]; then
if [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] nvm install 24.14.0"
else
# shellcheck disable=SC1090
if . "$NVM_SH" && nvm install 24.14.0 >/dev/null 2>&1; then
echo " [ ok ] nvm-node-24"; ok+=("nvm-node-24")
else
echo " [fail] nvm-node-24"; failed+=("nvm-node-24")
fi
fi
else
echo " [skip] nvm (not installed)"; skipped+=("nvm-node-24")
fi
# ---- 7. Default shell --------------------------------------------------------
echo
echo "==> Default shell"
BREW_PREFIX="$(brew --prefix 2>/dev/null)"
BREW_ZSH="${BREW_PREFIX:-/nonexistent}/bin/zsh"
if [ -z "$BREW_PREFIX" ] || [ ! -x "$BREW_ZSH" ]; then
echo " [skip] brew zsh not installed; keeping the system zsh"
skipped+=("chsh")
elif [ "$SHELL" = "$BREW_ZSH" ]; then
note_skip "chsh"
elif [ "$DRY_RUN" -eq 1 ]; then
echo " [dry ] add $BREW_ZSH to /etc/shells and chsh"
else
grep -qxF "$BREW_ZSH" /etc/shells 2>/dev/null || \
echo "$BREW_ZSH" | sudo tee -a /etc/shells >/dev/null
# NOT wrapped in run(): chsh prompts for the login password, and run()
# swallows stdout/stderr into a variable, so the prompt would be invisible
# and the script would look hung.
echo " chsh will ask for your login password:"
if chsh -s "$BREW_ZSH"; then
echo " [ ok ] chsh"; ok+=("chsh")
else
echo " [fail] chsh"; failed+=("chsh")
fi
fi
# ---- Summary -----------------------------------------------------------------
echo
echo "================ summary ================"
echo " done: ${#ok[@]}"
echo " already: ${#skipped[@]}"
echo " failed: ${#failed[@]}"
[ "${#failed[@]}" -gt 0 ] && printf ' %s\n' "${failed[@]}"
echo "========================================="
cat <<'MANUAL'
Still to do by hand — these need secrets and cannot be scripted:
1. SSH keys. Copy github_personal, github_work, id_ed25519 (and any .pem)
into ~/.ssh out of band, then:
chmod 700 ~/.ssh && chmod 600 ~/.ssh/github_* ~/.ssh/id_ed25519
~/.ssh/config is stowed from this repo and pulls in ~/.ssh/config.local
for the private hosts — recreate that file, it is deliberately untracked.
2. ~/.zsh_secrets (chmod 600). Sourced by .zshrc. Needs:
export NGROK_AUTHTOKEN="..."
export CLAUDE_CODE_OAUTH_TOKEN="..."
3. gh auth login — once per account (AnasIsmai1, AnasSledge).
~/.config/gh/hosts.yml is gitignored because it stores live OAuth tokens.
4. Open tmux and press prefix + I to let tpm fetch the plugins.
5. Open nvim once and let lazy.nvim restore from lazy-lock.json.
6. macOS fixups in .zshrc / .wezterm.lua — see MAC-MIGRATION.md section 9.
MANUAL
[ "${#failed[@]}" -eq 0 ]