mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 04:35:58 -10:00
fix: harden installer PATH/bootstrap wiring + warm update-check cache
Addresses reviewer feedback on PR #2954: - install.sh: fail loudly if uv bootstrap download is empty/failed and verify uv is on PATH afterwards (no more masked "uv: command not found"). - install.sh (bash): also wire the applicable login profile (.bash_profile/.bash_login/.profile) so login shells find praisonai. - install.sh (fish): guard fish_add_path with a POSIX-safe fallback for older fish versions. - self_manage/app: schedule a detached, non-blocking background refresh of the update-check cache on start-up so the "update available" hint is actually populated (never performs network I/O in the caller, never blocks/raises). Co-authored-by: Mervin Praison <[email protected]>
This commit is contained in:
1 parent
a6ed37645c
commit
8abfb92573
3 files changed
+140
-22
No files matched your search
+83
-21
@@ -34,31 +34,71 @@ detect_platform() {
|
||||
info "Detected platform: ${os} ${arch}"
|
||||
}
|
||||
|
||||
# Add a directory to PATH in the user's shell rc file, idempotently.
|
||||
# Append an export line to a single rc file, idempotently. Returns 0 on success.
|
||||
_append_path_line() {
|
||||
rc="$1"
|
||||
line="$2"
|
||||
[ -n "${rc}" ] || return 1
|
||||
if [ -f "${rc}" ] && grep -Fq "${bindir}" "${rc}" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
printf '\n# Added by PraisonAI installer\n%s\n' "${line}" >>"${rc}" 2>/dev/null
|
||||
}
|
||||
|
||||
wire_path_dispatch() {
|
||||
bindir="$1"
|
||||
shell_name="$(basename "${SHELL:-sh}")"
|
||||
updated=""
|
||||
|
||||
case "${shell_name}" in
|
||||
zsh)
|
||||
line="export PATH=\"${bindir}:\$PATH\""
|
||||
_append_path_line "${HOME}/.zshrc" "${line}" && updated="${HOME}/.zshrc"
|
||||
;;
|
||||
fish)
|
||||
mkdir -p "${HOME}/.config/fish" 2>/dev/null || true
|
||||
rc="${HOME}/.config/fish/config.fish"
|
||||
# `fish_add_path` is unavailable on older fish; guard it and provide a
|
||||
# POSIX-safe fallback so PATH is wired regardless of fish version.
|
||||
line="if type -q fish_add_path; fish_add_path ${bindir}; else; set -gx PATH ${bindir} \$PATH; end"
|
||||
_append_path_line "${rc}" "${line}" && updated="${rc}"
|
||||
;;
|
||||
bash)
|
||||
# Interactive non-login shells read ~/.bashrc; login shells read the
|
||||
# first existing of ~/.bash_profile, ~/.bash_login, ~/.profile. Wire the
|
||||
# interactive rc and whichever login profile applies so both work.
|
||||
line="export PATH=\"${bindir}:\$PATH\""
|
||||
_append_path_line "${HOME}/.bashrc" "${line}" && updated="${HOME}/.bashrc"
|
||||
if [ -f "${HOME}/.bash_profile" ]; then
|
||||
login_rc="${HOME}/.bash_profile"
|
||||
elif [ -f "${HOME}/.bash_login" ]; then
|
||||
login_rc="${HOME}/.bash_login"
|
||||
else
|
||||
login_rc="${HOME}/.profile"
|
||||
fi
|
||||
_append_path_line "${login_rc}" "${line}" \
|
||||
&& updated="${updated:+${updated} }${login_rc}"
|
||||
;;
|
||||
*)
|
||||
line="export PATH=\"${bindir}:\$PATH\""
|
||||
_append_path_line "${HOME}/.profile" "${line}" && updated="${HOME}/.profile"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "${updated}" ]; then
|
||||
info "Added ${bindir} to PATH in ${updated} (restart your shell to apply)."
|
||||
else
|
||||
warn "Could not update your shell profile; add ${bindir} to your PATH manually."
|
||||
fi
|
||||
}
|
||||
|
||||
# Add a directory to PATH in the user's shell rc file(s), idempotently.
|
||||
wire_path() {
|
||||
bindir="$1"
|
||||
case ":${PATH}:" in
|
||||
*":${bindir}:"*) return 0 ;;
|
||||
esac
|
||||
|
||||
shell_name="$(basename "${SHELL:-sh}")"
|
||||
case "${shell_name}" in
|
||||
zsh) rc="${HOME}/.zshrc"; line="export PATH=\"${bindir}:\$PATH\"" ;;
|
||||
fish)
|
||||
rc="${HOME}/.config/fish/config.fish"
|
||||
line="fish_add_path ${bindir}"
|
||||
mkdir -p "${HOME}/.config/fish" 2>/dev/null || true
|
||||
;;
|
||||
bash) rc="${HOME}/.bashrc"; line="export PATH=\"${bindir}:\$PATH\"" ;;
|
||||
*) rc="${HOME}/.profile"; line="export PATH=\"${bindir}:\$PATH\"" ;;
|
||||
esac
|
||||
|
||||
if [ -f "${rc}" ] && grep -Fq "${bindir}" "${rc}" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
printf '\n# Added by PraisonAI installer\n%s\n' "${line}" >>"${rc}" 2>/dev/null \
|
||||
&& info "Added ${bindir} to PATH in ${rc} (restart your shell or 'source ${rc}')." \
|
||||
|| warn "Could not update ${rc}; add ${bindir} to your PATH manually."
|
||||
wire_path_dispatch "${bindir}"
|
||||
}
|
||||
|
||||
spec() {
|
||||
@@ -88,7 +128,21 @@ bootstrap_uv() {
|
||||
else
|
||||
info "No uv/pipx found; bootstrapping uv..."
|
||||
fi
|
||||
curl -fsSL https://astral.sh/uv/install.sh | sh
|
||||
# Download the installer to a file first so a failed/empty download (which a
|
||||
# piped `curl | sh` can silently swallow) surfaces here with a useful message
|
||||
# instead of a later confusing `uv: command not found`.
|
||||
tmp_installer="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/uv-install.$$.sh")"
|
||||
if ! curl -fsSL https://astral.sh/uv/install.sh -o "${tmp_installer}" \
|
||||
|| [ ! -s "${tmp_installer}" ]; then
|
||||
rm -f "${tmp_installer}" 2>/dev/null || true
|
||||
err "Failed to download the uv bootstrap installer (network/DNS issue?)."
|
||||
err "Install uv or pipx manually, then re-run this installer:"
|
||||
err " https://docs.astral.sh/uv/getting-started/installation/"
|
||||
exit 1
|
||||
fi
|
||||
sh "${tmp_installer}"
|
||||
rm -f "${tmp_installer}" 2>/dev/null || true
|
||||
|
||||
# uv installs to ~/.local/bin or ~/.cargo/bin depending on platform.
|
||||
if [ -x "${HOME}/.local/bin/uv" ]; then
|
||||
PATH="${HOME}/.local/bin:${PATH}"
|
||||
@@ -96,6 +150,14 @@ bootstrap_uv() {
|
||||
PATH="${HOME}/.cargo/bin:${PATH}"
|
||||
fi
|
||||
export PATH
|
||||
|
||||
# Verify the bootstrap actually produced a working `uv`; otherwise fail loudly
|
||||
# instead of proceeding to `uv tool install` with no uv on PATH.
|
||||
if ! has uv; then
|
||||
err "uv bootstrap completed but 'uv' is not on PATH."
|
||||
err "Open a new shell or install uv/pipx manually, then re-run this installer."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -795,11 +795,17 @@ def main_callback(
|
||||
# raises (only reads a time-boxed cache, never performs network I/O here).
|
||||
if mode == OutputMode.TEXT and not state.quiet:
|
||||
try:
|
||||
from .features.self_manage import read_cached_hint
|
||||
from .features.self_manage import (
|
||||
maybe_schedule_update_check,
|
||||
read_cached_hint,
|
||||
)
|
||||
|
||||
hint = read_cached_hint()
|
||||
if hint:
|
||||
typer.echo(hint, err=True)
|
||||
# Warm the cache in a detached child for the next invocation. Never
|
||||
# performs network I/O here and never blocks or raises.
|
||||
maybe_schedule_update_check()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -258,3 +258,53 @@ def refresh_update_cache(timeout: float = 5.0) -> None:
|
||||
except Exception:
|
||||
# A read-only home or unwritable state dir must never break the CLI.
|
||||
return
|
||||
|
||||
|
||||
def _cache_is_fresh() -> bool:
|
||||
"""Return True when the update cache exists and is within its TTL."""
|
||||
try:
|
||||
path = _update_cache_path()
|
||||
if not path.exists():
|
||||
return False
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
checked_at = float(data.get("checked_at", 0))
|
||||
return (time.time() - checked_at) <= _UPDATE_CHECK_TTL_SECONDS
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def maybe_schedule_update_check() -> None:
|
||||
"""Warm the update cache in a fully detached background process.
|
||||
|
||||
Called once per CLI start. Performs **no** network I/O in the caller and
|
||||
never blocks or raises: if the cache is missing/stale it spawns a
|
||||
short-lived detached child that refreshes it for the *next* invocation. If
|
||||
the cache is already fresh, or checks are disabled, this is a no-op.
|
||||
"""
|
||||
if update_check_disabled():
|
||||
return
|
||||
try:
|
||||
if _cache_is_fresh():
|
||||
return
|
||||
# Detached, output-suppressed child so the parent CLI never waits on it.
|
||||
code = (
|
||||
"from praisonai_code.cli.features.self_manage import "
|
||||
"refresh_update_cache; refresh_update_cache()"
|
||||
)
|
||||
kwargs = {
|
||||
"stdout": subprocess.DEVNULL,
|
||||
"stderr": subprocess.DEVNULL,
|
||||
"stdin": subprocess.DEVNULL,
|
||||
}
|
||||
if os.name == "posix":
|
||||
kwargs["start_new_session"] = True
|
||||
else: # pragma: no cover - Windows-only detach flag
|
||||
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0) | getattr(
|
||||
subprocess, "DETACHED_PROCESS", 0
|
||||
)
|
||||
if creationflags:
|
||||
kwargs["creationflags"] = creationflags
|
||||
subprocess.Popen([sys.executable, "-c", code], **kwargs)
|
||||
except Exception:
|
||||
# Best-effort only: a spawn failure must never break start-up.
|
||||
return
|
||||
Reference in new issue
Block a user