-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall
More file actions
241 lines (225 loc) · 11.6 KB
/
Copy pathinstall
File metadata and controls
241 lines (225 loc) · 11.6 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
#!/bin/zsh
#
# dotfiles installer | https://github.com/fernandoataoldotcom/github_dotfiles
# Served at https://setup.decentturing.com (Cloudflare Pages, served as text/plain)
#
# Install: curl -fsSL https://setup.decentturing.com | zsh
# (https:// + -L matter: the domain 301-redirects http -> https, and
# -f makes curl fail instead of piping an error page into zsh.)
# Read it: open the URL in a browser. Cloudflare Pages serves this file as
# text/plain, so the browser shows it as raw monospace text and
# `curl ... | zsh` runs it as a script. No HTML tricks needed.
#
# Linux/Debian only -- used mostly over SSH / remote VMs / devcontainers.
# Installs Oh-My-Zsh + zsh plugins + config files (zsh, tmux, vim, htop, claude)
# and the Claude Code CLI (npm if present, else the official native installer).
# Does NOT install system packages -- install tmux/gh/jq/kubectl/vim yourself
# (see README). Safe to re-run; backs up anything it would overwrite.
set -euo pipefail
# Which version of this repo to install config files from. An interactive run
# ALWAYS prompts you to choose (the menu uses /dev/tty, so `curl ... | zsh` works).
# Escape hatches: REPO_RAW (explicit source, e.g. file:///repo for tests) overrides
# everything; and only when there is NO TTY, DOTFILES_REF / `zsh -s -- <tag>`, else
# newest tag, else main. Use immutable tags for reproducible, tamper-evident installs.
REPO_SLUG="${REPO_SLUG:-fernandoataoldotcom/github_dotfiles}"
REPO_RAW="${REPO_RAW:-}"
DOTFILES_REF="${DOTFILES_REF:-}"
# --- Pinned dependency versions (resolved 2026-06-26) ------------------------
# Pinned for reproducible, supply-chain-safe installs. Bump deliberately.
# OMZ/vim-plug use commit SHAs (content-addressed). zsh plugins are cloned by tag
# but the resolved commit is VERIFIED against a pinned SHA (tags are mutable).
OMZ_REF="d2379b2701df66a36b217a7707e77f8029a99814" # ohmyzsh/ohmyzsh
VIM_PLUG_REF="88e31471818e9a29a8a20a0ee61360cfd7bdc1cd" # junegunn/vim-plug
ZSH_SYNTAX_HIGHLIGHTING_REF="0.8.0"; ZSH_SYNTAX_HIGHLIGHTING_SHA="db085e4661f6aafd24e5acb5b2e17e4dd5dddf3e"
ZSH_AUTOSUGGESTIONS_REF="v0.7.1"; ZSH_AUTOSUGGESTIONS_SHA="e52ee8ca55bcc56a17c828767a3f98f22a68d4eb"
YOU_SHOULD_USE_REF="1.11.1"; YOU_SHOULD_USE_SHA="ff371d6a11b653e1fa8dda4e61c896c78de26bfa"
TS="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="${HOME}/.dotfiles-backup-${TS}"
log() { print -r -- "==> $*"; }
warn() { print -r -- "WARN: $*" >&2; }
die() { print -r -- "ERROR: $*" >&2; exit 1; }
os_guard() {
[[ "$(uname -s)" == "Linux" ]] || die "Linux (Debian/Ubuntu) only. Detected $(uname -s). Aborting."
if [[ -r /etc/os-release ]]; then
local id idlike
id="$(. /etc/os-release 2>/dev/null; print -r -- "${ID:-}")"
idlike="$(. /etc/os-release 2>/dev/null; print -r -- "${ID_LIKE:-}")"
[[ "$id" == debian || "$id" == ubuntu || "$idlike" == *debian* ]] \
|| warn "Non Debian/Ubuntu Linux (ID='$id'); continuing but untested."
fi
command -v git >/dev/null 2>&1 || die "git is required. Install it and re-run."
command -v curl >/dev/null 2>&1 || die "curl is required. Install it and re-run."
}
# This repo's tags, newest first (empty if none / offline).
list_tags() {
git ls-remote --tags --refs "https://github.com/${REPO_SLUG}" 2>/dev/null \
| awk -F/ '{print $NF}' | sort -rV
}
# Interactive picker -- called only when /dev/tty is available. The menu is written
# to and read from /dev/tty (works even though the piped script is stdin); only the
# chosen ref is printed to stdout so it can be captured.
pick_tag() {
local tags; tags="$(list_tags)"
if [[ -z "$tags" ]]; then
warn "No tags on ${REPO_SLUG} yet -- cut one (git tag v1 && git push --tags) to enable version selection. Installing from 'main'."
print -r -- "refs/heads/main"; return 0
fi
local -a arr=("${(@f)tags}")
arr+=("refs/heads/main") # offer 'latest main' as a choice too
{
print -r -- ""
print -r -- "Which version of ${REPO_SLUG} do you want to install?"
local i label
for (( i = 1; i <= ${#arr[@]}; i++ )); do
label="${arr[$i]}"; [[ "$label" == "refs/heads/main" ]] && label="main (latest, unpinned)"
print -r -- " ${i}) ${label}"
done
print -rn -- "Enter a number [default 1 = ${arr[1]}]: "
} >/dev/tty
local choice=""
read -r choice </dev/tty || true
if [[ -z "$choice" ]]; then
print -r -- "${arr[1]}"
elif [[ "$choice" == <-> ]] && (( choice >= 1 && choice <= ${#arr[@]} )); then
print -r -- "${arr[$choice]}"
else
print -r -- "$choice" # treat input as a literal tag/ref name
fi
}
# Resolve REPO_RAW. Interactive runs ALWAYS prompt; REPO_RAW / DOTFILES_REF are
# escape hatches for automation.
select_repo_ref() {
if [[ -n "$REPO_RAW" ]]; then
log "Using REPO_RAW override: ${REPO_RAW}"; return 0
fi
local ref=""
# A real controlling terminal? Test by *opening* /dev/tty -- `[[ -r /dev/tty ]]`
# only checks the device's permission bits (always rw), not whether a tty exists.
if { true </dev/tty; } 2>/dev/null; then
ref="$(pick_tag)" # interactive: always ask the human
else
ref="${DOTFILES_REF:-${1:-}}" # no TTY (CI/automation): env / positional
if [[ -z "$ref" ]]; then
local tags; tags="$(list_tags)"
if [[ -n "$tags" ]]; then local -a arr=("${(@f)tags}"); ref="${arr[1]}"; else ref="refs/heads/main"; fi
fi
warn "No TTY; non-interactive install from ref: ${ref}"
fi
REPO_RAW="https://raw.githubusercontent.com/${REPO_SLUG}/${ref}"
log "Installing config files from ref: ${ref}"
}
backup_one() { # copy (not move) existing target into timestamped backup
local t="$1"; [[ -e "$t" || -L "$t" ]] || return 0
mkdir -p "$BACKUP_DIR"
local dest="${BACKUP_DIR}/${t#$HOME/}"
mkdir -p "${dest:h}"; cp -a "$t" "$dest"; log " backed up: $t"
}
backup_existing() {
log "Backing up any existing files to ${BACKUP_DIR}"
for f in ~/.tmux.conf ~/.vimrc ~/.config/htop/htoprc ~/.zshrc ~/.claude/settings.json \
~/.oh-my-zsh/custom/fernandoataoldotcom-functions.zsh; do
backup_one "$f"
done
}
install_omz() { # pinned clone; non-interactive; we ship our own .zshrc
if [[ -d ~/.oh-my-zsh ]]; then
log "Oh-My-Zsh already present; skipping."
else
# Clone + checkout a pinned commit instead of piping the upstream installer
# to sh (reproducible, and no remote script execution).
log "Installing Oh-My-Zsh (pinned @ ${OMZ_REF})"
git -c advice.detachedHead=false clone --quiet https://github.com/ohmyzsh/ohmyzsh ~/.oh-my-zsh
git -C ~/.oh-my-zsh -c advice.detachedHead=false checkout --quiet "$OMZ_REF"
fi
}
fetch() { local u="$1" d="$2"; mkdir -p "${d:h}"; curl -fsSL "$u" -o "$d"; log " fetched: $d"; }
fetch_configs() {
log "Fetching configs and custom scripts"
mkdir -p ~/.oh-my-zsh/custom
fetch "${REPO_RAW}/zshrc" ~/.zshrc
fetch "${REPO_RAW}/fernandoataoldotcom-functions.zsh" ~/.oh-my-zsh/custom/fernandoataoldotcom-functions.zsh
fetch "${REPO_RAW}/tmux.conf" ~/.tmux.conf
fetch "${REPO_RAW}/htoprc" ~/.config/htop/htoprc
fetch "${REPO_RAW}/vimrc" ~/.vimrc
fetch "${REPO_RAW}/claude-settings.json" ~/.claude/settings.json
fetch "https://raw.githubusercontent.com/junegunn/vim-plug/${VIM_PLUG_REF}/plug.vim" ~/.vim/autoload/plug.vim
}
clone_plugin() { # idempotent; tolerates already-installed under set -e
local url="$1" ref="$2" sha="$3" dir="$4"
[[ -d "$dir/.git" ]] && { log " plugin present: ${dir:t}"; return 0; }
[[ -e "$dir" ]] && { backup_one "$dir"; rm -rf "$dir"; }
git -c advice.detachedHead=false clone --depth 1 --branch "$ref" "$url" "$dir"
# Verify the tag resolved to the pinned commit (detects a moved/tampered tag).
local got; got="$(git -C "$dir" rev-parse HEAD)"
[[ "$got" == "$sha" ]] || die "Plugin ${dir:t}: tag '$ref' resolved to $got, expected $sha (moved/tampered tag?)."
log " cloned: ${dir:t}@${ref} (verified ${sha})"
}
install_zsh_plugins() {
log "Installing zsh plugins"
local p=~/.oh-my-zsh/custom/plugins; mkdir -p "$p"
clone_plugin https://github.com/zsh-users/zsh-syntax-highlighting "$ZSH_SYNTAX_HIGHLIGHTING_REF" "$ZSH_SYNTAX_HIGHLIGHTING_SHA" "$p/zsh-syntax-highlighting"
clone_plugin https://github.com/zsh-users/zsh-autosuggestions "$ZSH_AUTOSUGGESTIONS_REF" "$ZSH_AUTOSUGGESTIONS_SHA" "$p/zsh-autosuggestions"
clone_plugin https://github.com/MichaelAquilina/zsh-you-should-use.git "$YOU_SHOULD_USE_REF" "$YOU_SHOULD_USE_SHA" "$p/you-should-use"
}
install_vim_plugins() {
if command -v vim >/dev/null 2>&1; then
log "Installing vim plugins (vim +PlugInstall +qall)"
vim -E -s -u ~/.vimrc +PlugInstall +qall </dev/null >/dev/null 2>&1 \
|| warn "vim +PlugInstall returned non-zero (often harmless); re-run 'vim +PlugInstall +qall' if needed."
else
warn "vim not found; skipping. After installing vim, run: vim +PlugInstall +qall"
fi
}
# Install the Claude Code CLI so the box is usable right after this script.
# Idempotent: skips if already present. Prefers npm (when a Node toolchain exists);
# otherwise uses Anthropic's official native installer, which needs no Node and
# installs a user-local binary to ~/.local/bin (no sudo). Never aborts the run.
install_claude_code() {
# The native installer drops the binary in ~/.local/bin, which our shipped .zshrc
# adds to PATH but THIS script's shell usually does not. Put it on PATH up front so
# both the "already installed" check and the post-install check see it; otherwise a
# successful install gets misreported as a failure.
export PATH="${HOME}/.local/bin:${PATH}"
hash -r 2>/dev/null || true
if command -v claude >/dev/null 2>&1; then
log "Claude Code already installed ($(claude --version 2>/dev/null || print -r -- present)); skipping."
return 0
fi
log "Installing Claude Code CLI"
# Prefer npm when a Node toolchain exists, but FALL BACK to the native installer
# if the npm global install fails (e.g. an unwritable prefix / no sudo in a
# devcontainer) -- otherwise a failed `npm -g` would leave the box without claude.
if command -v npm >/dev/null 2>&1 && npm install -g @anthropic-ai/claude-code; then
: # installed via npm
else
command -v npm >/dev/null 2>&1 \
&& warn "npm install of Claude Code failed; falling back to the native installer."
# Native installer (user-local, no sudo, no Node) -> ~/.local/bin.
curl -fsSL https://claude.ai/install.sh | bash \
|| warn "Claude Code native install failed; see https://docs.anthropic.com/en/docs/claude-code"
fi
hash -r 2>/dev/null || true # forget cached lookups so a fresh binary is found
if command -v claude >/dev/null 2>&1; then
log " Claude Code installed ($(claude --version 2>/dev/null || print -r -- present)). Run 'claude' once to authenticate."
else
warn "Claude Code not on PATH yet; start a fresh shell (ensure ~/.local/bin is on PATH), then run 'claude' to authenticate."
fi
}
main() {
os_guard
select_repo_ref "$@"
# Dry run: print the resolved source and stop (no changes made).
[[ -n "${DOTFILES_RESOLVE_ONLY:-}" ]] && { print -r -- "REPO_RAW=${REPO_RAW}"; return 0; }
backup_existing
install_omz
install_zsh_plugins
fetch_configs
install_vim_plugins
install_claude_code
log "Done. Backups (if any): ${BACKUP_DIR}"
print -r -- ""
print -r -- "Next: start a fresh shell ('exec zsh'); reload tmux with 'tmux source-file ~/.tmux.conf'."
print -r -- "Install deps yourself if missing: tmux(>=3.2) gh jq kubectl vim"
print -r -- "Claude Code: settings are at ~/.claude/settings.json; run 'claude' once to authenticate."
}
main "$@"