#!/bin/bash # pwa-prereqs - check or install PWA development prerequisites (Xcode CLT, nvm, Node LTS) # # Usage: # pwa-prereqs # check-only: report which components are installed # pwa-prereqs --install # install any missing components # pwa-prereqs --install --shell both # pwa-prereqs -h # # Options: # -i, --install Install missing components (default is check-only) # -s, --shell WHICH Which shell's startup file(s) to wire up: auto|bash|zsh|both|none # (default auto -- detect from $SHELL; none writes nothing) # -h, --help Show help message _pwa_prereqs() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="pwa-prereqs" ;; esac _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } _warn() { echo "[WRN][$SCRIPT_NAME] $*" >&2; } _show_help() { local s; [ -t 1 ] && s=$'\033[4m' local r; [ -t 1 ] && r=$'\033[24m' cat < ~/.zshrc read by every interactive zsh (login and not) # bash -> ~/.bash_profile macOS login-shell target (Terminal.app opens login shells) # other -> ~/.profile POSIX fallback for unrecognized shells # These match what nvm's installer targets for macOS, chosen so a freshly # opened Terminal window actually sources the file _rc_file_for() { case "$1" in zsh) echo "$HOME/.zshrc" ;; bash) echo "$HOME/.bash_profile" ;; *) echo "$HOME/.profile" ;; esac } # Idempotently append a labeled block to a startup file, creating the file # (and its parent dir) if missing. A block is skipped when its marker line is # already present, so re-running --install never duplicates entries. # Args: $1 = component label (names the block in messages), $2 = rc file path, # $3 = grep marker (fixed string), $4 = block text _ensure_rc_block() { local label="$1" local rc="$2" local marker="$3" local block="$4" mkdir -p "$(dirname "$rc")" 2>/dev/null || : if [ -f "$rc" ] && grep -qF "$marker" "$rc" 2>/dev/null; then echo " $label already wired in $rc" return 0 fi if printf '\n%s\n' "$block" >>"$rc"; then echo " $label wired into $rc" else _error "Failed to write $label setup lines to $rc" return 1 fi } # Write the nvm setup lines into every target startup file. Called even when # nvm was already present, so a re-run repairs a startup file that never got # the lines. Wiring is keyed off NVM_DIR/HOME; returns nonzero on write failure _wire_shell_startup() { # nvm's canonical three-line block, matching what its own installer writes local nvm_block # shellcheck disable=SC2016 # "Expressions don't expand in single quotes, use double quotes for that." -- $NVM_DIR/$HOME must land in the rc file literally so they expand at the user's shell-startup time, not now nvm_block="$(printf '%s\n%s\n%s' \ 'export NVM_DIR="$HOME/.nvm"' \ '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' \ '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion')" # --shell none: touch no files. Print the block so a self-managing user # can add it wherever they keep their shell setup if [ "${#RC_TARGETS[@]}" -eq 0 ]; then echo "Skipping shell startup wiring (--shell none)." echo "To enable nvm in new shells yourself, add these lines to your shell startup file:" # Indent every line of the block by replacing each newline with # newline+2spaces; the leading " " handles the first line echo " ${nvm_block//$'\n'/$'\n '}" return 0 fi local rc for rc in "${RC_TARGETS[@]}"; do echo "Wiring shell startup: $rc" _ensure_rc_block "nvm" "$rc" 'NVM_DIR/nvm.sh' "$nvm_block" || return 1 done } local mode="check" local shell_choice="auto" _expand_short_opts "s" "$@" set -- "${_EXPANDED[@]}"; unset _EXPANDED while [ $# -gt 0 ]; do case "$1" in -h|--help) _show_help; return 0 ;; -i|--install) mode="install"; shift ;; -s|--shell) [ -n "${2-}" ] || { _error "--shell requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shell_choice="$2"; shift 2 ;; --shell=*) shell_choice="${1#*=}" [ -n "$shell_choice" ] || { _error "--shell requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; -*) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; *) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; esac done case "$shell_choice" in auto|bash|zsh|both|none) ;; *) _error "Invalid --shell '$shell_choice' (valid: auto|bash|zsh|both|none). Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; esac # Platform check: macOS only local platform; platform="$(uname -s)" if [ "$platform" != "Darwin" ]; then _error "macOS only (detected: $platform)" return 1 fi # --- check mode --- if [ "$mode" = "check" ]; then local all_ok=0 # Xcode Command Line Tools: `xcode-select -p` prints the active dev dir and exits 0 when present if xcode-select -p >/dev/null 2>&1; then echo "[ok] Xcode Command Line Tools" else echo "[missing] Xcode Command Line Tools" all_ok=1 fi # nvm is a shell function (not a PATH binary) and isn't inherited by this # child process, so `command -v nvm` can't detect it -- a sourceable # ~/.nvm/nvm.sh is the authoritative signal. Source it and confirm the # function appears local nvm_present=1 if [ -s "$HOME/.nvm/nvm.sh" ]; then # NVM_DIR must be exported -- nvm.sh reads it from the environment export NVM_DIR="$HOME/.nvm" # shellcheck disable=SC1091 # "Not following: $NVM_DIR/nvm.sh was not specified as input (see shellcheck -x)." -- nvm.sh is installed at runtime and not present at lint time; nothing to follow statically . "$NVM_DIR/nvm.sh" >/dev/null 2>&1 command -v nvm >/dev/null 2>&1 && nvm_present=0 fi if [ "$nvm_present" -eq 0 ]; then echo "[ok] nvm" else echo "[missing] nvm" all_ok=1 fi # Node LTS: if nvm is present, ask it; otherwise fall back to `node` in PATH local node_present=1 if [ "$nvm_present" -eq 0 ] && nvm ls --no-colors 2>/dev/null | grep -q 'lts'; then node_present=0 elif command -v node >/dev/null 2>&1; then node_present=0 fi if [ "$node_present" -eq 0 ]; then echo "[ok] Node LTS" else echo "[missing] Node LTS" all_ok=1 fi return "$all_ok" fi # --- install mode --- # Resolve which startup file(s) to wire. `auto` follows $SHELL; `both` # targets bash and zsh so the tools work no matter which shell is launched; # `none` wires nothing (RC_TARGETS stays empty) for self-managed dotfiles local -a RC_TARGETS=() local detected; detected="$(_detect_shell)" case "$shell_choice" in auto) RC_TARGETS+=("$(_rc_file_for "$detected")") [ "$detected" = "other" ] && _warn "Unrecognized \$SHELL '$SHELL'; wiring $HOME/.profile as a fallback" ;; bash) RC_TARGETS+=("$(_rc_file_for bash)") ;; zsh) RC_TARGETS+=("$(_rc_file_for zsh)") ;; both) RC_TARGETS+=("$(_rc_file_for bash)") RC_TARGETS+=("$(_rc_file_for zsh)") ;; none) ;; # leave RC_TARGETS empty; nvm's installer is opted out below too esac # PROFILE tells nvm's installer which file to append its own block to. Point # it at our primary target so nvm and this script agree; grep-guarded writes # keep the two from duplicating. For --shell none, /dev/null is nvm's # documented opt-out ("do NOT touch my profile"), so neither we nor nvm write local nvm_profile if [ "$shell_choice" = "none" ]; then nvm_profile="/dev/null" else nvm_profile="${RC_TARGETS[0]}" fi echo "--- START xcode ---" if xcode-select -p >/dev/null 2>&1; then echo "Xcode Command Line Tools already installed." echo "--- FINISH xcode ---" else echo "Installing Xcode Command Line Tools..." # Triggers Apple's GUI installer dialog. The command returns quickly; # the actual install happens asynchronously and requires user click-through xcode-select --install >/dev/null 2>&1 || : echo "A GUI installer was launched. Complete it, then re-run \`$SCRIPT_NAME --install\`." echo "--- FINISH xcode ---" return 1 fi # NVM_DIR must be exported -- nvm.sh and its child processes read it from the # environment (CONVENTIONS allows export for genuinely env-needed vars) export NVM_DIR="$HOME/.nvm" echo "--- START nvm ---" # `nvm` is a shell function, not a binary, and functions are NOT inherited by # a child process -- so `command -v nvm` is always false here in the executed # / `curl | bash` path. The authoritative "installed?" signal is a sourceable # nvm.sh; we source it into THIS process below either way so `nvm install` # works now (the startup-file wiring is what makes it persist for the user) if [ -s "$NVM_DIR/nvm.sh" ]; then echo "nvm is already installed." else # PROFILE steers nvm's installer to our target startup file. It must sit # on `bash` (right of the pipe), where the installer runs -- a prefix on # `curl` sets it only for curl and the installer never sees it if curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | PROFILE="$nvm_profile" bash; then echo "nvm installed successfully." else _error "nvm installation failed" return 1 fi if [ ! -s "$NVM_DIR/nvm.sh" ]; then _error "nvm script not found in $NVM_DIR after install" return 1 fi fi # Load nvm into this process. Sourcing nvm.sh defines the `nvm` function here; # verify it took, since `nvm install --lts` below depends on it # shellcheck disable=SC1091 # "Not following: $NVM_DIR/nvm.sh was not specified as input (see shellcheck -x)." -- nvm.sh is installed at runtime and not present at lint time; nothing to follow statically . "$NVM_DIR/nvm.sh" if ! command -v nvm >/dev/null 2>&1; then _error "nvm not available after sourcing $NVM_DIR/nvm.sh" return 1 fi echo "--- FINISH nvm ---" echo "--- START node/npm ---" echo "Installing latest Long-Term Support (LTS) Node.js..." if nvm install --lts; then # automatically runs `nvm use --lts` and sets the default alias echo "Node.js installed successfully." else _error "Node.js installation failed" return 1 fi echo "--- FINISH node/npm ---" # Wire startup files last, once everything is installed. Runs even when # components were already present, so a re-run repairs a startup file that is # missing the setup lines echo "--- START shell-startup ---" _wire_shell_startup || return 1 echo "--- FINISH shell-startup ---" echo "" if [ "$shell_choice" = "none" ]; then echo "All set. Shell startup was not modified (--shell none); add the lines" echo "above to your own setup so node and npm load in new shells." else echo "All set. node and npm are ready in NEW terminal windows." echo "To use them in THIS window right now, reload your shell:" echo " exec \"\$SHELL\" -l" echo "or just close this terminal and open a new one." fi ) _pwa_prereqs "$@" __pwa_prereqs_rc=$? unset -f _pwa_prereqs if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __pwa_prereqs_rc; return $__pwa_prereqs_rc" fi eval "unset __pwa_prereqs_rc; exit $__pwa_prereqs_rc"