-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.sh
More file actions
executable file
·386 lines (328 loc) · 14.1 KB
/
bootstrap.sh
File metadata and controls
executable file
·386 lines (328 loc) · 14.1 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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#!/bin/sh
# ============================================================================
# @file bootstrap.sh
# @description Universal dotfiles bootstrap script. Works with ANY shell
# (sh, bash, zsh, fish, nushell). Detects the current shell,
# creates appropriate symlinks, and sets up the environment.
#
# This is the TRUE entry point of the dotfiles system.
# Written in POSIX sh for maximum portability.
#
# @usage # First install:
# git clone https://github.com/ca971/dotfiles.git ~/dotfiles
# sh ~/dotfiles/bootstrap.sh
#
# # Or one-liner:
# curl -fsSL https://raw.githubusercontent.com/ca971/dotfiles/main/bootstrap.sh | sh
#
# @repository https://github.com/ca971/dotfiles.git
# @author ca971
# @license MIT
# @created 2025-07-15
# @version 1.0.0
# ============================================================================
set -eu
# ============================================================================
# Configuration
# ============================================================================
DOTFILES_DIR="${DOTFILES_DIR:-${HOME}/dotfiles}"
REPO_URL="https://github.com/ca971/dotfiles.git"
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-${HOME}/.config}"
# ── Colors (POSIX-safe) ─────────────────────────────────────────────────────
if [ -t 1 ]; then
RED="\033[0;31m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
BLUE="\033[0;34m"
BOLD="\033[1m"
DIM="\033[2m"
RESET="\033[0m"
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET=""
fi
_info() { printf "${BLUE}ℹ${RESET} %s\n" "$1"; }
_success() { printf "${GREEN}✓${RESET} %s\n" "$1"; }
_warn() { printf "${YELLOW}⚠${RESET} %s\n" "$1"; }
_error() { printf "${RED}✗${RESET} %s\n" "$1"; }
_step() { printf "\n${BOLD}━━━ %s ━━━${RESET}\n\n" "$1"; }
_has() { command -v "$1" > /dev/null 2>&1; }
# ============================================================================
# Detect Current Shell
# ============================================================================
detect_shell() {
# -- $SHELL is the LOGIN shell, not necessarily the current one
CURRENT_SHELL="$(basename "${SHELL:-sh}")"
# -- Try to detect more precisely
if [ -n "${ZSH_VERSION:-}" ]; then
CURRENT_SHELL="zsh"
elif [ -n "${BASH_VERSION:-}" ]; then
CURRENT_SHELL="bash"
elif [ -n "${FISH_VERSION:-}" ]; then
CURRENT_SHELL="fish"
elif [ -n "${NU_VERSION:-}" ]; then
CURRENT_SHELL="nu"
fi
_info "Detected shell: ${CURRENT_SHELL}"
}
# ============================================================================
# Detect Platform
# ============================================================================
detect_platform() {
PLATFORM="unknown"
DISTRO="unknown"
case "$(uname -s)" in
Darwin) PLATFORM="darwin" ;;
Linux)
if grep -qi 'microsoft\|wsl' /proc/version 2> /dev/null; then
PLATFORM="wsl"
else
PLATFORM="linux"
fi
if [ -f /etc/os-release ]; then
DISTRO=$(. /etc/os-release && echo "${ID:-unknown}")
fi
;;
esac
_info "Platform: ${PLATFORM} (${DISTRO})"
}
# ============================================================================
# Clone or Update Repository
# ============================================================================
setup_repo() {
_step "Repository"
if [ -d "${DOTFILES_DIR}/.git" ]; then
_info "Existing dotfiles found — updating..."
cd "$DOTFILES_DIR" && git pull --rebase --autostash 2> /dev/null
_success "Repository updated"
elif [ -d "$DOTFILES_DIR" ] && [ ! -d "${DOTFILES_DIR}/.git" ]; then
_warn "${DOTFILES_DIR} exists but is not a git repo"
_info "Skipping clone — using existing directory"
else
_info "Cloning dotfiles..."
if _has git; then
git clone --depth=1 "$REPO_URL" "$DOTFILES_DIR"
_success "Repository cloned"
else
_error "Git is required. Install git first."
exit 1
fi
fi
}
# ============================================================================
# Create Directory Structure
# ============================================================================
create_directories() {
_step "Directories"
for dir in \
"${XDG_CONFIG_HOME}" \
"${HOME}/.local/share" \
"${HOME}/.local/state" \
"${HOME}/.cache" \
"${HOME}/.local/bin" \
"${DOTFILES_DIR}/local" \
"${DOTFILES_DIR}/cache" \
"${DOTFILES_DIR}/generated" \
"${DOTFILES_DIR}/shells/zsh" \
"${DOTFILES_DIR}/shells/fish" \
"${DOTFILES_DIR}/shells/bash" \
"${DOTFILES_DIR}/shells/nushell"; do
mkdir -p "$dir" 2> /dev/null
done
_success "Directory structure created"
}
# ============================================================================
# Create Shell-Specific Symlinks
# ============================================================================
link_shell() {
_step "Shell Symlinks"
local backup_suffix
backup_suffix="bak.$(date +%Y%m%d_%H%M%S)"
# ── ZSH ────────────────────────────────────────────────────────────────
if _has zsh || [ "$CURRENT_SHELL" = "zsh" ]; then
# Backup existing
if [ -f "${HOME}/.zshenv" ] && [ ! -L "${HOME}/.zshenv" ]; then
mv "${HOME}/.zshenv" "${HOME}/.zshenv.${backup_suffix}"
_warn "Backed up existing ~/.zshenv"
fi
if [ -f "${DOTFILES_DIR}/shells/zsh/.zshenv" ]; then
ln -sf "${DOTFILES_DIR}/shells/zsh/.zshenv" "${HOME}/.zshenv"
_success "~/.zshenv → shells/zsh/.zshenv"
fi
fi
# ── Bash ───────────────────────────────────────────────────────────────
if _has bash || [ "$CURRENT_SHELL" = "bash" ]; then
if [ -f "${DOTFILES_DIR}/shells/bash/.bashrc" ]; then
if [ -f "${HOME}/.bashrc" ] && [ ! -L "${HOME}/.bashrc" ]; then
mv "${HOME}/.bashrc" "${HOME}/.bashrc.${backup_suffix}"
_warn "Backed up existing ~/.bashrc"
fi
ln -sf "${DOTFILES_DIR}/shells/bash/.bashrc" "${HOME}/.bashrc"
_success "~/.bashrc → shells/bash/.bashrc"
if [ -f "${DOTFILES_DIR}/shells/bash/.bash_profile" ]; then
if [ -f "${HOME}/.bash_profile" ] && [ ! -L "${HOME}/.bash_profile" ]; then
mv "${HOME}/.bash_profile" "${HOME}/.bash_profile.${backup_suffix}"
fi
ln -sf "${DOTFILES_DIR}/shells/bash/.bash_profile" "${HOME}/.bash_profile"
_success "~/.bash_profile → shells/bash/.bash_profile"
fi
else
_info "Bash config not yet created (shells/bash/.bashrc)"
fi
fi
# ── Fish ───────────────────────────────────────────────────────────────
if _has fish || [ "$CURRENT_SHELL" = "fish" ]; then
local fish_config_dir="${XDG_CONFIG_HOME}/fish"
mkdir -p "$fish_config_dir"
if [ -f "${DOTFILES_DIR}/shells/fish/config.fish" ]; then
if [ -f "${fish_config_dir}/config.fish" ] && [ ! -L "${fish_config_dir}/config.fish" ]; then
mv "${fish_config_dir}/config.fish" "${fish_config_dir}/config.fish.${backup_suffix}"
_warn "Backed up existing fish config"
fi
ln -sf "${DOTFILES_DIR}/shells/fish/config.fish" "${fish_config_dir}/config.fish"
_success "fish/config.fish → shells/fish/config.fish"
else
_info "Fish config not yet created (shells/fish/config.fish)"
fi
fi
# ── Nushell ────────────────────────────────────────────────────────────
if _has nu || [ "$CURRENT_SHELL" = "nu" ]; then
local nu_config_dir="${XDG_CONFIG_HOME}/nushell"
mkdir -p "$nu_config_dir"
if [ -f "${DOTFILES_DIR}/shells/nushell/config.nu" ]; then
ln -sf "${DOTFILES_DIR}/shells/nushell/config.nu" "${nu_config_dir}/config.nu"
_success "nushell/config.nu → shells/nushell/config.nu"
fi
if [ -f "${DOTFILES_DIR}/shells/nushell/env.nu" ]; then
ln -sf "${DOTFILES_DIR}/shells/nushell/env.nu" "${nu_config_dir}/env.nu"
_success "nushell/env.nu → shells/nushell/env.nu"
fi
if [ ! -f "${DOTFILES_DIR}/shells/nushell/config.nu" ]; then
_info "Nushell config not yet created (shells/nushell/config.nu)"
fi
fi
# ── Starship (cross-shell prompt) ──────────────────────────────────────
if [ -f "${DOTFILES_DIR}/themes/starship.toml" ]; then
ln -sf "${DOTFILES_DIR}/themes/starship.toml" "${XDG_CONFIG_HOME}/starship.toml"
_success "starship.toml → themes/starship.toml"
fi
}
# ============================================================================
# Generate SSOT Files
# ============================================================================
generate_ssot() {
_step "SSOT Generation"
local gen_script="${DOTFILES_DIR}/ssot/generators/generate-all.sh"
if [ -f "$gen_script" ]; then
chmod +x "${DOTFILES_DIR}/ssot/generators/"*.sh 2> /dev/null
if _has bash; then
bash "$gen_script"
_success "SSOT files generated for all shells"
else
_warn "Bash required for SSOT generation"
fi
else
_warn "Generator script not found"
fi
}
# ============================================================================
# macOS Defaults (darwin only)
# ============================================================================
apply_macos_defaults() {
[ "$PLATFORM" = "darwin" ] || return 0
_step "macOS Preferences"
if [ ! -d "${DOTFILES_DIR}/platform/darwin-defaults/defaults.d" ]; then
_warn "darwin-defaults/ not found — skipping"
return 0
fi
printf " Apply macOS system preferences? [y/N] "
read -r apply_macos
if [ "$apply_macos" = "y" ] || [ "$apply_macos" = "Y" ]; then
if _has bash; then
# Use dot CLI which sources _core.sh for proper formatting
bash "${DOTFILES_DIR}/bin/dot" macos apply --force
else
_warn "Bash required for macOS defaults — run later: dot macos apply"
fi
else
_info "Skipped — run later with: ${GREEN}dot macos apply${RESET}"
fi
}
# ============================================================================
# Check Available Shells
# ============================================================================
check_shells() {
_step "Available Shells"
for shell_name in zsh bash fish nu; do
if _has "$shell_name"; then
local ver
ver=$("$shell_name" --version 2> /dev/null | head -1 || echo "")
_success "${shell_name} — ${ver}"
else
printf " ${DIM}○ ${shell_name} (not installed)${RESET}\n"
fi
done
echo ""
_info "Default shell: ${SHELL:-unknown}"
}
# ============================================================================
# Install Missing Shell (optional)
# ============================================================================
offer_shell_install() {
_step "Shell Setup"
local missing_shells=""
if ! _has zsh; then
missing_shells="${missing_shells} zsh"
fi
if [ -z "$missing_shells" ]; then
_success "ZSH is available"
return
fi
_warn "Missing shells:${missing_shells}"
_info "Install with your package manager:"
if _has brew; then
printf " brew install%s\n" "$missing_shells"
elif _has apt; then
printf " sudo apt install -y%s\n" "$missing_shells"
elif _has dnf; then
printf " sudo dnf install -y%s\n" "$missing_shells"
elif _has pacman; then
printf " sudo pacman -S%s\n" "$missing_shells"
fi
}
# ============================================================================
# Summary
# ============================================================================
summary() {
_step "Bootstrap Complete 🎉"
printf " ${BOLD}Dotfiles:${RESET} %s\n" "$DOTFILES_DIR"
printf " ${BOLD}Platform:${RESET} %s (%s)\n" "$PLATFORM" "$DISTRO"
printf " ${BOLD}Shell:${RESET} %s\n" "$CURRENT_SHELL"
printf "\n ${BOLD}Configured shells:${RESET}\n"
[ -L "${HOME}/.zshenv" ] && printf " ✅ zsh\n" || printf " ○ zsh\n"
[ -L "${HOME}/.bashrc" ] && printf " ✅ bash\n" || printf " ○ bash\n"
[ -L "${XDG_CONFIG_HOME}/fish/config.fish" ] 2> /dev/null && printf " ✅ fish\n" || printf " ○ fish\n"
[ -L "${XDG_CONFIG_HOME}/nushell/config.nu" ] 2> /dev/null && printf " ✅ nu\n" || printf " ○ nu\n"
printf "\n ${BOLD}Next steps:${RESET}\n"
printf " 1. Restart your shell: ${GREEN}exec \$SHELL${RESET}\n"
printf " 2. Run diagnostics: ${GREEN}just doctor${RESET}\n"
printf " 3. Check tools: ${GREEN}just tools${RESET}\n"
printf "\n"
}
# ============================================================================
# Main
# ============================================================================
main() {
printf "\n${BOLD} 🚀 Dotfiles Bootstrap — Cross-Platform, Cross-Shell${RESET}\n"
printf " ${DIM}%s${RESET}\n\n" "$REPO_URL"
detect_shell
detect_platform
setup_repo
create_directories
link_shell
generate_ssot
apply_macos_defaults
check_shells
offer_shell_install
summary
}
main "$@"