From 39145762a85b05da57bca8135583a237dbf80218 Mon Sep 17 00:00:00 2001 From: Gabriel Trabanco Llano <1969593+gtrabanco@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:57:46 +0000 Subject: [PATCH 1/4] fix(install): instalar en $HOME/.local/bin sin necesidad de root (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cambiar directorio de instalación por defecto a $HOME/.local/bin - Crear el directorio si no existe con mkdir -p - Añadir $HOME/.local/bin al PATH actual y a ~/.profile si no estaba --- scripts/install.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index efdd2e1..29703bd 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -4,7 +4,7 @@ set -euo pipefail # Overridable so a fork can install its own build, and so the failure paths # below can be exercised against a repo that is not there. REPO="${REPO:-helmcode/nan-cli}" -INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" VERSION="${VERSION:-}" if [ -t 1 ]; then @@ -185,7 +185,16 @@ check_path() { case ":$PATH:" in *":$dir:"*) ;; *) - warn "$dir is not in your PATH" + # Add to current session PATH + export PATH="${PATH:+${PATH}:}$dir" + # Add to .profile so it persists + if ! grep -q "$dir" "$HOME/.profile" 2>/dev/null; then + cat >> "$HOME/.profile" << 'PROFILE_EOF' +# Added by nan-cli installer +export PATH="${PATH:+${PATH}:}${HOME}/.local/bin" +PROFILE_EOF + fi + warn "$dir was not in your PATH — added it" warn "add this to your shell profile:" printf " export PATH=\"%s:\$PATH\"\n" "$dir" ;; @@ -227,6 +236,8 @@ main() { tar xz -C "$tmpdir" -f "$tmpdir/$archive" + mkdir -p "$INSTALL_DIR" + info "installing to ${INSTALL_DIR}/nan..." install_bin "$tmpdir/nan" "$INSTALL_DIR" From 221b4cde683b7ac60b49dd23d74beec73994e279 Mon Sep 17 00:00:00 2001 From: Gabriel Trabanco Llano <1969593+gtrabanco@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:52:07 +0000 Subject: [PATCH 2/4] fix(install): skip attestation check when gh doesn't support it --- scripts/install.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/install.sh b/scripts/install.sh index 29703bd..98548cb 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -137,6 +137,14 @@ verify_provenance() { return 0 fi + # "unknown command" or similar means gh is too old for attestation — skip + case "$out" in + *"unknown command"*|*"command not found"*|*"subcommand not found"*) + info "gh does not support attestation (too old), skipping provenance check" + return 0 + ;; + esac + # Nothing recorded against these bytes, which is a 404 from the attestations # API. Two different things look identical from out here: a release from # before this repo signed anything, and an archive that is not the one it From 86f2b07ae4ee5206ab3b21ce3faaab32f503eec7 Mon Sep 17 00:00:00 2001 From: Gabriel Trabanco Llano <1969593+gtrabanco@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:46:12 +0000 Subject: [PATCH 3/4] fix(install): PATH por shell, aviso de nan anterior y sin pisar perfiles ajenos (#34) El instalador escribia la linea del PATH en ~/.profile, que zsh no lee y bash ignora si existe ~/.bash_profile, y ademas la ponia al final, con lo que un nan anterior seguia ganando. La linea era fija a ${HOME}/.local/bin aunque se hubiera cambiado INSTALL_DIR, y el mensaje se contradecia. Ahora elige el fichero segun $SHELL (zsh, bash, fish u otro), antepone el directorio, no toca ningun perfil cuando INSTALL_DIR es propio, avisa si otro nan anterior lo tapa, y deja claro que hay que hacer source del perfil o abrir otra terminal porque el export de curl | bash no llega a la shell que lo lanzo. Se quita el mkdir -p redundante, se actualizan README, CONTRIBUTING, la CI y el comentario del instalador de Windows, y se anade una prueba sin red de la logica del PATH. --- .github/workflows/ci.yml | 72 +++++++++++++++++++++++++-- CONTRIBUTING.md | 4 +- README.md | 6 ++- scripts/install.ps1 | 8 +-- scripts/install.sh | 102 ++++++++++++++++++++++++++++++++------- 5 files changed, 163 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5887242..995427d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,8 +239,10 @@ jobs: - uses: actions/checkout@v4 # INSTALL_DIR inside the runner's temp, so there is no sudo prompt and - # nothing to clean up. The default path is /usr/local/bin and that is - # what the sudo fallback is for; this checks the installer, not sudo. + # nothing to clean up. The default path is now the writable per-user + # $HOME/.local/bin (no sudo required); this checks the installer, not + # sudo. The sudo fallback is for a custom INSTALL_DIR that is not + # writable, as the /opt test below exercises. # /bin/bash explicitly, and the versions on the log, so which shell this # ran under is a fact and not an assumption. macOS ships bash 3.2.57 - # frozen in 2007 by its licence - where several things that work @@ -268,9 +270,73 @@ jobs: run: | set -euo pipefail /bin/bash scripts/install.sh + # The default install dir is $HOME/.local/bin; verify the binary exists + # there and runs. $HOME/.local/bin is not on PATH yet — a fresh step + # does not source the user's profile — so we still need the file test. + test -x "$HOME/.local/bin/nan" + "$HOME/.local/bin/nan" --version + # Append $HOME/.local/bin to $GITHUB_PATH so later steps inherit it, + # mirroring what a fresh terminal would get from the profile the + # installer wrote. + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + # The default install is on PATH in a fresh shell. + - name: The default install is on PATH in a fresh shell + run: | + set -euo pipefail command -v nan nan --version + # The PATH logic is the part of this installer that runs after the + # binary is in place, and the part that was wrong: it wrote ~/.profile, + # which zsh never reads and bash skips when ~/.bash_profile exists. This + # exercises it with no download at all, by sourcing the installer and + # calling the functions directly. + - name: The PATH line lands in the right shell profile + run: | + set -euo pipefail + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + + # shell_profile() has to name the file that shell actually reads. + test "$(HOME="$tmp/h1" SHELL=/bin/zsh bash -c 'source scripts/install.sh; shell_profile')" = "$tmp/h1/.zshrc" + test "$(HOME="$tmp/h2" SHELL=/bin/bash bash -c 'source scripts/install.sh; shell_profile')" = "$tmp/h2/.bashrc" + mkdir -p "$tmp/h3"; touch "$tmp/h3/.bash_profile" + test "$(HOME="$tmp/h3" SHELL=/bin/bash bash -c 'source scripts/install.sh; shell_profile')" = "$tmp/h3/.bash_profile" + test "$(HOME="$tmp/h4" SHELL=/usr/bin/fish bash -c 'source scripts/install.sh; shell_profile')" = "$tmp/h4/.config/fish/config.fish" + test "$(HOME="$tmp/h5" SHELL=/bin/tcsh bash -c 'source scripts/install.sh; shell_profile')" = "$tmp/h5/.profile" + + # A default install prepends the directory - so a nan already earlier + # in PATH cannot keep winning - and running it twice changes nothing. + home="$tmp/default"; mkdir -p "$home" + HOME="$home" SHELL=/bin/zsh PATH=/usr/bin:/bin bash -c 'source scripts/install.sh; check_path "$HOME/.local/bin"' >/dev/null 2>&1 + grep -qF "export PATH=\"$home/.local/bin:\$PATH\"" "$home/.zshrc" + HOME="$home" SHELL=/bin/zsh PATH=/usr/bin:/bin bash -c 'source scripts/install.sh; check_path "$HOME/.local/bin"' >/dev/null 2>&1 + test "$(grep -cF "$home/.local/bin" "$home/.zshrc")" = 1 + + # fish gets fish syntax, not an export. + home="$tmp/fish"; mkdir -p "$home" + HOME="$home" SHELL=/usr/bin/fish PATH=/usr/bin:/bin bash -c 'source scripts/install.sh; check_path "$HOME/.local/bin"' >/dev/null 2>&1 + grep -qF "fish_add_path \"$home/.local/bin\"" "$home/.config/fish/config.fish" + + # A custom INSTALL_DIR is a deliberate choice: never edit a profile. + home="$tmp/custom"; mkdir -p "$home" + HOME="$home" SHELL=/bin/zsh PATH=/usr/bin:/bin bash -c 'source scripts/install.sh; check_path "$HOME/elsewhere"' >/dev/null 2>&1 + test ! -e "$home/.zshrc" + test ! -e "$home/.profile" + + # A nan earlier in PATH is what turns a fixed installer into a bug + # report, so it has to be called out. + home="$tmp/shadow"; mkdir -p "$home/shadow" + printf '#!/bin/sh\n' > "$home/shadow/nan"; chmod +x "$home/shadow/nan" + out="$(bash -c "source scripts/install.sh; warn_if_shadowed \"$home/shadow/nan\" \"$home/.local/bin\"" 2>&1)" + grep -qF "$home/shadow/nan" <<<"$out" + grep -qi shadow <<<"$out" + out="$(bash -c "source scripts/install.sh; warn_if_shadowed \"$home/.local/bin/nan\" \"$home/.local/bin\"" 2>&1)" + test -z "$out" + + echo "PATH logic verified for zsh, bash, fish and a custom dir" + # The version lookup failing has to SAY so. It did not: the pipeline runs # under `set -euo pipefail`, so a grep finding no tag_name - which is the # rate-limited case - killed the script one line before the message that @@ -300,7 +366,7 @@ jobs: # And the branch nobody had ever taken: a destination the user cannot # write to, where install_bin falls back to sudo. On a runner both - # /usr/local/bin and the temp dir are writable, so the fallback would + # $HOME/.local/bin and the temp dir are writable, so the fallback would # never fire on its own and "it installs fine" would say nothing about # it. A root-owned directory forces it. - name: Force the sudo fallback diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11d0e46..cd0b9c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,10 +25,10 @@ To install a released binary instead of building from source: curl -fsSL https://nan.builders/install | bash ``` -By default it installs to `/usr/local/bin`. Override with `INSTALL_DIR`: +By default it installs to `$HOME/.local/bin`, which is per-user and needs no root. Override with `INSTALL_DIR`: ```bash -INSTALL_DIR=~/.local/bin curl -fsSL https://nan.builders/install | bash +INSTALL_DIR=~/.nan/bin curl -fsSL https://nan.builders/install | bash ``` ## Project structure diff --git a/README.md b/README.md index e695b6a..bb18d8d 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,14 @@ CLI for [nan.builders](https://nan.builders) — manage your account, monitor us curl -fsSL https://nan.builders/install | bash ``` -By default installs to `/usr/local/bin`. Override with `INSTALL_DIR`: +By default installs to `$HOME/.local/bin`, which is per-user and needs no root. Override with `INSTALL_DIR`: ```bash -INSTALL_DIR=~/.local/bin curl -fsSL https://nan.builders/install | bash +INSTALL_DIR=~/.nan/bin curl -fsSL https://nan.builders/install | bash ``` +If `$HOME/.local/bin` is not already on your `PATH`, the installer appends it to the startup file your shell actually reads — `~/.zshrc` for zsh, `~/.bashrc` or `~/.bash_profile` for bash, `config.fish` for fish — and tells you to source that file or open a new terminal. + On Windows, from PowerShell: ```powershell diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 24e8430..e2fb14c 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -13,10 +13,10 @@ - The artifact is a .zip, not a .tar.gz. Nothing on a stock Windows unpacks a tarball by double-clicking, and Expand-Archive is built in. - The default install directory is per-user (LOCALAPPDATA\Programs\nan) and - not a machine-wide one. /usr/local/bin has a sudo prompt that a person - expects; the Windows equivalent is an elevation dialog out of a piped - script, which is worse than installing for one user. Set -InstallDir to - override. + not a machine-wide one. $HOME/.local/bin is per-user and needs no elevation; + the old convention of a machine-wide directory like /usr/local/bin would + require a sudo prompt, which is worse for a piped script than installing + for one user. Set -InstallDir to override. .PARAMETER Version A tag such as v0.1.3. Defaults to the latest release. diff --git a/scripts/install.sh b/scripts/install.sh index 98548cb..cd6ed04 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -4,7 +4,11 @@ set -euo pipefail # Overridable so a fork can install its own build, and so the failure paths # below can be exercised against a repo that is not there. REPO="${REPO:-helmcode/nan-cli}" -INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" +# The directory the one-liner uses. Kept separate from INSTALL_DIR because +# check_path only edits a shell profile for this default: a custom INSTALL_DIR +# is a deliberate choice and must not come with a silent profile edit. +DEFAULT_INSTALL_DIR="$HOME/.local/bin" +INSTALL_DIR="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" VERSION="${VERSION:-}" if [ -t 1 ]; then @@ -188,29 +192,88 @@ install_bin() { sudo install -d "$dest_dir" && sudo install -m 755 "$src" "$dest_dir/nan" } +shell_profile() { + local shell_name + shell_name="$(basename "${SHELL:-}" 2>/dev/null || echo "")" + case "$shell_name" in + zsh) + echo "$HOME/.zshrc" + ;; + bash) + if [ -f "$HOME/.bashrc" ]; then + echo "$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + echo "$HOME/.bash_profile" + else + echo "$HOME/.bashrc" + fi + ;; + fish) + echo "${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" + ;; + *) + echo "$HOME/.profile" + ;; + esac +} + check_path() { local dir="$1" case ":$PATH:" in - *":$dir:"*) ;; - *) - # Add to current session PATH - export PATH="${PATH:+${PATH}:}$dir" - # Add to .profile so it persists - if ! grep -q "$dir" "$HOME/.profile" 2>/dev/null; then - cat >> "$HOME/.profile" << 'PROFILE_EOF' -# Added by nan-cli installer -export PATH="${PATH:+${PATH}:}${HOME}/.local/bin" -PROFILE_EOF - fi - warn "$dir was not in your PATH — added it" - warn "add this to your shell profile:" - printf " export PATH=\"%s:\$PATH\"\n" "$dir" + *":$dir:"*) + info "$dir is already in PATH" + return ;; esac + + if [ "$dir" != "$DEFAULT_INSTALL_DIR" ]; then + warn "$dir is not in PATH. To add it, append this line to your shell profile:" + printf " export PATH=\"%s:\$PATH\"\n" "$dir" + warn "the installer did not modify any profile for a custom INSTALL_DIR" + return + fi + + local profile line + profile="$(shell_profile)" + line="fish_add_path \"$dir\"" + if [ "$(basename "${SHELL:-}" 2>/dev/null || echo "")" != "fish" ]; then + line="export PATH=\"$dir:\$PATH\"" + fi + if grep -qF "$dir" "$profile" 2>/dev/null; then + info "$dir is already in $profile" + return + fi + local profile_dir + profile_dir="$(dirname "$profile")" + if [ ! -d "$profile_dir" ]; then + mkdir -p "$profile_dir" 2>/dev/null || true + fi + printf '\n# Added by nan-cli installer\n%s\n' "$line" >> "$profile" + log "added $dir to PATH in $profile" + warn "this shell was not changed; run 'source $profile' or open a new terminal" +} + +warn_if_shadowed() { + local found="$1" install_dir="$2" + if [ -z "$found" ]; then + return + fi + local found_dir + found_dir="$(cd "$(dirname "$found")" 2>/dev/null && pwd)" || found_dir="" + local install_dir_resolved + install_dir_resolved="$(cd "$install_dir" 2>/dev/null && pwd)" || install_dir_resolved="" + if [ "$found_dir" = "$install_dir_resolved" ]; then + return + fi + warn "another nan at $found is earlier in PATH and will run until PATH is fixed" + if [ "$found" = "/usr/local/bin/nan" ]; then + warn "remove the old one: sudo rm /usr/local/bin/nan" + fi + warn "opening a new terminal will put $install_dir first once your profile is sourced" } main() { - local os arch version archive url checksums_url expected_checksum + local os arch version archive url checksums_url expected_checksum existing_nan os="$(detect_os)" arch="$(detect_arch)" @@ -244,13 +307,16 @@ main() { tar xz -C "$tmpdir" -f "$tmpdir/$archive" - mkdir -p "$INSTALL_DIR" + existing_nan="$(type -P nan 2>/dev/null || true)" info "installing to ${INSTALL_DIR}/nan..." install_bin "$tmpdir/nan" "$INSTALL_DIR" log "installed ${BOLD}${version}${RESET} to ${INSTALL_DIR}/nan" + warn_if_shadowed "$existing_nan" "$INSTALL_DIR" check_path "$INSTALL_DIR" } -main "$@" +if [ -z "${BASH_SOURCE[0]:-}" ] || [ "${BASH_SOURCE[0]}" = "$0" ]; then + main "$@" +fi From 05260ed2ea38f0b9372aae8e9207299b4ed30701 Mon Sep 17 00:00:00 2001 From: Gabriel Trabanco Llano <1969593+gtrabanco@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:39:24 +0000 Subject: [PATCH 4/4] docs(install): documentar la instalacion de sistema en /usr/local/bin --- CONTRIBUTING.md | 6 ++++++ README.md | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd0b9c7..724d9de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,12 @@ By default it installs to `$HOME/.local/bin`, which is per-user and needs no roo INSTALL_DIR=~/.nan/bin curl -fsSL https://nan.builders/install | bash ``` +A custom `INSTALL_DIR` never edits a shell profile: if the directory is not on your `PATH`, the installer prints the line to add yourself. For a machine-wide install, with root or sudo: + +```bash +curl -fsSL https://nan.builders/install | sudo INSTALL_DIR=/usr/local/bin bash +``` + ## Project structure ``` diff --git a/README.md b/README.md index bb18d8d..95689d4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,14 @@ INSTALL_DIR=~/.nan/bin curl -fsSL https://nan.builders/install | bash If `$HOME/.local/bin` is not already on your `PATH`, the installer appends it to the startup file your shell actually reads — `~/.zshrc` for zsh, `~/.bashrc` or `~/.bash_profile` for bash, `config.fish` for fish — and tells you to source that file or open a new terminal. +A custom `INSTALL_DIR` is a deliberate choice, so the installer never edits a shell profile for it; if the directory is not on your `PATH`, it prints the line to add yourself. To make `nan` available to every user on the machine, pass the system-wide directory (needs root or sudo): + +```bash +curl -fsSL https://nan.builders/install | sudo INSTALL_DIR=/usr/local/bin bash +``` + +`/usr/local/bin` is already on root's `PATH`, so nothing else is needed. + On Windows, from PowerShell: ```powershell