Repository navigation
Expand file tree
/
Copy pathdevctl
More file actions
366 lines (315 loc) · 10.4 KB
/
Copy pathdevctl
File metadata and controls
366 lines (315 loc) · 10.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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
#!/usr/bin/env bash
# devctl v1.0 - resilient macOS dev maintenance CLI (developer-friendly output)
set -u
DRY_RUN=0
ASSUME_YES=0
STRICT=0
FAILURES=0
WARNINGS=0
LOG_DIR="$HOME/.local/state/devctl"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/devctl-$(date +%Y%m%d-%H%M%S).log"
# ---------- Colors ----------
if [[ -t 1 ]]; then
RESET=$'\033[0m'
RED=$'\033[31m'
GREEN=$'\033[32m'
YELLOW=$'\033[33m'
CYAN=$'\033[36m'
MAG=$'\033[35m'
DIM=$'\033[2m'
else
RESET=""; RED=""; GREEN=""; YELLOW=""; CYAN=""; MAG=""; DIM=""
fi
log() { printf "%s\n" "$*" >> "$LOG_FILE"; }
info() { printf "%s\n" "${CYAN}ℹ${RESET} $*"; log "$*"; }
ok() { printf "%s\n" "${GREEN}✓${RESET} $*"; log "$*"; }
warn() { WARNINGS=$((WARNINGS+1)); printf "%s\n" "${YELLOW}⚠${RESET} $*"; log "$*"; }
fail() { FAILURES=$((FAILURES+1)); printf "%s\n" "${RED}✗${RESET} $*"; log "$*"; }
hr() { printf "%s\n" "${DIM}------------------------------------------------------------${RESET}"; }
confirm() {
if [[ "$ASSUME_YES" == "1" ]]; then return 0; fi
read -r -p "$1 (y/n) " ans
[[ "${ans:-n}" == "y" ]]
}
has() { command -v "$1" >/dev/null 2>&1; }
# Capture output + exit code without killing whole script; show tail on failure.
run_cmd() {
local label="$1"; shift
info "$label"
if [[ "$DRY_RUN" == "1" ]]; then
printf "%s\n" " ${DIM}[dry-run]${RESET} $*"
return 0
fi
"$@" >> "$LOG_FILE" 2>&1
local rc=$?
if [[ $rc -eq 0 ]]; then
ok "$label"
else
fail "$label (exit $rc)"
printf "%s\n" " ${DIM}Last output:${RESET}"
tail -n 20 "$LOG_FILE" 2>/dev/null | sed 's/^/ /'
printf "%s\n" " ${DIM}Log:${RESET} $LOG_FILE"
if [[ "$STRICT" == "1" ]]; then
printf "%s\n" "Strict mode enabled. Exiting."
exit $rc
fi
fi
return $rc
}
# Helper to print "before → after" versions.
print_change() {
local label="$1" before="$2" after="$3"
if [[ -z "$before" && -z "$after" ]]; then return; fi
if [[ "$before" == "$after" ]]; then
printf "%s\n" "${DIM}${label}:${RESET} $after (unchanged)"
else
printf "%s\n" "${MAG}${label}:${RESET} ${before:-unknown} → ${after:-unknown}"
fi
}
# ---------- Version Snapshot ----------
# Grabs current versions for reporting. Safe if tools missing.
snapshot_versions() {
SNAP_BREW=""; SNAP_NODE=""; SNAP_NPM=""; SNAP_PNPM=""; SNAP_DOCKER=""; SNAP_COREPACK=""
if has brew; then SNAP_BREW="$(brew --version 2>/dev/null | head -n 1)"; fi
if has node; then SNAP_NODE="$(node -v 2>/dev/null)"; fi
if has npm; then SNAP_NPM="$(npm -v 2>/dev/null)"; fi
if has pnpm; then SNAP_PNPM="$(pnpm -v 2>/dev/null)"; fi
if has docker; then SNAP_DOCKER="$(docker --version 2>/dev/null)"; fi
if has corepack; then SNAP_COREPACK="$(corepack --version 2>/dev/null)"; fi
}
print_snapshot() {
printf "%s\n" "${MAG}Versions:${RESET}"
printf "%s\n" " brew: ${SNAP_BREW:-not found}"
printf "%s\n" " node: ${SNAP_NODE:-not found}"
printf "%s\n" " npm: ${SNAP_NPM:-not found}"
printf "%s\n" " pnpm: ${SNAP_PNPM:-not found}"
printf "%s\n" " corepack:${SNAP_COREPACK:-not found}"
printf "%s\n" " docker: ${SNAP_DOCKER:-not found}"
}
# Print authoritative Node info from an nvm-loaded login shell (path + version + current)
print_node_resolution() {
local nvm_dir="$1"
info "Active Node resolution:"
if [[ "$DRY_RUN" == "1" ]]; then
printf "%s\n" " ${DIM}[dry-run]${RESET} which node; node -v; nvm current"
return 0
fi
bash -lc "source \"$nvm_dir/nvm.sh\" >/dev/null 2>&1 || true; \
echo \" node: \$(command -v node 2>/dev/null || echo 'not found')\"; \
echo \" version: \$(node -v 2>/dev/null || echo 'not found')\"; \
echo \" nvm current: \$(command -v nvm >/dev/null 2>&1 && nvm current || echo 'nvm not found')\";" \
| tee -a "$LOG_FILE"
}
# ---------- Bundles ----------
brew_bundle() {
printf "%s\n" "---- Homebrew ----"
has brew || { warn "brew not found"; return; }
local before_brew="$SNAP_BREW"
run_cmd "brew update" brew update
# Show what's outdated BEFORE upgrading (helpful for decision-making)
info "Outdated packages (if any):"
if [[ "$DRY_RUN" == "1" ]]; then
printf "%s\n" " ${DIM}[dry-run]${RESET} brew outdated"
else
local outdated
outdated="$(brew outdated 2>/dev/null || true)"
if [[ -z "$outdated" ]]; then
printf "%s\n" " (none)"
log " (none)"
else
printf "%s\n" "$outdated" | sed 's/^/ /' | tee -a "$LOG_FILE"
fi
fi
if confirm "Upgrade installed formulae and casks?"; then
run_cmd "brew upgrade" brew upgrade
else
info "Skipped brew upgrade"
fi
if confirm "Run brew cleanup (remove old versions)?"; then
run_cmd "brew cleanup" brew cleanup
else
info "Skipped brew cleanup"
fi
# brew doctor returns non-zero when it finds warnings; treat as warning, not failure.
if confirm "Run brew doctor (diagnostics)?"; then
info "brew doctor (warnings are OK)"
if [[ "$DRY_RUN" == "1" ]]; then
printf "%s\n" " ${DIM}[dry-run]${RESET} brew doctor"
else
brew doctor >> "$LOG_FILE" 2>&1
local rc=$?
if [[ $rc -eq 0 ]]; then
ok "brew doctor"
else
warn "brew doctor reported warnings (exit $rc) — not treated as failure"
printf "%s\n" " ${DIM}Last output:${RESET}"
tail -n 25 "$LOG_FILE" 2>/dev/null | sed 's/^/ /'
printf "%s\n" " ${DIM}Log:${RESET} $LOG_FILE"
fi
fi
else
info "Skipped brew doctor"
fi
# Update snapshot + report change
snapshot_versions
print_change "brew" "$before_brew" "$SNAP_BREW"
}
node_bundle() {
printf "%s\n" "---- Node / NVM ----"
local before_node="$SNAP_NODE"
local before_npm="$SNAP_NPM"
local before_pnpm="$SNAP_PNPM"
local before_corepack="$SNAP_COREPACK"
export NVM_DIR="$HOME/.nvm"
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
# shellcheck source=/dev/null
source "$NVM_DIR/nvm.sh"
else
warn "nvm not found ($HOME/.nvm/nvm.sh missing)"
fi
# Show currently installed Node versions if nvm available (useful context)
if has nvm; then
info "Installed Node versions (nvm ls):"
if [[ "$DRY_RUN" == "1" ]]; then
printf "%s\n" " ${DIM}[dry-run]${RESET} nvm ls"
else
nvm ls 2>/dev/null | sed 's/^/ /' | tee -a "$LOG_FILE" || true
fi
fi
if has nvm && confirm "Install latest Node LTS via nvm and set as default?"; then
# Use subshell to avoid poisoning current shell state
run_cmd "nvm install --lts" bash -lc "source \"$NVM_DIR/nvm.sh\" && nvm install --lts"
run_cmd "nvm alias default lts/*" bash -lc "source \"$NVM_DIR/nvm.sh\" && nvm alias default lts/*"
run_cmd "nvm use default" bash -lc "source \"$NVM_DIR/nvm.sh\" && nvm use default"
else
info "Skipped Node LTS install"
fi
# Authoritative resolution info (path + version + current)
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
print_node_resolution "$NVM_DIR"
fi
# Print versions from current shell snapshot (may differ from nvm subshell; resolution info above is authoritative)
snapshot_versions
print_change "node" "$before_node" "$SNAP_NODE"
print_change "npm" "$before_npm" "$SNAP_NPM"
# npm update
if has npm && confirm "Update npm globally (npm@latest)?"; then
run_cmd "npm install -g npm" npm install -g npm
snapshot_versions
print_change "npm" "$before_npm" "$SNAP_NPM"
else
info "Skipped npm update"
fi
if has npm; then
run_cmd "npm cache verify" npm cache verify
fi
# pnpm update (Corepack-first)
if has pnpm; then
print_change "pnpm" "$before_pnpm" "$SNAP_PNPM"
if confirm "Update pnpm (prefer corepack if available)?"; then
if has corepack; then
run_cmd "corepack enable" corepack enable
run_cmd "corepack prepare pnpm@latest --activate" corepack prepare pnpm@latest --activate
elif has npm; then
run_cmd "npm install -g pnpm" npm install -g pnpm
else
warn "No corepack or npm available to update pnpm"
fi
snapshot_versions
print_change "pnpm" "$before_pnpm" "$SNAP_PNPM"
print_change "corepack" "$before_corepack" "$SNAP_COREPACK"
else
info "Skipped pnpm update"
fi
if confirm "Prune pnpm store (safe cleanup)?"; then
run_cmd "pnpm store prune" pnpm store prune
else
info "Skipped pnpm store prune"
fi
else
if confirm "pnpm not found. Install pnpm?"; then
if has corepack; then
run_cmd "corepack enable" corepack enable
run_cmd "corepack prepare pnpm@latest --activate" corepack prepare pnpm@latest --activate
elif has npm; then
run_cmd "npm install -g pnpm" npm install -g pnpm
else
warn "No corepack or npm available to install pnpm"
fi
snapshot_versions
print_change "pnpm" "$before_pnpm" "$SNAP_PNPM"
fi
fi
}
docker_bundle() {
printf "%s\n" "---- Docker ----"
has docker || { warn "docker not found"; return; }
# If daemon isn't running, skip to avoid hangs.
if [[ "$DRY_RUN" != "1" ]]; then
docker info >/dev/null 2>&1
local daemon_rc=$?
if [[ $daemon_rc -ne 0 ]]; then
warn "Docker daemon not running; skipping Docker steps."
return
fi
fi
run_cmd "docker system df" docker system df
if confirm "Prune unused docker resources (non-interactive)?"; then
run_cmd "docker system prune -f" docker system prune -f
else
info "Skipped docker prune"
fi
}
status() {
printf "%s\n" "---- Status ----"
snapshot_versions
print_snapshot
printf "%s\n" "Log: $LOG_FILE"
}
summary() {
hr
printf "%s\n" "Warnings: $WARNINGS"
printf "%s\n" "Failures: $FAILURES"
printf "%s\n" "Log: $LOG_FILE"
}
# ---------- CLI ----------
cmd="${1:-}"
case "$cmd" in
run)
hr
info "devctl v1.0 starting"
log "devctl start $(date)"
snapshot_versions
print_snapshot
hr
brew_bundle
node_bundle
docker_bundle
hr
info "Final versions:"
snapshot_versions
print_snapshot
summary
;;
status)
status
;;
""|-h|--help)
printf "%s\n" "devctl v1.0"
printf "%s\n" "Usage:"
printf "%s\n" " devctl run"
printf "%s\n" " devctl status"
printf "%s\n" ""
printf "%s\n" "Notes:"
printf "%s\n" " - Docker steps are skipped if the daemon isn't running."
printf "%s\n" " - pnpm updates prefer corepack; fallback to npm."
printf "%s\n" " - brew doctor warnings are not treated as failures."
printf "%s\n" " - Logs: $LOG_DIR/"
;;
*)
printf "%s\n" "Unknown command: $cmd"
printf "%s\n" "Try: devctl --help"
exit 2
;;
esac