Skip to content

Commit 4e21eaf

Browse files
Stop re-fixing already-installed devcontainer CLI and Claude Code every run
The devcontainer and Claude Code checks verified against the live PATH only (have_cmd), unlike the env-var checks which read the persisted dotfile. Under curl … | sh the fix persists the PATH line to ~/.zprofile / ~/.bash_profile but cannot mutate the parent shell, so a later run's fresh sh still lacks the dir on PATH — the check reported "not on PATH" and re-ran its fix on every invocation. - Add path_line_persisted(), the read counterpart to persist_path_line, that greps both login files for a persisted PATH export - Hoist the persisted PATH export to DEVCONTAINER_PATH_LINE / CLAUDE_PATH_LINE constants shared by the check (grep) and the fix (append) so they can't drift, matching the existing GITHUB_TOKEN_LINE pattern - check_devcontainer / check_claude now resolve the binary once and pass when it is on the live PATH, or installed at its known dir with its PATH line already persisted (noting "open a new terminal to use it"); still fail when installed without a persisted line or not installed, so the fix can repair PATH or install - Document the check-side gotcha in .claude/rules/setup-sh-checks.md Co-Authored-By: Claude Code <bot+claudecode@lumivero.com>
1 parent 3bd042d commit 4e21eaf

2 files changed

Lines changed: 102 additions & 45 deletions

File tree

.claude/rules/setup-sh-checks.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ The script is served from GitHub Pages and run as `curl -fsSL … | sh`:
9090
- **`check_git`/`fix_git`** — minimal template in the `main()` comment block.
9191
- **`check_docker`/`fix_docker`** — installed *and* daemon-reachable; per-OS
9292
detail strings; Linux-only auto-install via official convenience script.
93-
- **`check_devcontainer`/`fix_devcontainer`** — installs a CLI and repairs PATH.
93+
- **`check_devcontainer`/`fix_devcontainer`** — installs a CLI and repairs PATH;
94+
the check passes on the live PATH *or* on "installed at its known dir + PATH
95+
line persisted" (see `path_line_persisted`), so a fixed install is not
96+
re-fixed every run. `check_claude`/`fix_claude` follow the same shape.
9497

9598
## Gotchas (learned the hard way)
9699

@@ -99,6 +102,16 @@ The script is served from GitHub Pages and run as `curl -fsSL … | sh`:
99102
for future sessions, and (b) `export PATH=…` in the current process so the
100103
re-check passes now. Do **not** use `source ~/.zprofile``source` is a bashism
101104
and sourcing a startup file under non-interactive `sh` is unreliable.
105+
- **A `check_` for a persisted tool must not depend on the *live* PATH only.**
106+
Same root cause as above, but for the *check*: a later `curl … | sh` is a
107+
fresh `sh` whose PATH comes from the parent shell, which may not have sourced
108+
the file the fix wrote — so `have_cmd` stays false and the fix re-runs *every
109+
run*. Mirror the env-var checks (which grep the dotfile, not the live env):
110+
treat "binary at its known install dir **and** its PATH line present in the
111+
startup files" as satisfied. Use `path_line_persisted "$THE_LINE"`, and share
112+
one constant for the persisted line between the check and the fix (as
113+
`DEVCONTAINER_PATH_LINE` / `CLAUDE_PATH_LINE` / `GITHUB_TOKEN_LINE` do) so the
114+
grep and the append can never drift.
102115
- **Put each export in the file its kind belongs in**, per shell convention:
103116
regular env vars via `persist_env_line` (zsh `~/.zshenv`, bash `~/.bash_profile`),
104117
PATH edits via `persist_path_line` (zsh `~/.zprofile`, bash `~/.bash_profile`).

lumivero-api_setup.sh

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,23 @@ persist_path_line() {
277277
ensure_line_in_file "${HOME}/.bash_profile" "$1"
278278
}
279279

280+
# True when <line> (a PATH export persisted by persist_path_line) is already
281+
# present in both login files it writes — zsh ~/.zprofile and bash
282+
# ~/.bash_profile. This is the read counterpart of persist_path_line and the
283+
# same file-as-source-of-truth test the env-var checks use (check_github_token
284+
# et al. grep the dotfile rather than the live environment): once the line is
285+
# persisted, a fresh login shell puts the directory on PATH, so a check can
286+
# treat a tool installed at its known location as satisfied even when the
287+
# current `curl … | sh` process — which cannot see edits to the parent shell's
288+
# startup files — does not yet have it on PATH. Without this, such a check would
289+
# report "not on PATH" and re-run its fix on every invocation.
290+
path_line_persisted() {
291+
for _rc in "${HOME}/.zprofile" "${HOME}/.bash_profile"; do
292+
grep -qF "$1" "$_rc" 2>/dev/null || return 1
293+
done
294+
return 0
295+
}
296+
280297
# Install a package by Homebrew formula (macOS) or apt package (Debian/Ubuntu).
281298
# Usage: install_pkg <brew-formula> <apt-package>
282299
# Sets CHECK_DETAIL and returns non-zero when it cannot install.
@@ -434,23 +451,40 @@ fix_docker() {
434451
# binary can therefore exist there before that directory is on PATH.
435452
DEVCONTAINER_BIN_DIR="${HOME}/.devcontainers/bin"
436453

437-
# devcontainer CLI — installed and runnable from PATH. We treat "on PATH and
438-
# executes" as the bar; a binary that exists in the default install dir but is
439-
# not yet on PATH is reported separately so the fix knows to repair PATH only.
454+
# The PATH line the fix persists for the devcontainer CLI. A single constant so
455+
# the check (path_line_persisted) and the fix (persist_path_line) test and write
456+
# the exact same string — the same single-source-of-truth pattern as
457+
# GITHUB_TOKEN_LINE. Literal $HOME/$PATH so the startup shell expands them later,
458+
# not this script now (hence the SC2016 disable).
459+
# shellcheck disable=SC2016
460+
DEVCONTAINER_PATH_LINE='export PATH="$HOME/.devcontainers/bin:$PATH"'
461+
462+
# devcontainer CLI — installed and runnable. Satisfied when it is on the live
463+
# PATH, or when it is installed at its default location and that directory's
464+
# PATH line is already persisted to the startup files (a fresh login shell will
465+
# then have it on PATH — see path_line_persisted). The latter is why a fixed
466+
# install is not re-fixed on every run: this `curl … | sh` process cannot see
467+
# PATH edits made to the parent shell's startup files, so have_cmd alone would
468+
# keep reporting "not on PATH". A binary present but with no persisted PATH line
469+
# is reported separately so the fix knows to repair PATH only.
440470
check_devcontainer() {
471+
_new_term=""
441472
if have_cmd devcontainer; then
442-
_ver="$(devcontainer --version 2>/dev/null)" || _ver=""
443-
CHECK_DETAIL="${_ver:-installed}"
444-
return 0
445-
fi
446-
447-
if [ -x "${DEVCONTAINER_BIN_DIR}/devcontainer" ]; then
473+
_dc="devcontainer"
474+
elif [ -x "${DEVCONTAINER_BIN_DIR}/devcontainer" ] && path_line_persisted "$DEVCONTAINER_PATH_LINE"; then
475+
_dc="${DEVCONTAINER_BIN_DIR}/devcontainer"
476+
_new_term=" (open a new terminal to use it)"
477+
elif [ -x "${DEVCONTAINER_BIN_DIR}/devcontainer" ]; then
448478
CHECK_DETAIL="installed but ${DEVCONTAINER_BIN_DIR} is not on PATH"
449479
return 1
480+
else
481+
CHECK_DETAIL="not installed"
482+
return 1
450483
fi
451484

452-
CHECK_DETAIL="not installed"
453-
return 1
485+
_ver="$("$_dc" --version 2>/dev/null)" || _ver=""
486+
CHECK_DETAIL="${_ver:-installed}${_new_term}"
487+
return 0
454488
}
455489

456490
# Install the devcontainer CLI via the official script when missing, then put
@@ -464,10 +498,7 @@ fix_devcontainer() {
464498
fi
465499
fi
466500

467-
# Literal $HOME/$PATH so the startup shell expands them later, not now.
468-
# shellcheck disable=SC2016
469-
_line='export PATH="$HOME/.devcontainers/bin:$PATH"'
470-
persist_path_line "$_line"
501+
persist_path_line "$DEVCONTAINER_PATH_LINE"
471502

472503
case ":${PATH}:" in
473504
*":${DEVCONTAINER_BIN_DIR}:"*) ;;
@@ -730,38 +761,54 @@ fix_checksum_secret() {
730761
# devcontainer CLI above).
731762
CLAUDE_BIN_DIR="${HOME}/.local/bin"
732763

733-
# Claude Code (the `claude` CLI) — installed, on PATH, signed in, and current.
734-
# Login is probed non-interactively with `claude auth status --json`, which
735-
# reports `"loggedIn": true` for a usable session; the interactive
764+
# The PATH line the fix persists for Claude Code — the single source of truth
765+
# shared by the check (path_line_persisted) and the fix (persist_path_line), as
766+
# with DEVCONTAINER_PATH_LINE above. Literal $HOME/$PATH for the startup shell
767+
# (SC2016).
768+
# shellcheck disable=SC2016
769+
CLAUDE_PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
770+
771+
# Claude Code (the `claude` CLI) — installed, reachable, signed in, and current.
772+
# It is "reachable" when on the live PATH, or installed at its default location
773+
# with its PATH line already persisted (a fresh login shell will then find it —
774+
# see path_line_persisted); that second case is what stops a fixed install from
775+
# being re-fixed every run, since this `curl … | sh` process can't see PATH
776+
# edits to the parent shell's startup files. The binary is resolved once (by its
777+
# absolute path when only persisted, else by name) and the same checks run on
778+
# it: login is probed non-interactively with `claude auth status --json`, which
779+
# reports `"loggedIn": true` for a usable session — the interactive
736780
# `claude auth login` lives in the fix, never here, so a check never blocks
737-
# waiting for a browser. When installed and signed in, the check also runs
738-
# `claude update` to keep the install current on every run — best-effort, so a
739-
# failed or already-current update never flips a healthy check to failing
740-
# ("up to date" is reported only when the update actually succeeds). A binary
741-
# present in the default install dir but not yet on PATH is reported separately
742-
# so the fix knows to repair PATH only.
781+
# waiting for a browser. When signed in, the check also runs `claude update` to
782+
# keep the install current — best-effort, so a failed or already-current update
783+
# never flips a healthy check to failing ("up to date" is reported only when the
784+
# update actually succeeds). A binary present with no persisted PATH line is
785+
# reported separately so the fix knows to repair PATH only.
743786
check_claude() {
787+
_new_term=""
744788
if have_cmd claude; then
745-
if claude auth status --json 2>/dev/null | grep -qE '"loggedIn"[[:space:]]*:[[:space:]]*true'; then
746-
if claude update >/dev/null 2>&1; then
747-
_upd=", up to date"
748-
else
749-
_upd=""
750-
fi
751-
_ver="$(claude --version 2>/dev/null | cut -d' ' -f1)" || _ver=""
752-
CHECK_DETAIL="${_ver:+v${_ver}, }logged in${_upd}"
753-
return 0
754-
fi
755-
CHECK_DETAIL="installed but not logged in — run 'claude auth login'"
756-
return 1
757-
fi
758-
759-
if [ -x "${CLAUDE_BIN_DIR}/claude" ]; then
789+
_claude="claude"
790+
elif [ -x "${CLAUDE_BIN_DIR}/claude" ] && path_line_persisted "$CLAUDE_PATH_LINE"; then
791+
_claude="${CLAUDE_BIN_DIR}/claude"
792+
_new_term=" (open a new terminal to use it)"
793+
elif [ -x "${CLAUDE_BIN_DIR}/claude" ]; then
760794
CHECK_DETAIL="installed but ${CLAUDE_BIN_DIR} is not on PATH"
761795
return 1
796+
else
797+
CHECK_DETAIL="not installed"
798+
return 1
762799
fi
763800

764-
CHECK_DETAIL="not installed"
801+
if "$_claude" auth status --json 2>/dev/null | grep -qE '"loggedIn"[[:space:]]*:[[:space:]]*true'; then
802+
if "$_claude" update >/dev/null 2>&1; then
803+
_upd=", up to date"
804+
else
805+
_upd=""
806+
fi
807+
_ver="$("$_claude" --version 2>/dev/null | cut -d' ' -f1)" || _ver=""
808+
CHECK_DETAIL="${_ver:+v${_ver}, }logged in${_upd}${_new_term}"
809+
return 0
810+
fi
811+
CHECK_DETAIL="installed but not logged in — run 'claude auth login'"
765812
return 1
766813
}
767814

@@ -778,10 +825,7 @@ fix_claude() {
778825
fi
779826
fi
780827

781-
# Literal $HOME/$PATH so the startup shell expands them later, not now.
782-
# shellcheck disable=SC2016
783-
_line='export PATH="$HOME/.local/bin:$PATH"'
784-
persist_path_line "$_line"
828+
persist_path_line "$CLAUDE_PATH_LINE"
785829

786830
case ":${PATH}:" in
787831
*":${CLAUDE_BIN_DIR}:"*) ;;

0 commit comments

Comments
 (0)