-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall
More file actions
executable file
·351 lines (305 loc) · 16.4 KB
/
install
File metadata and controls
executable file
·351 lines (305 loc) · 16.4 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
342
343
344
345
346
347
348
349
350
351
#!/usr/bin/env bash
set -euo pipefail
#
# Cross-platform development environment bootstrap.
# Supports: macOS (desktop & headless), Ubuntu/Debian (server & desktop).
#
# macOS: ./install <user> <email>
# Linux server: run ./bootstrap from your local machine instead (sets up SSH key auth first)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/platform.sh"
if [[ $# -lt 2 ]]; then
echo "Usage: ./install <user> <email> [hostname]"
exit 1
fi
USERNAME="$1"
EMAIL="$2"
HOSTNAME_ARG="${3:-}" # optional; Linux only — ignored on macOS
OS="$(detect_os)"
GUI=$(has_gui && echo "yes" || echo "no")
log_info "Platform: $OS | GUI: $GUI"
if [[ "$OS" == "unsupported" ]]; then
log_error "Unsupported operating system: $(uname -s)"
exit 1
fi
# ── Linux root bootstrap: create user, then re-exec as that user ─────────────
# Fallback for running install directly as root (normal path uses ./bootstrap
# which sets up the user and SSHes in as them before calling install).
if is_linux && [[ "$(id -u)" -eq 0 ]] && [[ "$USERNAME" != "root" ]]; then
log_info "Running as root — setting up user '$USERNAME'..."
if ! id "$USERNAME" &>/dev/null; then
useradd -m -s /bin/bash "$USERNAME"
log_info "Created user $USERNAME"
fi
# Grant passwordless sudo access (required for non-interactive installs)
usermod -aG sudo "$USERNAME"
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/$USERNAME"
chmod 440 "/etc/sudoers.d/$USERNAME"
log_info "$USERNAME added to sudo group (NOPASSWD)"
# Always copy the fresh clone into user's home (overwrites any stale previous copy)
TARGET_DIR="/home/$USERNAME/Workspace/setup"
if [[ "$SCRIPT_DIR" != "$TARGET_DIR" ]]; then
mkdir -p "/home/$USERNAME/Workspace"
rm -rf "$TARGET_DIR"
cp -r "$SCRIPT_DIR" "$TARGET_DIR"
chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/Workspace"
fi
# Set hostname if provided (do this as root before switching users)
if [[ -n "$HOSTNAME_ARG" ]]; then
hostnamectl set-hostname "$HOSTNAME_ARG"
# Update /etc/hosts so the new hostname resolves locally
if grep -q "127.0.1.1" /etc/hosts; then
sed_inplace "s/^127\.0\.1\.1.*/127.0.1.1\t$HOSTNAME_ARG/" /etc/hosts
else
echo "127.0.1.1 $HOSTNAME_ARG" >> /etc/hosts
fi
log_info "Hostname set to $HOSTNAME_ARG"
fi
log_info "Switching to $USERNAME — re-running install..."
exec su -l "$USERNAME" -c "bash '$TARGET_DIR/install' '$USERNAME' '$EMAIL' '$HOSTNAME_ARG'"
fi
# ── SSH ─────────────────────────────────────────────────────────────────────
if ! [ -f ~/.ssh/id_rsa ]; then
log_info "Generating SSH key..."
mkdir -p ~/.ssh && chmod 700 ~/.ssh
ssh-keygen -t rsa -b 4096 -C "$EMAIL" -N "" -f ~/.ssh/id_rsa
fi
# ── Platform-specific packages ──────────────────────────────────────────────
if is_macos; then
source "$SCRIPT_DIR/lib/macos.sh"
install_macos_prerequisites
install_macos_packages "$SCRIPT_DIR" "$GUI"
elif is_linux; then
source "$SCRIPT_DIR/lib/linux.sh"
install_linux_all "$GUI"
fi
# ── asdf plugins & runtimes ─────────────────────────────────────────────────
# (asdf is installed via Homebrew on macOS, via git clone on Linux)
log_info "Setting up asdf plugins..."
# Ensure asdf is on PATH
if is_macos && command -v brew &>/dev/null; then
source "$(brew --prefix asdf 2>/dev/null)/libexec/asdf.sh" 2>/dev/null || true
elif [[ -f "$HOME/.asdf/asdf.sh" ]]; then
export PATH="$HOME/.asdf/bin:$PATH"
source "$HOME/.asdf/asdf.sh"
fi
if command -v asdf &>/dev/null; then
# add fails when the plugin already exists — suppress that and fall back to update.
# If update also fails (e.g. network down but plugin is already usable), warn only.
asdf_plugin_add() {
asdf plugin add "$1" 2>/dev/null || asdf plugin update "$1" || log_warn "asdf: could not add/update plugin: $1"
}
for plugin in golang java erlang elixir ruby python rebar nodejs rust; do
asdf_plugin_add "$plugin"
done
# asdf 0.16+ uses `asdf set -u`; older bash versions use `asdf global`
asdf_global() {
asdf set -u "$1" "$2" 2>/dev/null || asdf global "$1" "$2"
}
log_info "Installing default runtimes..."
asdf install nodejs 22.22.1
asdf_global nodejs 22.22.1
npm config set prefix ~/.npm-global
# On Linux, symlink node into /usr/local/bin so non-login processes (e.g.
# vim/coc.nvim, systemd services) can find it without needing asdf shims
# on $PATH. This is safe to repeat — -sf overwrites stale links.
if is_linux; then
node_bin="$(asdf which node 2>/dev/null)" || true
npm_bin="$(asdf which npm 2>/dev/null)" || true
if [[ -n "$node_bin" ]]; then
_sudo ln -sf "$node_bin" /usr/local/bin/node
fi
if [[ -n "$npm_bin" ]]; then
_sudo ln -sf "$npm_bin" /usr/local/bin/npm
fi
fi
asdf install golang 1.23.4
asdf_global golang 1.23.4
asdf install java openjdk-23.0.1
asdf_global java openjdk-23.0.1
# Enable gobin helper
if [[ -f ~/.asdf/plugins/golang/set-env.zsh ]]; then
cat "$SCRIPT_DIR/golang/set-env.zsh" > ~/.asdf/plugins/golang/set-env.zsh
fi
else
log_warn "asdf not found; skipping runtime installation"
fi
# ── Oh My Zsh ───────────────────────────────────────────────────────────────
log_info "Setting up Oh My Zsh..."
if ! command -v zsh &>/dev/null; then
log_warn "zsh not installed — skipping Oh My Zsh setup"
elif ! [ -d ~/.oh-my-zsh ]; then
curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh \
| sed 's/exec zsh -l//g' \
| sh
else
git -C ~/.oh-my-zsh pull || log_warn "Could not update Oh My Zsh (network?)"
fi
# ── Powerlevel10k ───────────────────────────────────────────────────────────
log_info "Setting up Powerlevel10k..."
if ! [ -d ~/powerlevel10k ]; then
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
git clone --depth=1 https://github.com/powerline/fonts.git ~/powerlevel10k/fonts
# Only install fonts if there's a GUI to display them
if [[ "$GUI" == "yes" ]]; then
~/powerlevel10k/fonts/install.sh || log_warn "Powerlevel10k fonts install failed"
fi
else
git -C ~/powerlevel10k pull || log_warn "Could not update Powerlevel10k (network?)"
fi
# ── Vim (maximum-awesome) ──────────────────────────────────────────────────
log_info "Setting up Vim..."
if ! [ -d ~/maximum-awesome ]; then
git clone https://github.com/yangou/maximum-awesome.git ~/maximum-awesome
fi
cd ~/maximum-awesome
git pull || log_warn "Could not update maximum-awesome (network?)"
sed_inplace 's/File\.exists?/File.exist?/g' Rakefile 2>/dev/null || true
if is_macos && command -v rake &>/dev/null; then
# rake sets up symlinks and installs deps via brew — macOS only
rake 2>/dev/null || log_warn "rake failed - you may need to run it manually"
elif is_linux; then
# On Linux, rake calls brew and aborts — create symlinks manually instead
ln -sf ~/maximum-awesome/vimrc ~/.vimrc
ln -sf ~/maximum-awesome/vimrc.bundles ~/.vimrc.bundles
mkdir -p ~/.vim
if ! [ -d ~/.vim/bundle/Vundle.vim ]; then
git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim
fi
# Set up whitespace plugin that rake normally handles on macOS
mkdir -p ~/.vim/plugin/whitespace
ln -sf ~/maximum-awesome/vim/plugin/whitespace/whitespace.vim \
~/.vim/plugin/whitespace/whitespace.vim
fi
cd "$SCRIPT_DIR"
ensure_line "source $SCRIPT_DIR/vim/.vimrc.bundles.local" ~/.vimrc.bundles.local
ensure_line "source $SCRIPT_DIR/vim/.vimrc.local" ~/.vimrc.local
log_info "Installing vim plugins (this may take a few minutes)..."
vim -es -u ~/.vimrc +PluginInstall +qall 2>/dev/null || true
log_info "Installing Go binaries for vim-go (this may take a few minutes)..."
vim -es -u ~/.vimrc +GoInstallBinaries +qall 2>/dev/null || true
build_coc_nvim() {
local dir="$1" flavor="$2"
[ -d "$dir" ] || return 0
cd "$dir"
export PATH="$HOME/.asdf/shims:$HOME/.asdf/bin:$PATH"
npm ci || log_warn "coc.nvim ($flavor): npm ci failed"
npm run build || log_warn "coc.nvim ($flavor): build failed"
npm audit fix 2>/dev/null || true
cd "$SCRIPT_DIR"
}
build_coc_nvim ~/.vim/bundle/coc.nvim vim
# ── Neovim ──────────────────────────────────────────────────────────────────
log_info "Setting up Neovim..."
mkdir -p ~/.config
if ! [ -e ~/.config/nvim ]; then
ln -s "$SCRIPT_DIR/nvim" ~/.config/nvim
fi
nvim --headless -c "Lazy! install" -c "qa" 2>/dev/null || true
nvim --headless -c "GoInstallBinaries" -c "qa" 2>/dev/null || true
build_coc_nvim ~/.local/share/nvim/lazy/coc.nvim nvim
# ── Claude Code (Linux: install via npm; macOS: installed via Brewfile cask) ──
if is_linux && ! command -v claude &>/dev/null; then
log_info "Installing Claude Code..."
export PATH="$HOME/.asdf/shims:$HOME/.asdf/bin:$PATH"
npm install -g @anthropic-ai/claude-code
# npm installs to ~/.npm-global/bin (set earlier via `npm config set prefix`)
# must update PATH now so the plugin section below can find `claude`
export PATH="$HOME/.npm-global/bin:$PATH"
fi
# ── Claude Code plugins ────────────────────────────────────────────────────
# Marketplace add is idempotent; plugin install can fail if a plugin is
# renamed/removed upstream — warn and continue rather than abort.
if command -v claude &>/dev/null; then
log_info "Setting up Claude Code plugins..."
claude plugin marketplace add affaan-m/everything-claude-code || log_warn "marketplace add failed: everything-claude-code"
claude plugin marketplace add obra/superpowers-marketplace || log_warn "marketplace add failed: superpowers-marketplace"
claude plugin marketplace add jarrodwatts/claude-hud || log_warn "marketplace add failed: claude-hud"
claude plugin marketplace add anthropics/claude-code || log_warn "marketplace add failed: claude-code-plugins"
claude plugin marketplace add anthropics/claude-plugins-official || log_warn "marketplace add failed: claude-plugins-official"
claude plugin marketplace add mixedbread-ai/mgrep || log_warn "marketplace add failed: mgrep"
claude plugin install everything-claude-code@everything-claude-code || log_warn "plugin install failed: everything-claude-code"
claude plugin install superpowers@superpowers-marketplace || log_warn "plugin install failed: superpowers"
claude plugin install claude-hud@claude-hud || log_warn "plugin install failed: claude-hud"
# claude-code-plugins (anthropics/claude-code marketplace)
claude plugin install ralph-wiggum@claude-code-plugins || log_warn "plugin install failed: ralph-wiggum"
claude plugin install frontend-design@claude-code-plugins || log_warn "plugin install failed: frontend-design"
claude plugin install commit-commands@claude-code-plugins || log_warn "plugin install failed: commit-commands"
claude plugin install security-guidance@claude-code-plugins || log_warn "plugin install failed: security-guidance"
claude plugin install pr-review-toolkit@claude-code-plugins || log_warn "plugin install failed: pr-review-toolkit"
claude plugin install feature-dev@claude-code-plugins || log_warn "plugin install failed: feature-dev"
claude plugin install explanatory-output-style@claude-code-plugins || log_warn "plugin install failed: explanatory-output-style"
claude plugin install code-review@claude-code-plugins || log_warn "plugin install failed: code-review"
# claude-plugins-official (anthropics/claude-plugins-official marketplace)
claude plugin install typescript-lsp@claude-plugins-official || log_warn "plugin install failed: typescript-lsp"
claude plugin install hookify@claude-plugins-official || log_warn "plugin install failed: hookify"
claude plugin install code-simplifier@claude-plugins-official || log_warn "plugin install failed: code-simplifier"
claude plugin install context7@claude-plugins-official || log_warn "plugin install failed: context7"
claude plugin install pyright-lsp@claude-plugins-official || log_warn "plugin install failed: pyright-lsp"
# mgrep (mixedbread-ai/mgrep marketplace)
claude plugin install mgrep@Mixedbread-Grep || log_warn "plugin install failed: mgrep"
claude plugin disable mgrep
else
log_warn "claude not found; skipping Claude Code plugin setup"
fi
# ── Everything Claude Code rules ────────────────────────────────────────────
# everything-claude-code lives as a git submodule at $SCRIPT_DIR/claude/everything-claude-code
git -C "$SCRIPT_DIR" submodule update --init --remote claude/everything-claude-code \
|| log_warn "Could not update everything-claude-code submodule (network?)"
if [ -f "$SCRIPT_DIR/claude/everything-claude-code/install.sh" ]; then
cd "$SCRIPT_DIR/claude/everything-claude-code"
export PATH="$HOME/.asdf/shims:$HOME/.asdf/bin:$PATH"
# Install the exact Node version it requires (from .tool-versions), if not already present
if [ -f .tool-versions ]; then
ecc_node="$(grep '^nodejs' .tool-versions | awk '{print $2}')"
if [[ -n "$ecc_node" ]]; then
asdf install nodejs "$ecc_node" || log_warn "Could not install node $ecc_node for ecc"
asdf local nodejs "$ecc_node" || log_warn "Could not set local node $ecc_node for ecc"
fi
fi
npm install || log_warn "everything-claude-code: npm install failed"
./install.sh typescript golang python swift php cpp csharp java kotlin perl rust
cd "$SCRIPT_DIR"
fi
# ── Tmux ────────────────────────────────────────────────────────────────────
log_info "Setting up Tmux..."
if ! [ -d ~/.tmux/plugins/tpm ]; then
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
else
git -C ~/.tmux/plugins/tpm pull || log_warn "Could not update tpm (network?)"
fi
ensure_line "source $SCRIPT_DIR/tmux/.tmux.conf.local" ~/.tmux.conf.local
# ── Git ─────────────────────────────────────────────────────────────────────
log_info "Configuring Git..."
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.cp cherry-pick
git config --global alias.cm commit
git config --global alias.df diff
git config --global alias.st status
git config --global user.name "$USERNAME"
git config --global user.email "$EMAIL"
# ── Shell config ────────────────────────────────────────────────────────────
log_info "Configuring shell..."
ensure_line "source $SCRIPT_DIR/zsh/.zprofile" ~/.zprofile
ensure_line "source $SCRIPT_DIR/zsh/.zshrc" ~/.zshrc
# p10k config — symlink from home to repo so both machines share one source of truth
ln -sf "$SCRIPT_DIR/zsh/.p10k.zsh" ~/.p10k.zsh
chmod 644 "$SCRIPT_DIR/zsh/.p10k.zsh"
# Set zsh as default shell if it isn't already.
# On Linux: use `usermod -s` via sudo — `chsh` requires an interactive password
# prompt and silently fails in non-interactive SSH sessions.
if [[ "$(basename "$SHELL")" != "zsh" ]]; then
local_zsh="$(which zsh 2>/dev/null)"
if [[ -z "$local_zsh" ]]; then
log_warn "zsh not found in PATH — skipping default shell change (was zsh installed?)"
else
log_info "Setting zsh as default shell..."
if is_linux; then
_sudo usermod -s "$local_zsh" "$USER"
else
chsh -s "$local_zsh"
fi
fi
fi
log_info "Setup complete! Restart your shell or run: exec zsh -l"