From 94ecf2f8285fba6facd5e6147f83f7c247ba1312 Mon Sep 17 00:00:00 2001 From: Asad Iqbal Date: Mon, 8 Jun 2026 12:54:36 +0500 Subject: [PATCH 1/4] fix(install.sh): persist user-local PREFIX even when on PATH; tighten rc idempotency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Cursor Bugbot findings on #61's PATH-persistence block, both of which let the installer print a false "Added ... to your PATH" while a new shell still couldn't find the binary — the exact failure #61 set out to kill. 1. Loose idempotency: `grep -F "$PREFIX"` matched the directory anywhere in the rc, including a bare comment, so a stray mention set added=1 with no export written. Now strip comment lines, keep only real PATH=/fish_add_path lines, then fixed-match the prefix. 2. Session PATH skipped the rc: the outer `case ":$PATH:"` skipped rc persistence whenever $PREFIX was on PATH for the current shell, so a one-off `export` (which won't survive a new terminal) left the rc untouched. Branch on the install location instead — a user-local prefix (under $HOME) always persists; a system prefix (e.g. /usr/local/bin) is already on PATH for every shell and only gets a nudge if it isn't. Validated: sh -n / bash -n / dash -n, shellcheck clean, and a functional matrix over the extracted block (fresh, idempotent re-run, comment-only, unrelated PYTHONPATH= line, system-prefix nudge, session-on-PATH persist, plus zsh/fish/macOS-bash routing) — 9/9. Refs #61 Co-Authored-By: Claude Opus 4.8 --- scripts/install.sh | 52 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 2c87f82..a8eef24 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -259,16 +259,22 @@ echo "Verify with:" echo " $PREFIX/$BINARY_NAME version" echo "" -# PATH handling for the fallback case. install.ps1 persists the PATH -# entry on Windows (SetEnvironmentVariable, User scope) — do the same on -# Unix by writing to the rc file the user's shell actually reads. The old -# print-only advice silently failed on Ubuntu: ~/.profile adds -# ~/.local/bin only at *login* and only if it already existed, but the -# installer creates it mid-session, so a new (non-login) terminal reading -# ~/.bashrc never picks it up. -case ":$PATH:" in - *":$PREFIX:"*) ;; # already on PATH — nothing to do - *) +# PATH handling. install.ps1 persists the PATH entry on Windows +# (SetEnvironmentVariable, User scope) — do the same on Unix by writing to +# the rc file the user's shell actually reads. The old print-only advice +# silently failed on Ubuntu: ~/.profile adds ~/.local/bin only at *login* +# and only if it already existed, but the installer creates it mid-session, +# so a new (non-login) terminal reading ~/.bashrc never picks it up. +# +# Branch on the install location, NOT the current session's $PATH. A +# user-local prefix (the unprivileged `curl | sh` fallback, under $HOME) +# always wants a persisted rc entry — even when a one-off `export` already +# put it on $PATH for *this* shell, because that won't survive into a new +# terminal. A system prefix (e.g. /usr/local/bin) is already on PATH for +# every shell and can't be persisted via a per-user rc, so we only nudge if +# it somehow isn't on PATH. +case "$PREFIX" in + "$HOME"/*) shell_name="$(basename "${SHELL:-sh}")" case "$shell_name" in zsh) rc="$HOME/.zshrc" ;; @@ -289,8 +295,17 @@ case ":$PATH:" in added=0 mkdir -p "$(dirname "$rc")" 2>/dev/null || true - if grep -qsF "$PREFIX" "$rc" 2>/dev/null; then - added=1 # rc already references it — leave it alone (idempotent) + # Idempotency: only an actual, non-comment PATH-setting line that + # references $PREFIX counts as "already configured". A bare comment + # or an unrelated line that merely mentions the directory must NOT + # pass — otherwise we'd print a false "Added to PATH" while a new + # shell still can't find the binary (the very failure #61 fixes). + # Strip comment lines, keep real PATH=/fish_add_path lines, then + # fixed-match the prefix. + if grep -v '^[[:space:]]*#' "$rc" 2>/dev/null \ + | grep -E '(^|[^A-Za-z_])PATH=|fish_add_path' \ + | grep -qF "$PREFIX"; then + added=1 # rc already persists it — leave it alone (idempotent) # Group the append so the redirection-open error (e.g. an # existing but read-only rc, or an unwritable parent dir) is # suppressed too: `cmd >> "$rc" 2>/dev/null` leaks the shell's @@ -313,6 +328,19 @@ case ":$PATH:" in fi echo "" ;; + *) + # System prefix — already on PATH for every shell out of the box, so + # normally nothing to do. Only nudge if it somehow isn't on PATH. + case ":$PATH:" in + *":$PREFIX:"*) ;; + *) + echo "" + echo "Note: $PREFIX is not on \$PATH. Add it, then open a new terminal:" + echo " export PATH=\"$PREFIX:\$PATH\"" + echo "" + ;; + esac + ;; esac echo "First steps:" From 31fe5c98e20e71510b1e074cb3169c17a579ace0 Mon Sep 17 00:00:00 2001 From: Asad Iqbal Date: Mon, 8 Jun 2026 13:05:35 +0500 Subject: [PATCH 2/4] fix(install.sh): tolerate a trailing slash in $HOME for the user-local match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugbot on #65: `case "$PREFIX" in "$HOME"/*)` misclassifies a user-local prefix as a system one when $HOME ends in a slash (e.g. HOME=/home/u/ with --prefix /home/u/.local/bin -> pattern /home/u//* never matches the single- slash path), so rc persistence is skipped. Normalize with home_dir="${HOME%/}" (POSIX suffix strip) before matching. Functional matrix extended with a trailing-slash-HOME case — 10/10; sh/bash/ dash -n + shellcheck clean. Co-Authored-By: Claude Opus 4.8 --- scripts/install.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index a8eef24..341d3f3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -272,9 +272,12 @@ echo "" # put it on $PATH for *this* shell, because that won't survive into a new # terminal. A system prefix (e.g. /usr/local/bin) is already on PATH for # every shell and can't be persisted via a per-user rc, so we only nudge if -# it somehow isn't on PATH. +# it somehow isn't on PATH. ($HOME is stripped of a trailing slash first, so +# a HOME like "/home/u/" can't misclassify a user-local prefix as a system +# one via a "/home/u//*" pattern that the single-slash path won't match.) +home_dir="${HOME%/}" case "$PREFIX" in - "$HOME"/*) + "$home_dir"/*) shell_name="$(basename "${SHELL:-sh}")" case "$shell_name" in zsh) rc="$HOME/.zshrc" ;; From 28a1d3e32e066c84b28a2d4fbd98e3f875952013 Mon Sep 17 00:00:00 2001 From: Asad Iqbal Date: Mon, 8 Jun 2026 13:20:38 +0500 Subject: [PATCH 3/4] fix(install.sh): accurate present/added/failed messaging; recognize PATH+= and zsh path+=() Two more Bugbot findings on #65: - The user-local branch's failure message claimed "$PREFIX is not on $PATH", which is wrong now that the branch runs regardless of session PATH (a one-off export could already include it). Replaced the added/else pair with explicit present/added/failed states so each message is accurate; the failed case no longer asserts PATH state. - The idempotency filter only matched PATH=/fish_add_path, missing the PATH+= append idiom (and zsh's path+=() array), so a manual entry of that form wasn't recognized and a re-run could append a duplicate block. Broadened to case-insensitive (^|[^A-Za-z_])path[+]?= (still excludes PYTHONPATH=/MYPATH=). Functional matrix now 13/13 (adds PATH+=, zsh path+=(), persist-failure message); sh/bash/dash -n + shellcheck clean. Co-Authored-By: Claude Opus 4.8 --- scripts/install.sh | 58 +++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 341d3f3..fee1977 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -296,39 +296,45 @@ case "$PREFIX" in path_line="export PATH=\"$PREFIX:\$PATH\"" fi - added=0 + # Track three outcomes precisely so the message can neither over- nor + # under-claim: already configured / freshly added / couldn't write. + state=failed mkdir -p "$(dirname "$rc")" 2>/dev/null || true - # Idempotency: only an actual, non-comment PATH-setting line that - # references $PREFIX counts as "already configured". A bare comment - # or an unrelated line that merely mentions the directory must NOT - # pass — otherwise we'd print a false "Added to PATH" while a new - # shell still can't find the binary (the very failure #61 fixes). - # Strip comment lines, keep real PATH=/fish_add_path lines, then - # fixed-match the prefix. + # Idempotency: only an actual, non-comment PATH op that references + # $PREFIX counts as "already configured" — a bare comment or an + # unrelated line that merely mentions the dir must NOT pass, or we'd + # claim success while a new shell still can't find the binary (#61). + # Match PATH= / PATH+= / fish_add_path / zsh's path+=() (case- + # insensitive); the [^A-Za-z_] guard keeps PYTHONPATH=/MYPATH= out. if grep -v '^[[:space:]]*#' "$rc" 2>/dev/null \ - | grep -E '(^|[^A-Za-z_])PATH=|fish_add_path' \ + | grep -iE '(^|[^A-Za-z_])path[+]?=|fish_add_path' \ | grep -qF "$PREFIX"; then - added=1 # rc already persists it — leave it alone (idempotent) - # Group the append so the redirection-open error (e.g. an - # existing but read-only rc, or an unwritable parent dir) is - # suppressed too: `cmd >> "$rc" 2>/dev/null` leaks the shell's - # "Permission denied" because the >> open is attempted before - # 2>/dev/null applies. Wrapping in { ... } 2>/dev/null puts the - # stderr redirect in scope first, so the fallback message below - # is the only thing the user sees. + state=present # rc already persists it — leave it alone + # Group the append so the redirection-open error (e.g. a read-only + # rc, or an unwritable parent dir) is suppressed too: `cmd >> "$rc" + # 2>/dev/null` leaks the shell's "Permission denied" because the >> + # open is attempted before 2>/dev/null applies. Wrapping in { ... } + # 2>/dev/null puts the stderr redirect in scope first. elif { printf '\n# Added by the tracebloc CLI installer\n%s\n' "$path_line" >> "$rc"; } 2>/dev/null; then - added=1 + state=added fi echo "" - if [ "$added" = "1" ]; then - echo "Added $PREFIX to your PATH in $rc." - echo "Open a new terminal — or load it now: . \"$rc\"" - else - echo "Note: $PREFIX is not on \$PATH and the installer couldn't update your shell config." - echo "Add this line, then open a new terminal:" - echo " $path_line" - fi + case "$state" in + added) + echo "Added $PREFIX to your PATH in $rc." + echo "Open a new terminal — or load it now: . \"$rc\"" + ;; + present) + echo "$PREFIX is already in your PATH config ($rc) — nothing to add." + echo "If a new terminal can't find it yet, open one — or load it now: . \"$rc\"" + ;; + *) + echo "Note: the installer couldn't update your shell config ($rc)." + echo "Add this line to it (or your shell's startup file), then open a new terminal:" + echo " $path_line" + ;; + esac echo "" ;; *) From 163fc784706b7398cd867a0047994785ee6143cf Mon Sep 17 00:00:00 2001 From: Asad Iqbal Date: Mon, 8 Jun 2026 13:31:02 +0500 Subject: [PATCH 4/4] fix(install.sh): persist any off-PATH prefix to rc, not just $HOME-local ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugbot on #65: keying rc persistence on `case "$PREFIX" in "$home_dir"/*)` dropped writable non-$HOME targets (e.g. `--prefix /opt/tracebloc`) into the system branch, which only nudges — so a custom off-PATH install no longer persisted and a new terminal couldn't find tracebloc. cli#61's original code persisted ANY off-PATH prefix. Compute a `persist` decision instead: yes when the prefix is under $HOME (always — survives a new terminal even if a session export already has it) or when it's a non-$HOME prefix not on the current $PATH (e.g. /opt/...); no for a non-$HOME prefix already on PATH (the default /usr/local/bin needs nothing). One rc-writing block then handles every persist case. Functional matrix 14/14 (adds off-PATH /opt prefix persists; default system prefix on PATH stays silent); sh/bash/dash -n + shellcheck clean. Co-Authored-By: Claude Opus 4.8 --- scripts/install.sh | 152 +++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 80 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index fee1977..1af70f5 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -266,91 +266,83 @@ echo "" # and only if it already existed, but the installer creates it mid-session, # so a new (non-login) terminal reading ~/.bashrc never picks it up. # -# Branch on the install location, NOT the current session's $PATH. A -# user-local prefix (the unprivileged `curl | sh` fallback, under $HOME) -# always wants a persisted rc entry — even when a one-off `export` already -# put it on $PATH for *this* shell, because that won't survive into a new -# terminal. A system prefix (e.g. /usr/local/bin) is already on PATH for -# every shell and can't be persisted via a per-user rc, so we only nudge if -# it somehow isn't on PATH. ($HOME is stripped of a trailing slash first, so -# a HOME like "/home/u/" can't misclassify a user-local prefix as a system -# one via a "/home/u//*" pattern that the single-slash path won't match.) +# Decide whether to persist a PATH entry to the user's shell rc: +# - a user-local prefix (under $HOME — the unprivileged `curl | sh` fallback) +# ALWAYS needs one, even when a one-off `export` already put it on $PATH for +# this shell (that won't survive into a new terminal); +# - a non-$HOME prefix that ISN'T on $PATH (e.g. `--prefix /opt/tracebloc`) +# also needs one; +# - a non-$HOME prefix already on $PATH (e.g. the default /usr/local/bin) is +# on PATH for every shell and needs nothing. +# ($HOME is stripped of a trailing slash first so a HOME like "/home/u/" can't +# misclassify "/home/u/.local/bin" via a "/home/u//*" pattern it won't match.) home_dir="${HOME%/}" +persist=no case "$PREFIX" in - "$home_dir"/*) - shell_name="$(basename "${SHELL:-sh}")" - case "$shell_name" in - zsh) rc="$HOME/.zshrc" ;; - bash) - # macOS Terminal opens a login shell (reads .bash_profile); - # Linux terminals are interactive non-login (read .bashrc). - if [ "$OS" = "darwin" ]; then rc="$HOME/.bash_profile"; else rc="$HOME/.bashrc"; fi - ;; - fish) rc="$HOME/.config/fish/config.fish" ;; - *) rc="$HOME/.profile" ;; - esac + "$home_dir"/*) persist=yes ;; + *) case ":$PATH:" in *":$PREFIX:"*) ;; *) persist=yes ;; esac ;; +esac - if [ "$shell_name" = "fish" ]; then - path_line="fish_add_path $PREFIX" - else - path_line="export PATH=\"$PREFIX:\$PATH\"" - fi +if [ "$persist" = "yes" ]; then + shell_name="$(basename "${SHELL:-sh}")" + case "$shell_name" in + zsh) rc="$HOME/.zshrc" ;; + bash) + # macOS Terminal opens a login shell (reads .bash_profile); + # Linux terminals are interactive non-login (read .bashrc). + if [ "$OS" = "darwin" ]; then rc="$HOME/.bash_profile"; else rc="$HOME/.bashrc"; fi + ;; + fish) rc="$HOME/.config/fish/config.fish" ;; + *) rc="$HOME/.profile" ;; + esac - # Track three outcomes precisely so the message can neither over- nor - # under-claim: already configured / freshly added / couldn't write. - state=failed - mkdir -p "$(dirname "$rc")" 2>/dev/null || true - # Idempotency: only an actual, non-comment PATH op that references - # $PREFIX counts as "already configured" — a bare comment or an - # unrelated line that merely mentions the dir must NOT pass, or we'd - # claim success while a new shell still can't find the binary (#61). - # Match PATH= / PATH+= / fish_add_path / zsh's path+=() (case- - # insensitive); the [^A-Za-z_] guard keeps PYTHONPATH=/MYPATH= out. - if grep -v '^[[:space:]]*#' "$rc" 2>/dev/null \ - | grep -iE '(^|[^A-Za-z_])path[+]?=|fish_add_path' \ - | grep -qF "$PREFIX"; then - state=present # rc already persists it — leave it alone - # Group the append so the redirection-open error (e.g. a read-only - # rc, or an unwritable parent dir) is suppressed too: `cmd >> "$rc" - # 2>/dev/null` leaks the shell's "Permission denied" because the >> - # open is attempted before 2>/dev/null applies. Wrapping in { ... } - # 2>/dev/null puts the stderr redirect in scope first. - elif { printf '\n# Added by the tracebloc CLI installer\n%s\n' "$path_line" >> "$rc"; } 2>/dev/null; then - state=added - fi + if [ "$shell_name" = "fish" ]; then + path_line="fish_add_path $PREFIX" + else + path_line="export PATH=\"$PREFIX:\$PATH\"" + fi - echo "" - case "$state" in - added) - echo "Added $PREFIX to your PATH in $rc." - echo "Open a new terminal — or load it now: . \"$rc\"" - ;; - present) - echo "$PREFIX is already in your PATH config ($rc) — nothing to add." - echo "If a new terminal can't find it yet, open one — or load it now: . \"$rc\"" - ;; - *) - echo "Note: the installer couldn't update your shell config ($rc)." - echo "Add this line to it (or your shell's startup file), then open a new terminal:" - echo " $path_line" - ;; - esac - echo "" - ;; - *) - # System prefix — already on PATH for every shell out of the box, so - # normally nothing to do. Only nudge if it somehow isn't on PATH. - case ":$PATH:" in - *":$PREFIX:"*) ;; - *) - echo "" - echo "Note: $PREFIX is not on \$PATH. Add it, then open a new terminal:" - echo " export PATH=\"$PREFIX:\$PATH\"" - echo "" - ;; - esac - ;; -esac + # Track three outcomes precisely so the message can neither over- nor + # under-claim: already configured / freshly added / couldn't write. + state=failed + mkdir -p "$(dirname "$rc")" 2>/dev/null || true + # Idempotency: only an actual, non-comment PATH op that references $PREFIX + # counts as "already configured" — a bare comment or an unrelated line that + # merely mentions the dir must NOT pass, or we'd claim success while a new + # shell still can't find the binary (#61). Match PATH= / PATH+= / + # fish_add_path / zsh's path+=() (case-insensitive); the [^A-Za-z_] guard + # keeps PYTHONPATH=/MYPATH= out. + if grep -v '^[[:space:]]*#' "$rc" 2>/dev/null \ + | grep -iE '(^|[^A-Za-z_])path[+]?=|fish_add_path' \ + | grep -qF "$PREFIX"; then + state=present # rc already persists it — leave it alone + # Group the append so the redirection-open error (e.g. a read-only rc, or + # an unwritable parent dir) is suppressed too: `cmd >> "$rc" 2>/dev/null` + # leaks the shell's "Permission denied" because the >> open is attempted + # before 2>/dev/null applies. Wrapping in { ... } 2>/dev/null puts the + # stderr redirect in scope first. + elif { printf '\n# Added by the tracebloc CLI installer\n%s\n' "$path_line" >> "$rc"; } 2>/dev/null; then + state=added + fi + + echo "" + case "$state" in + added) + echo "Added $PREFIX to your PATH in $rc." + echo "Open a new terminal — or load it now: . \"$rc\"" + ;; + present) + echo "$PREFIX is already in your PATH config ($rc) — nothing to add." + echo "If a new terminal can't find it yet, open one — or load it now: . \"$rc\"" + ;; + *) + echo "Note: the installer couldn't update your shell config ($rc)." + echo "Add this line to it (or your shell's startup file), then open a new terminal:" + echo " $path_line" + ;; + esac + echo "" +fi echo "First steps:" echo " $BINARY_NAME cluster info # confirm CLI can reach your cluster"