6767# DEEPAGENTS_CODE_RIPGREP_INSTALLER — how to provision ripgrep:
6868# "managed" (default) eagerly installs the pinned, SHA-256-verified binary
6969# into ~/.deepagents/bin (no sudo) via `dcode tools install`; "system"
70- # keeps the interactive package-manager install (brew/apt/cargo/...). Set
71- # DEEPAGENTS_CODE_OFFLINE=1 to skip the managed download entirely.
70+ # keeps the interactive package-manager install (brew/apt/cargo/...),
71+ # which only counts as success when the resulting `rg` meets the minimum
72+ # supported version. Set DEEPAGENTS_CODE_OFFLINE=1 to skip the managed
73+ # download entirely.
74+ #
75+ # Before execution, the downloaded uv bootstrap script is checked for a shell
76+ # shebang and valid shell syntax. These structural checks help catch error
77+ # pages and some truncated downloads, but do not verify its integrity.
7278# DEEPAGENTS_CODE_SKIP_XCODE_CHECK — set to 1 to bypass the macOS Xcode
7379# Command Line Tools preflight check
7480# DEEPAGENTS_CODE_NO_MODIFY_PATH — set to 1 to skip PATH setup entirely
461467# ---------------------------------------------------------------------------
462468# Prompt helper — reads from /dev/tty when stdin is piped
463469# ---------------------------------------------------------------------------
470+ # prompt_yn QUESTION — three outcomes, so callers can tell a declined prompt
471+ # apart from one that could never be shown:
472+ # 0 — answered yes
473+ # 1 — answered no (or empty)
474+ # 2 — no terminal exists to ask on (non-interactive, or /dev/tty unusable)
475+ # Plain `if prompt_yn ...` keeps working unchanged: 0 is yes, anything else is
476+ # not-yes. Only callers that act differently on "no terminal" vs "no" check
477+ # for 2 explicitly.
464478prompt_yn () {
465479 local question=" $1 "
466480 if [ " $IS_INTERACTIVE " = false ]; then
467- return 1
481+ return 2
468482 fi
469483 local reply=" "
470484 if [ -t 0 ]; then
@@ -1296,6 +1310,14 @@ install_uv() {
12961310 exit " $uv_install_rc "
12971311 fi
12981312
1313+ # Astral does not publish a checksum that covers uv-installer.sh itself:
1314+ # the per-release sha256.sum and dist-manifest.json only list the platform
1315+ # archives, and the archives' digests are embedded in this script (which
1316+ # pins APP_VERSION and verifies them after download). The meaningful checks
1317+ # for the script fetch are therefore the shebang and parse checks below:
1318+ # they catch a captive-portal HTML page and a truncated body, which are the
1319+ # realistic failure modes for this URL.
1320+ #
12991321 # Verify the downloaded script starts with a shell shebang before executing
13001322 # it. This catches a non-shell response — an HTML error page or JSON from a
13011323 # proxy or captive portal that returned 200 — that would otherwise fail
@@ -2686,7 +2708,12 @@ ensure_path_setup() {
26862708 done
26872709 fi
26882710 if [ " $IS_INTERACTIVE " = true ] && can_prompt; then
2689- if ! prompt_yn " Add ~/.local/bin to your PATH in ${prompt_targets} ?" ; then
2711+ # Only an explicit "no" (rc=1) declines; rc=2 (no usable terminal) leaves
2712+ # the auto-add default in place, matching the non-interactive behavior.
2713+ # `|| prompt_rc=$?` keeps "no" from tripping `set -e`.
2714+ prompt_rc=0
2715+ prompt_yn " Add ~/.local/bin to your PATH in ${prompt_targets} ?" || prompt_rc=$?
2716+ if [ " $prompt_rc " -eq 1 ]; then
26902717 should_add=false
26912718 fi
26922719 fi
@@ -2951,6 +2978,46 @@ fi
29512978# ---------------------------------------------------------------------------
29522979# Optional tools — ripgrep
29532980# ---------------------------------------------------------------------------
2981+ # Oldest ripgrep the agent's grep tool is expected to work with. The managed
2982+ # installer pins a newer upstream release; this floor only guards the system
2983+ # path (brew/apt/...) so an ancient distro package doesn't install
2984+ # "successfully" and then behave differently at runtime.
2985+ MIN_RIPGREP_VERSION=" 12.0.0"
2986+
2987+ # version_at_least HAVE WANT — dotted numeric compare (12.0.0 >= 11.0.0). The
2988+ # installer runs before Python exists, so this stays in pure shell.
2989+ version_at_least () {
2990+ local have=" $1 " want=" $2 " IFS_save=" $IFS "
2991+ case " $have " in ' ' |* [!0-9.]* ) return 1 ;; esac
2992+ IFS=.
2993+ # shellcheck disable=SC2086 # word-splitting on '.' is the point
2994+ set -- $have
2995+ IFS=" $IFS_save "
2996+ local have_major=" ${1:- 0} " have_minor=" ${2:- 0} " have_patch=" ${3:- 0} "
2997+ IFS=.
2998+ # shellcheck disable=SC2086
2999+ set -- $want
3000+ IFS=" $IFS_save "
3001+ local want_major=" ${1:- 0} " want_minor=" ${2:- 0} " want_patch=" ${3:- 0} "
3002+ [ " $have_major " -gt " $want_major " ] && return 0
3003+ [ " $have_major " -lt " $want_major " ] && return 1
3004+ [ " $have_minor " -gt " $want_minor " ] && return 0
3005+ [ " $have_minor " -lt " $want_minor " ] && return 1
3006+ [ " $have_patch " -ge " $want_patch " ]
3007+ }
3008+
3009+ # version_older_than_have IS MIN — true when IS is present but below MIN.
3010+ version_too_old () {
3011+ local is=" $1 " min=" $2 "
3012+ [ -n " $is " ] || return 1
3013+ ! version_at_least " $is " " $min "
3014+ }
3015+
3016+ # Print the version of `rg` on PATH, or nothing when it can't be determined.
3017+ installed_rg_version () {
3018+ rg --version 2> /dev/null | head -1 | awk ' {print $2}'
3019+ }
3020+
29543021
29553022# Pre-check: verify sudo is usable before running sudo commands.
29563023# Returns 0 if sudo is available (cached or passwordless), 1 otherwise.
@@ -2970,52 +3037,69 @@ check_sudo() {
29703037 return 1
29713038}
29723039
3040+ # True when the `rg` on PATH is present and at least MIN_RIPGREP_VERSION.
3041+ # An install that produced a too-old binary does not count as success: an
3042+ # ancient distro package would otherwise satisfy `command -v rg` while behaving
3043+ # differently from what the grep tool expects.
3044+ installed_rg_is_acceptable () {
3045+ local ver
3046+ command -v rg > /dev/null 2>&1 || return 1
3047+ if ! ver=$( installed_rg_version) || [ -z " $ver " ]; then
3048+ return 1
3049+ fi
3050+ if version_too_old " $ver " " $MIN_RIPGREP_VERSION " ; then
3051+ log_warn " ripgrep ${ver} is older than the supported minimum (${MIN_RIPGREP_VERSION} )."
3052+ return 1
3053+ fi
3054+ return 0
3055+ }
3056+
29733057install_ripgrep_via_pkg () {
29743058 case " $OS " in
29753059 macos)
29763060 if command -v brew > /dev/null 2>&1 ; then
29773061 log_info " Installing ripgrep via Homebrew (this may take a moment)..."
29783062 if HOMEBREW_NO_AUTO_UPDATE=1 brew install ripgrep; then
2979- command -v rg > /dev/null 2>&1 && return 0
3063+ installed_rg_is_acceptable && return 0
29803064 fi
29813065 fi
29823066 if command -v port > /dev/null 2>&1 && check_sudo; then
29833067 log_info " Installing ripgrep via MacPorts..."
29843068 if sudo port install ripgrep; then
2985- command -v rg > /dev/null 2>&1 && return 0
3069+ installed_rg_is_acceptable && return 0
29863070 fi
29873071 fi
29883072 ;;
29893073 linux)
29903074 if command -v apt-get > /dev/null 2>&1 && check_sudo; then
29913075 log_info " Installing ripgrep via apt-get..."
29923076 if sudo apt-get install -y ripgrep; then
2993- command -v rg > /dev/null 2>&1 && return 0
3077+ installed_rg_is_acceptable && return 0
29943078 fi
29953079 elif command -v dnf > /dev/null 2>&1 && check_sudo; then
29963080 log_info " Installing ripgrep via dnf..."
29973081 if sudo dnf install -y ripgrep; then
2998- command -v rg > /dev/null 2>&1 && return 0
3082+ installed_rg_is_acceptable && return 0
29993083 fi
30003084 elif command -v pacman > /dev/null 2>&1 && check_sudo; then
30013085 log_info " Installing ripgrep via pacman..."
30023086 if sudo pacman -S --noconfirm ripgrep; then
3003- command -v rg > /dev/null 2>&1 && return 0
3087+ installed_rg_is_acceptable && return 0
30043088 fi
30053089 elif command -v zypper > /dev/null 2>&1 && check_sudo; then
30063090 log_info " Installing ripgrep via zypper..."
30073091 if sudo zypper install -y ripgrep; then
3008- command -v rg > /dev/null 2>&1 && return 0
3092+ installed_rg_is_acceptable && return 0
30093093 fi
30103094 elif command -v apk > /dev/null 2>&1 && check_sudo; then
30113095 log_info " Installing ripgrep via apk..."
30123096 if sudo apk add ripgrep; then
3013- command -v rg > /dev/null 2>&1 && return 0
3097+ installed_rg_is_acceptable && return 0
30143098 fi
30153099 elif command -v nix-env > /dev/null 2>&1 ; then
30163100 log_info " Installing ripgrep via nix..."
30173101 if nix-env -iA nixpkgs.ripgrep; then
3018- command -v rg > /dev/null 2>&1 && return 0
3102+ installed_rg_is_acceptable && return 0
30193103 fi
30203104 fi
30213105 ;;
@@ -3028,8 +3112,8 @@ install_ripgrep_via_cargo() {
30283112 log_info " Installing ripgrep via cargo (no sudo needed)..."
30293113 if cargo install ripgrep; then
30303114 fix_owner " ${HOME} /.cargo"
3031- command -v rg > /dev/null 2>&1 && return 0
3032- log_warn " cargo install succeeded but rg not found in PATH."
3115+ installed_rg_is_acceptable && return 0
3116+ log_warn " cargo install succeeded but rg not found in PATH or too old ."
30333117 fi
30343118 fi
30353119 return 1
@@ -3084,10 +3168,17 @@ if [ "$SKIP_OPTIONAL" != "1" ]; then
30843168 fi
30853169 fi
30863170 elif command -v rg > /dev/null 2>&1 ; then
3087- if [ " $VERBOSE " = " 1" ]; then
3171+ if ! rg_version=$( installed_rg_version) || [ -z " $rg_version " ]; then
3172+ echo " "
3173+ log_warn " Could not determine the version of ripgrep on PATH."
3174+ ripgrep_manual_hint
3175+ elif version_too_old " $rg_version " " $MIN_RIPGREP_VERSION " ; then
3176+ echo " "
3177+ log_warn " ripgrep ${rg_version} found, but the minimum supported is ${MIN_RIPGREP_VERSION} — the grep tool may misbehave."
3178+ ripgrep_manual_hint
3179+ elif [ " $VERBOSE " = " 1" ]; then
30883180 echo " "
30893181 log_info " Checking optional tools..."
3090- rg_version=$( rg --version 2> /dev/null | head -1 | awk ' {print $2}' ) || rg_version=" (version unknown)"
30913182 log_success " ripgrep ${rg_version} found"
30923183 fi
30933184 else
0 commit comments