#!/bin/zsh # XX # XX XX # XX X # X XX # XX XX # XX X # X XX # XX X # XX XX XX # X XX XX XX # XX XX X X # XX X XX XX # X XX XX X # XX XX X XX # XX X XX X # X XX X XX # XX XX XX XX XX # XX XX XX X X X # X X X XX XX XX # XX XX XX X X X # XX XX XX XX XX XX # X XXXXXXX XX XX XX # XX X X X # XX XX XX XX # XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXX ## Always run from the home folder cd NORTHSLOPE_DIR="${HOME}/.northslope" mkdir -p "${NORTHSLOPE_DIR}" #============================================================================== # SELF-UPDATE #============================================================================== case "$1" in --skip-update|--child|skip) shift ;; *) curl_output=$(curl -fsSL https://setup.northslope.dev/setup.sh -o "${NORTHSLOPE_DIR}/setup.sh" 2>&1) curl_exit_code=$? if [[ ${curl_exit_code} -ne 0 ]]; then echo "'setup' Failure 🚫" echo "Failed to download updated setup.sh: ${curl_output}" exit 1 fi chmod +x "${NORTHSLOPE_DIR}/setup.sh" exec /bin/zsh "${NORTHSLOPE_DIR}/setup.sh" --skip-update "$@" ;; esac # When invoked via pipe (e.g. curl ... | zsh), stdin is the pipe — not a TTY. # Reconnect stdin to the terminal so interactive prompts work. if [[ ! -t 0 ]] && [[ -r /dev/tty ]]; then exec < /dev/tty fi #============================================================================== # CONSTANTS #============================================================================== NORTHSLOPE_PACKAGES_DIR="${NORTHSLOPE_DIR}/packages" NS_MACHINE_SETUP_DIR="${NORTHSLOPE_PACKAGES_DIR}/ns-machine-setup" NS_MACHINE_SETUP_REPO="git@github.com:northslopetech/ns-machine-setup.git" NORTHSLOPE_SSH_KEY_PATH="${HOME}/.ssh/id_ed25519_northslope" NS_MACHINE_SETUP_VENV="${NORTHSLOPE_PACKAGES_DIR}/ns-machine-setup-venv" SCRIPT_VERSION="v5.14.2" mkdir -p "${NORTHSLOPE_PACKAGES_DIR}" #============================================================================== # LOGGING #============================================================================== _NS_RESET='\033[0m' _NS_BOLD='\033[1m' _NS_GREEN='\033[32m' _NS_YELLOW='\033[33m' _NS_RED='\033[31m' _NS_DIM='\033[2m' if [[ ! -t 1 ]] || [[ -n "${NO_COLOR:-}" ]]; then _NS_RESET='' _NS_BOLD='' _NS_GREEN='' _NS_YELLOW='' _NS_RED='' _NS_DIM='' fi log_section() { printf "\n${_NS_BOLD}▸ %s${_NS_RESET}\n" "$1"; } log_ok() { printf " ${_NS_GREEN}✓${_NS_RESET} %s\n" "$1"; } log_info() { printf " → %s\n" "$1"; } log_warn() { printf "\n ${_NS_YELLOW}⚠${_NS_RESET} %s\n" "$1"; } log_error() { printf " ${_NS_RED}✗${_NS_RESET} %s\n" "$1"; } printf "${_NS_BOLD}Northslope Machine Setup ${SCRIPT_VERSION}${_NS_RESET} 🚀\n" echo "${SCRIPT_VERSION}" > "${NORTHSLOPE_DIR}/setup-version" echo # Extract --debug before prereqs so NS_SETUP_DEBUG is available to all prereq scripts. if [[ "$1" == "--debug" ]]; then export NS_SETUP_DEBUG=1 shift fi #============================================================================== # FILE PERMISSIONS #============================================================================== log_section "File Permissions" mkdir -p "${HOME}/.northslope" _fp_errors=() if [[ ! -w "${HOME}/.northslope" ]]; then _fp_errors+=("No write permission for directory: ~/.northslope") fi for _fp_file in "${HOME}/.zshrc" "${HOME}/.bashrc" "${HOME}/.gitconfig"; do if [[ -e "${_fp_file}" ]]; then if [[ ! -w "${_fp_file}" ]]; then _fp_errors+=("No write permission for file: ${_fp_file}") fi else touch "${_fp_file}" 2>/dev/null || _fp_errors+=("Cannot create file: ${_fp_file}") fi done if [[ ${#_fp_errors[@]} -gt 0 ]]; then log_error "Permission errors detected:" for _fp_err in "${_fp_errors[@]}"; do printf " - %s\n" "${_fp_err}" done exit 1 fi log_ok "File permissions OK" #============================================================================== # XCODE COMMAND LINE TOOLS #============================================================================== log_section "Xcode Command Line Tools" xcode_output=$(xcode-select -p 2>&1) xcode_exit_code=$? if [[ ${xcode_exit_code} -ne 0 ]]; then log_info "Installing Xcode Command Line Tools..." xcode-select --install xcode_install_trigger_code=$? if [[ ${xcode_install_trigger_code} -ne 0 ]]; then log_error "xcode-select --install failed. Please install manually and re-run setup." exit 1 fi log_warn "A dialog has appeared — click 'Install' and wait for it to finish." log_info "Waiting for installation to complete..." until xcode-select -p &>/dev/null; do read -s -k 1 "?Once installation is complete, press Enter to continue..." /dev/null || true echo "" done log_ok "Xcode Command Line Tools installed" else log_ok "Xcode Command Line Tools already installed" fi #============================================================================== # HOMEBREW OWNERSHIP FIX #============================================================================== if [[ -d /opt/homebrew ]]; then homebrew_owner=$(stat -f '%Su' /opt/homebrew) if [[ "${homebrew_owner}" != "$(whoami)" ]]; then log_section "Homebrew Ownership" log_info "Fixing /opt/homebrew ownership (changed by macOS update)..." log_info "Please enter your computer password when prompted." sudo chown -R "$(whoami)" /opt/homebrew if [[ $? -ne 0 ]]; then log_error "Failed to fix /opt/homebrew ownership. Please run: sudo chown -R \$(whoami) /opt/homebrew" exit 1 fi log_ok "Homebrew ownership fixed" fi fi #============================================================================== # HOMEBREW #============================================================================== log_section "Homebrew" if ! brew --help > /dev/null 2>&1; then log_info "Installing Homebrew (this can take 10–20 minutes)..." log_info "You will be prompted for your computer password." sudo -v BREW_INSTALL_SHA256="dfd5145fe2aa5956a600e35848765273f5798ce6def01bd08ecec088a1268d91" # Pinned to Homebrew/install@61f57de — update SHA + checksum together when bumping BREW_INSTALL_TMP="$(mktemp)" curl -fsSL "https://raw.githubusercontent.com/Homebrew/install/61f57debbf8b06e07daf60e514bed21f81df493e/install.sh" -o "$BREW_INSTALL_TMP" shasum -a 256 "$BREW_INSTALL_TMP" | grep -q "^${BREW_INSTALL_SHA256}" || { echo "Error: Homebrew installer checksum mismatch — aborting" >&2; rm -f "$BREW_INSTALL_TMP"; exit 1 } bash "$BREW_INSTALL_TMP" rm -f "$BREW_INSTALL_TMP" eval "$(/opt/homebrew/bin/brew shellenv)" fi if ! brew --help > /dev/null 2>&1; then log_error "Homebrew installation failed. Please contact @tnguyen." exit 1 fi log_ok "Homebrew ready" #============================================================================== # SET ZSH TO DEFAULT SHELL #============================================================================== log_section "Default Shell" _current_shell=$(dscl . -read "/Users/$(whoami)" UserShell 2>/dev/null | awk '{print $NF}') if [[ "${_current_shell}" == "/bin/zsh" ]]; then log_ok "Default shell already /bin/zsh" else chsh -s /bin/zsh if [[ $? -ne 0 ]]; then log_warn "chsh failed — set manually with: chsh -s /bin/zsh" else log_ok "Default shell set to /bin/zsh" fi fi #============================================================================== # HOMEBREW SHELL RC #============================================================================== eval "$(/opt/homebrew/bin/brew shellenv)" #============================================================================== # PYTHON (BREW) #============================================================================== log_section "Python 3.13" # Always use the brew-installed binary directly to avoid asdf shim interference. # asdf shims for python3.13 exist even when no version is set, causing silent failures. BREW_PYTHON313="$(brew --prefix python@3.13 2>/dev/null)/bin/python3.13" # Validate python works — not just that the binary exists. A broken Homebrew install # can leave an executable binary with missing dylibs that uv rejects. # Note: platform.mac_ver() returns '' on macOS Tahoe (Darwin 25+), so we use sys.version_info instead. _python313_ok() { [[ -x "$1" ]] && "$1" -c "import sys; exit(0 if sys.version_info[:2] == (3, 13) else 1)" 2>/dev/null } if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] BREW_PYTHON313=%s\n" "${BREW_PYTHON313}" printf " [debug] binary exists: %s\n" "$([[ -e "$BREW_PYTHON313" ]] && echo yes || echo no)" printf " [debug] binary executable: %s\n" "$([[ -x "$BREW_PYTHON313" ]] && echo yes || echo no)" printf " [debug] sys.version: %s\n" "$("$BREW_PYTHON313" -c "import sys; print(sys.version)" 2>&1)" printf " [debug] which python3.13: %s\n" "$(which python3.13 2>/dev/null)" printf " [debug] type -a python3.13:\n"; type -a python3.13 2>/dev/null | sed 's/^/ /' fi if _python313_ok "$BREW_PYTHON313"; then PYTHON3_BIN="$BREW_PYTHON313" else if [[ -x "$BREW_PYTHON313" ]]; then log_info "python@3.13 is broken — reinstalling..." brew reinstall python@3.13 else log_info "python3.13 not found — installing python@3.13 via brew..." brew install python@3.13 fi if [[ $? -ne 0 ]]; then log_error "python@3.13 installation failed." exit 1 fi BREW_PYTHON313="$(brew --prefix python@3.13)/bin/python3.13" if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] after install — BREW_PYTHON313=%s\n" "${BREW_PYTHON313}" printf " [debug] after install — sys.version: %s\n" "$("$BREW_PYTHON313" -c "import sys; print(sys.version)" 2>&1)" fi if _python313_ok "$BREW_PYTHON313"; then PYTHON3_BIN="$BREW_PYTHON313" else log_error "python3.13 not found or broken after install." exit 1 fi fi log_ok "python3 ready ($("${PYTHON3_BIN}" --version 2>/dev/null))" log_section "uv" UV_BIN="$(brew --prefix uv 2>/dev/null)/bin/uv" if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] UV_BIN=%s\n" "${UV_BIN}" printf " [debug] which uv: %s\n" "$(which uv 2>/dev/null)" fi if [[ ! -x "$UV_BIN" ]]; then log_info "uv not found — installing via brew..." brew install uv if [[ $? -ne 0 ]]; then log_error "uv installation failed." exit 1 fi UV_BIN="$(brew --prefix uv)/bin/uv" if [[ ! -x "$UV_BIN" ]]; then log_error "uv not found after brew install." exit 1 fi fi log_ok "uv ready ($("${UV_BIN}" --version 2>/dev/null))" log_section "Virtualenv" VENV_PYTHON3="${NS_MACHINE_SETUP_VENV}/bin/python3" VENV_NEEDS_RECREATE=1 if [[ -x "$VENV_PYTHON3" ]] && "$VENV_PYTHON3" -c "import sys; v=sys.version_info; exit(0 if v.major==3 and v.minor==13 else 1)" 2>/dev/null && "$VENV_PYTHON3" -m pip --version &>/dev/null; then VENV_NEEDS_RECREATE=0 fi if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] NS_MACHINE_SETUP_VENV=%s\n" "${NS_MACHINE_SETUP_VENV}" printf " [debug] VENV_PYTHON3=%s\n" "${VENV_PYTHON3}" printf " [debug] venv python exists: %s\n" "$([[ -x "$VENV_PYTHON3" ]] && echo yes || echo no)" printf " [debug] VENV_NEEDS_RECREATE=%s\n" "${VENV_NEEDS_RECREATE}" printf " [debug] PYTHON3_BIN=%s\n" "${PYTHON3_BIN}" fi if [[ $VENV_NEEDS_RECREATE -eq 1 ]]; then if [[ -e "${NS_MACHINE_SETUP_VENV}" ]]; then log_info "Removing invalid virtualenv..." rm -rf "${NS_MACHINE_SETUP_VENV}" fi log_info "Creating virtualenv..." "${UV_BIN}" venv --seed --python "${PYTHON3_BIN}" "${NS_MACHINE_SETUP_VENV}" if [[ $? -ne 0 ]]; then log_error "Failed to create virtualenv at ${NS_MACHINE_SETUP_VENV}." exit 1 fi fi "${NS_MACHINE_SETUP_VENV}/bin/python3" -m pip install --quiet questionary if [[ $? -ne 0 ]]; then log_error "Failed to install questionary." exit 1 fi log_ok "Virtualenv ready" #============================================================================== # GITHUB CLI #============================================================================== log_section "GitHub CLI" # Use the brew-installed binary directly to avoid asdf shim interference # (same pattern as JQ_BIN in jq.sh). # gh 2.83.0 introduced --json support on auth status. GH_MIN_VERSION="2.83.0" GH_BIN="$(brew --prefix gh 2>/dev/null)/bin/gh" _gh_installed_version() { "${GH_BIN}" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 } _gh_version_ge() { # Returns 0 if $1 >= $2 (both in x.y.z format). awk -v a="$1" -v b="$2" 'BEGIN { split(a, av, "."); split(b, bv, ".") for (i = 1; i <= 3; i++) { if (av[i]+0 > bv[i]+0) exit 0 if (av[i]+0 < bv[i]+0) exit 1 } exit 0 }' } if [[ ! -x "${GH_BIN}" ]]; then log_info "Installing GitHub CLI..." brew install gh GH_BIN="$(brew --prefix gh 2>/dev/null)/bin/gh" if [[ ! -x "${GH_BIN}" ]]; then log_error "GitHub CLI not found after install." exit 1 fi elif ! _gh_version_ge "$(_gh_installed_version)" "${GH_MIN_VERSION}"; then log_info "Upgrading GitHub CLI ($(_gh_installed_version) → ${GH_MIN_VERSION}+)..." brew upgrade gh GH_BIN="$(brew --prefix gh 2>/dev/null)/bin/gh" fi log_ok "GitHub CLI ready" if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] GH_BIN=%s\n" "${GH_BIN}" printf " [debug] gh version: %s\n" "$("${GH_BIN}" --version 2>&1 | head -1)" printf " [debug] which gh: %s\n" "$(which gh 2>/dev/null)" printf " [debug] type -a gh:\n"; type -a gh 2>/dev/null | sed 's/^/ /' fi #============================================================================== # GIT USER.NAME #============================================================================== CURRENT_GIT_NAME=$(git config --global user.name 2>/dev/null) if [[ -z "${CURRENT_GIT_NAME}" ]]; then log_section "git user.name" while true; do printf " → What is your full name? (e.g. Tam Nguyen): " read -r GIT_USER_NAME if [[ -n "${GIT_USER_NAME}" ]]; then git config --global user.name "${GIT_USER_NAME}" break fi log_warn "Name cannot be empty." done log_ok "git user.name set" fi #============================================================================== # GIT USER.EMAIL #============================================================================== CURRENT_GIT_EMAIL=$(git config --global user.email 2>/dev/null) if [[ -z "${CURRENT_GIT_EMAIL}" ]]; then log_section "git user.email" while true; do printf " → What is your email address? (e.g. test@northslopetech.com): " read -r GIT_USER_EMAIL if [[ -n "${GIT_USER_EMAIL}" ]]; then git config --global user.email "${GIT_USER_EMAIL}" break fi log_warn "Email cannot be empty." done log_ok "git user.email set" fi #============================================================================== # GH AUTH #============================================================================== log_section "gh auth" "${GH_BIN}" auth status > /dev/null 2>&1 GH_AUTH_ALREADY_SET=$? if [[ ${GH_AUTH_ALREADY_SET} -ne 0 ]]; then log_info "Not authorized. Authenticating..." if [[ ! -f "${NORTHSLOPE_SSH_KEY_PATH}" ]]; then log_info "Generating SSH key..." mkdir -p "${HOME}/.ssh" && chmod 700 "${HOME}/.ssh" ssh-keygen -t ed25519 -f "${NORTHSLOPE_SSH_KEY_PATH}" -N "" -C "northslope" fi GH_AUTH_ATTEMPTS=0 while true; do GH_AUTH_ATTEMPTS=$((GH_AUTH_ATTEMPTS + 1)) if [[ ${GH_AUTH_ATTEMPTS} -gt 5 ]]; then log_error "Too many failed login attempts. Press Ctrl+C to exit." break fi "${GH_BIN}" auth login --hostname github.com --git-protocol ssh --skip-ssh-key --scopes write:public_key --web gh_auth_status=$? if [[ ${gh_auth_status} -eq 0 ]]; then NSLP_USER=$("${GH_BIN}" auth status --json hosts --jq '.hosts | to_entries[] | .value[] | select(.login | endswith("_nslp")) | .login' 2>/dev/null | head -1) GIT_PROTOCOL=$("${GH_BIN}" auth status --json hosts --jq '.hosts | to_entries[] | .value[] | select(.login | endswith("_nslp")) | .gitProtocol' 2>/dev/null | head -1) if [[ -z "${NSLP_USER}" ]]; then log_warn "You must log in with your Northslope GitHub account (username ending in _nslp)." log_info "If you don't have one, visit: https://setup.northslope.dev/github-enterprise" continue fi if [[ "${GIT_PROTOCOL}" == "ssh" ]]; then log_ok "gh auth authorized" while IFS= read -r key_id; do [[ -n "${key_id}" ]] && "${GH_BIN}" ssh-key delete "${key_id}" --yes done < <("${GH_BIN}" api user/keys --jq '.[] | select(.title == "ns-machine") | .id') if "${GH_BIN}" ssh-key add "${NORTHSLOPE_SSH_KEY_PATH}.pub" --title "ns-machine"; then log_ok "SSH key 'ns-machine' registered" break else log_error "gh ssh-key add failed." break fi else log_warn "Git protocol is '${GIT_PROTOCOL}', SSH is required. Logging out and retrying..." "${GH_BIN}" auth logout -u "${NSLP_USER}" > /dev/null 2>&1 fi else log_error "gh auth login failed or was interrupted." break fi done else log_ok "gh auth already authorized" NSLP_USER=$("${GH_BIN}" auth status --json hosts --jq '.hosts | to_entries[] | .value[] | select(.login | endswith("_nslp")) | .login' 2>/dev/null | head -1) if [[ -f "${NORTHSLOPE_SSH_KEY_PATH}.pub" ]]; then NS_MACHINE_KEY_ID=$("${GH_BIN}" api user/keys --jq '.[] | select(.title == "ns-machine") | .id' 2>/dev/null | head -1) if [[ -z "${NS_MACHINE_KEY_ID}" ]]; then "${GH_BIN}" auth refresh -h github.com --scopes write:public_key while IFS= read -r key_id; do [[ -n "${key_id}" ]] && "${GH_BIN}" ssh-key delete "${key_id}" --yes done < <("${GH_BIN}" api user/keys --jq '.[] | select(.title == "ns-machine") | .id') "${GH_BIN}" ssh-key add "${NORTHSLOPE_SSH_KEY_PATH}.pub" --title "ns-machine" \ && log_ok "SSH key 'ns-machine' registered" fi fi fi if [[ -f "${NORTHSLOPE_SSH_KEY_PATH}.pub" ]] && [[ ! -f "${HOME}/.ssh/config" ]]; then mkdir -p "${HOME}/.ssh" && chmod 700 "${HOME}/.ssh" { printf '\nHost github.com\n' printf ' IdentityFile ~/.ssh/id_ed25519_northslope\n' printf ' IdentitiesOnly yes\n' } >> "${HOME}/.ssh/config" chmod 600 "${HOME}/.ssh/config" log_ok "GitHub SSH config written" fi #============================================================================== # NORTHSLOPETECH ORG #============================================================================== log_section "northslopetech org" if "${GH_BIN}" org list | grep -q "^northslope-tech$"; then log_warn "Your account is in 'northslope-tech' (the old org)." log_info "Follow https://setup.northslope.dev/github-enterprise to migrate." exit 1 fi if ! "${GH_BIN}" org list | grep -q "^northslopetech$"; then log_warn "You are not a member of the northslopetech org." log_info "Contact @tnguyen to be added. Press Enter when you have an invitation..." read open 'https://github.com/orgs/northslopetech/invitation' log_info "Press Enter after accepting the invitation..." read if ! "${GH_BIN}" org list | grep -q "northslopetech"; then log_error "Still not in northslopetech org. Contact @tnguyen." exit 1 fi fi log_ok "northslopetech org verified" #============================================================================== # JQ #============================================================================== log_section "jq" # Use the brew-installed binary directly to avoid asdf shim interference. JQ_BIN="$(brew --prefix jq 2>/dev/null)/bin/jq" if [[ ! -x "${JQ_BIN}" ]]; then log_info "Installing jq..." brew install jq JQ_BIN="$(brew --prefix jq 2>/dev/null)/bin/jq" if [[ ! -x "${JQ_BIN}" ]]; then log_error "jq not found after brew install." exit 1 fi fi log_ok "jq ready ($(${JQ_BIN} --version))" if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] JQ_BIN=%s\n" "${JQ_BIN}" fi #============================================================================== # GITHUB SSO #============================================================================== log_section "GitHub SSO" # Parse the _nslp account from plain JSON output — no --jq so inactive/secondary # accounts on the same host are included. gh's inline --jq (gojq) can silently # miss non-active accounts, causing spurious re-auth on machines where the _nslp # account already exists but is not the currently active gh account. # GH_BIN and JQ_BIN are set by their prereqs (direct brew paths, avoids asdf # shim interference — the asdf shim can produce unexpected stdout that silently # breaks piped JSON parsing). _gh_auth_json() { "${GH_BIN}" auth status --json hosts 2>/dev/null } _nslp_user() { _gh_auth_json \ | "${JQ_BIN}" -r '.hosts | to_entries[] | .value[] | select(.login | endswith("_nslp")) | .login' \ 2>/dev/null | head -1 } _nslp_has_scope() { local scope="$1" _gh_auth_json \ | "${JQ_BIN}" -r '.hosts | to_entries[] | .value[] | select(.login | endswith("_nslp")) | .scopes' \ 2>/dev/null | grep -qF "${scope}" } # Check for _nslp user FIRST — scope refresh is meaningless without one and # triggers a spurious device-flow on fresh machines. if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] GH_BIN=%s\n" "${GH_BIN}" printf " [debug] JQ_BIN=%s\n" "${JQ_BIN}" printf " [debug] gh auth status --json hosts (stdout):\n" "${GH_BIN}" auth status --json hosts 2>/dev/null | head -5 | sed 's/^/ /' printf " [debug] gh auth status --json hosts (stderr):\n" "${GH_BIN}" auth status --json hosts 2>&1 >/dev/null | head -5 | sed 's/^/ /' printf " [debug] jq parse result:\n" "${GH_BIN}" auth status --json hosts 2>/dev/null \ | "${JQ_BIN}" -r '.hosts | to_entries[] | .value[] | select(.login | endswith("_nslp")) | .login' \ 2>&1 | head -5 | sed 's/^/ /' printf " [debug] _nslp_user()=%s\n" "$(_nslp_user)" fi NSLP_USER=$(_nslp_user) if [[ -z "${NSLP_USER}" ]]; then log_warn "No _nslp account found — opening GitHub login..." "${GH_BIN}" auth login --hostname github.com --git-protocol ssh --skip-ssh-key --scopes write:public_key --web if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] gh auth status after login (stdout):\n" "${GH_BIN}" auth status --json hosts 2>/dev/null | head -3 | sed 's/^/ /' printf " [debug] gh auth status after login (stderr):\n" "${GH_BIN}" auth status --json hosts 2>&1 >/dev/null | head -3 | sed 's/^/ /' printf " [debug] _nslp_user() after login=%s\n" "$(_nslp_user)" fi NSLP_USER=$(_nslp_user) if [[ -z "${NSLP_USER}" ]]; then log_warn "GitHub authentication failed — sign in with your _nslp account and re-run setup." exit 1 fi fi SSO_ATTEMPT=0 while true; do SSO_ATTEMPT=$((SSO_ATTEMPT + 1)) if [[ ${SSO_ATTEMPT} -gt 5 ]]; then log_error "Too many SSH retries (SAML or permission-denied) — re-run setup." exit 1 fi # Test with default SSH — do not force northslope key. # If SSH already works, skip key generation and config changes entirely. GH_SSO_OUTPUT=$(git ls-remote git@github.com:northslopetech/ns-cli.git 2>&1) GH_SSO_EXIT=$? if [[ -n "${NS_SETUP_DEBUG:-}" ]]; then printf " [debug] git ls-remote exit=%d output=%s\n" "${GH_SSO_EXIT}" "${GH_SSO_OUTPUT:0:120}" fi if echo "${GH_SSO_OUTPUT}" | grep -qi "saml"; then log_warn "GitHub SSO Authorization Required" log_info "Your SSH key must be authorized for the northslopetech SSO org." log_info "Go to: https://github.com/settings/keys" log_info "Click 'Configure SSO' next to your key → authorize 'northslopetech'." echo "" printf " → Press Enter when complete... "; read -r _ elif [[ ${GH_SSO_EXIT} -eq 0 ]]; then log_ok "GitHub SSO authorized" break elif echo "${GH_SSO_OUTPUT}" | grep -qi "permission denied"; then log_info "SSH access failed — setting up northslope SSH key and config..." if [[ ! -f "${NORTHSLOPE_SSH_KEY_PATH}" ]]; then mkdir -p "${HOME}/.ssh" && chmod 700 "${HOME}/.ssh" ssh-keygen -t ed25519 -f "${NORTHSLOPE_SSH_KEY_PATH}" -N "" -C "northslope" fi SSH_CONFIG="${HOME}/.ssh/config" if grep -qE "^Host[[:space:]]+github\.com" "${SSH_CONFIG}" 2>/dev/null && ! grep -q "id_ed25519_northslope" "${SSH_CONFIG}" 2>/dev/null; then log_error "~/.ssh/config already has a Host github.com stanza without id_ed25519_northslope." log_info "Add the following lines to that block and re-run setup:" log_info " IdentityFile ~/.ssh/id_ed25519_northslope" log_info " IdentitiesOnly yes" exit 1 fi if ! grep -q "id_ed25519_northslope" "${SSH_CONFIG}" 2>/dev/null || ! grep -q "IdentitiesOnly yes" "${SSH_CONFIG}" 2>/dev/null; then mkdir -p "${HOME}/.ssh" && chmod 700 "${HOME}/.ssh" { echo "" echo "Host github.com" echo " IdentityFile ~/.ssh/id_ed25519_northslope" echo " IdentitiesOnly yes" } >> "${SSH_CONFIG}" chmod 600 "${SSH_CONFIG}" log_ok "GitHub SSH config written" fi while IFS= read -r key_id; do [[ -n "${key_id}" ]] && "${GH_BIN}" ssh-key delete "${key_id}" --yes done < <("${GH_BIN}" api user/keys --jq '.[] | select(.title == "ns-machine") | .id') if ! _nslp_has_scope "write:public_key"; then "${GH_BIN}" auth refresh -h github.com --scopes write:public_key || true fi if ! "${GH_BIN}" ssh-key add "${NORTHSLOPE_SSH_KEY_PATH}.pub" --title "ns-machine"; then log_error "gh ssh-key add failed — re-run setup." exit 1 fi else log_warn "Could not verify SSO (${GH_SSO_OUTPUT}). Continuing..." break fi done #============================================================================== # GITHUB SSH KNOWN_HOSTS #============================================================================== log_section "GitHub SSH Known Hosts" mkdir -p "${HOME}/.ssh" && chmod 700 "${HOME}/.ssh" if ! ssh-keygen -F github.com > /dev/null 2>&1; then log_info "Adding GitHub SSH host key to known_hosts..." ssh-keyscan -H github.com >> "${HOME}/.ssh/known_hosts" 2>/dev/null fi log_ok "GitHub SSH known_hosts ready" #============================================================================== # NS-MACHINE-SETUP #============================================================================== log_section "ns-machine-setup" if [[ ! -d "${NS_MACHINE_SETUP_DIR}/.git" ]]; then log_info "Cloning ns-machine-setup..." git clone --quiet "${NS_MACHINE_SETUP_REPO}" "${NS_MACHINE_SETUP_DIR}" if [[ $? -ne 0 ]]; then log_error "Failed to clone ns-machine-setup. Cannot continue." exit 1 fi fi NS_MACHINE_SETUP_TARGET_VERSION=$(curl -fsSL "https://setup.northslope.dev/version.txt" 2>/dev/null | tr -d '[:space:]') if [[ -z "${NS_MACHINE_SETUP_TARGET_VERSION}" ]]; then log_error "Failed to read target version from setup.northslope.dev/version.txt. Cannot continue." exit 1 fi git -C "${NS_MACHINE_SETUP_DIR}" fetch --quiet --tags origin 2>/dev/null git -C "${NS_MACHINE_SETUP_DIR}" checkout --quiet "${NS_MACHINE_SETUP_TARGET_VERSION}" if [[ $? -ne 0 ]]; then log_error "Failed to checkout ns-machine-setup ${NS_MACHINE_SETUP_TARGET_VERSION}. Cannot continue." exit 1 fi log_ok "ns-machine-setup at ${NS_MACHINE_SETUP_TARGET_VERSION}" #============================================================================== # HAND OFF TO PYTHON INSTALLER # # Usage: # setup — re-run update for your saved profile # setup config — reconfigure your profile # setup tasks — pick and run individual tasks interactively # setup --task — run one or more specific task IDs # setup --toggle — toggle a task as user-managed in install-config.json # setup --debug [...] — enable verbose debug output (pass through to installer) #============================================================================== case "$1" in --toggle) _toggle_id="${2:-}" if [[ -z "$_toggle_id" ]]; then log_error "--toggle requires a task ID" printf " Usage: setup --toggle \n" exit 1 fi _config_path="${NORTHSLOPE_DIR}/install-config.json" if ! command -v jq &>/dev/null; then log_error "jq is required for --toggle (run 'setup' first to install it)" exit 1 fi # Bootstrap a minimal config if none exists yet if [[ ! -f "$_config_path" ]]; then printf '{\n "version": 2,\n "tasks": {}\n}\n' > "$_config_path" fi _current=$(jq -r --arg id "$_toggle_id" '.tasks[$id] // empty' "$_config_path") if [[ "$_current" == "user-managed" ]]; then jq --arg id "$_toggle_id" 'del(.tasks[$id])' "$_config_path" \ > "${_config_path}.tmp" && mv "${_config_path}.tmp" "$_config_path" log_ok "${_toggle_id} — now managed by ns-machine-setup" else jq --arg id "$_toggle_id" '.tasks[$id] = "user-managed"' "$_config_path" \ > "${_config_path}.tmp" && mv "${_config_path}.tmp" "$_config_path" log_ok "${_toggle_id} — marked user-managed (ns-machine-setup will skip it)" fi exit 0 ;; config) exec "${NS_MACHINE_SETUP_VENV}/bin/python3" "${NS_MACHINE_SETUP_DIR}/install.py" --config ;; tasks) exec "${NS_MACHINE_SETUP_VENV}/bin/python3" "${NS_MACHINE_SETUP_DIR}/install.py" --tasks ;; --task) shift exec "${NS_MACHINE_SETUP_VENV}/bin/python3" "${NS_MACHINE_SETUP_DIR}/install.py" --task "$@" ;; *) exec "${NS_MACHINE_SETUP_VENV}/bin/python3" "${NS_MACHINE_SETUP_DIR}/install.py" --update ;; esac