Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 254 additions & 78 deletions scripts/update-version-pins.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# update-version-pins.sh - Refresh shared version pins from upstream sources
#
# Verbose TUI with real-time fetch progress, grouped version comparison,
# and optional changelog display for updated tools.
# Concurrent fetches (capped background jobs) with a live TUI: each tool
# goes pending -> checking -> done/failed in place. Non-TTY (CI) falls back
# to plain ordered line output. Optional changelog display for updates.

set -euo pipefail

Expand All @@ -17,6 +18,15 @@ SHOW_CHANGELOG=0
IS_TTY=0
[[ -t 1 ]] && IS_TTY=1

# Honor NO_COLOR; keep non-TTY output free of escapes entirely.
if [[ -n ${NO_COLOR:-} || $IS_TTY -eq 0 ]]; then
RESET='' BOLD='' DIM='' RED='' GREEN='' YELLOW='' CYAN='' WHITE=''
fi

MAX_JOBS=8
SPIN_FRAMES="|/-\\"
DASH_RULE='──────────────────────────────────────────────────'

N_UPDATED=0
N_UNCHANGED=0
N_FAILED=0
Expand Down Expand Up @@ -62,65 +72,250 @@ fetch_latest_commit() {
git ls-remote "$1" "$2" 2>/dev/null | awk 'NR == 1 { print $1 }'
}

# ── Pin: fetch one version and display result ────────────────────────────
# Usage: pin NAME VAR_NAME FETCH_TYPE [FETCH_ARGS...]
# ── Pin table ────────────────────────────────────────────────────────────
# GROUP|NAME|VAR|FETCH_TYPE[|FETCH_ARGS...]
# Order here is the display and merge order; keep it stable.

PIN_TABLE=(
"Toolchains|Go|GO_VERSION|go"
"Toolchains|delta|DELTA_VERSION|git-tag|https://github.com/dandavison/delta.git"
"Agent CLIs|Claude Code|CLAUDE_CODE_VERSION|npm|@anthropic-ai/claude-code"
"Agent CLIs|cctrace|CCTRACE_VERSION|npm|@thevibeworks/cctrace"
"Agent CLIs|Codex|CODEX_VERSION|npm|@openai/codex"
"Agent CLIs|Gemini CLI|GEMINI_CLI_VERSION|npm|@google/gemini-cli"
"Agent CLIs|Grok CLI|GROK_CLI_VERSION|npm|@xai-official/grok"
"Agent CLIs|Kimi Code|KIMI_CODE_VERSION|npm|@moonshot-ai/kimi-code"
"Agent CLIs|opencode|OPENCODE_VERSION|npm|opencode-ai"
"Agent CLIs|pi|PI_CODING_AGENT_VERSION|npm|@earendil-works/pi-coding-agent"
"Agent CLIs|dsh|DSH_VERSION|npm|@deepseek-ai/dsh"
"Agent CLIs|cursor|CURSOR_CLI_VERSION|cursor"
"Agent CLIs|CCX|CCX_VERSION|git-tag|https://github.com/thevibeworks/ccx.git"
"Agent CLIs|Copilot API|COPILOT_API_VERSION|git-commit|https://github.com/ericc-ch/copilot-api.git|refs/heads/master"
"Browser Tools|Playwright|PLAYWRIGHT_VERSION|npm|playwright"
"Browser Tools|CloakBrowser|CLOAKBROWSER_WRAPPER_VERSION|npm|cloakbrowser"
"Browser Tools|Kimi WebBridge|KIMI_WEBBRIDGE_VERSION|webbridge"
)

# Parallel per-tool arrays, filled by init_table.
T_NAME=() T_VAR=() T_TYPE=() T_ARG1=() T_ARG2=()
T_OLD=() T_NEW=() T_STATE=() # STATE: pending|running|fail|same|bump
LAYOUT=() # render items: head1:G, head2:G, gap, tool:i
TOTAL=0
WORK_DIR=""
RENDERED_LINES=0
LAYOUT_POS=0
FRAME=0

init_table() {
local entry group name var ftype a1 a2 i=0 prev_group=""
for entry in "${PIN_TABLE[@]}"; do
IFS='|' read -r group name var ftype a1 a2 <<< "$entry"
T_NAME[i]=$name
T_VAR[i]=$var
T_TYPE[i]=$ftype
T_ARG1[i]=${a1:-}
T_ARG2[i]=${a2:-}
T_OLD[i]=${!var:-}
T_NEW[i]=""
T_STATE[i]=pending
if [[ $group != "$prev_group" ]]; then
if [[ -n $prev_group ]]; then
LAYOUT+=("gap" "head2:$group")
else
LAYOUT+=("head1:$group")
fi
prev_group=$group
fi
LAYOUT+=("tool:$i")
i=$((i + 1))
done
TOTAL=$i
}

# ── Background fetch job (always exits 0; empty value = failure) ─────────

fetch_one() {
local idx=$1 ftype=$2 a1=$3 a2=$4
local val=""
case $ftype in
go) val=$(fetch_go_version) || true ;;
npm) val=$(fetch_npm_version "$a1") || true ;;
git-tag) val=$(fetch_latest_git_tag "$a1") || true ;;
git-commit) val=$(fetch_latest_commit "$a1" "$a2") || true ;;
webbridge) val=$(_webbridge_cdn_latest) || true ;;
cursor) val=$(_cursor_installer_latest) || true ;;
esac
printf '%s' "$val" > "$WORK_DIR/$idx.val"
: > "$WORK_DIR/$idx.done" # marker last: .val is complete once this exists
}

classify() {
local i=$1 val
val=$(<"$WORK_DIR/$i.val")
T_NEW[i]=$val
if [[ -z $val ]]; then
T_STATE[i]=fail
elif [[ $val == "${T_OLD[$i]}" ]]; then
T_STATE[i]=same
else
T_STATE[i]=bump
fi
}

# ── Display ──────────────────────────────────────────────────────────────

pin() {
local name=$1 var=$2 fetch_type=$3
shift 3
local old_val=${!var:-}
tool_line() {
local i=$1 eol=${2:-}
local name=${T_NAME[$i]} old=${T_OLD[$i]} new=${T_NEW[$i]}
local pad
pad=$(printf '%-16s' "$name")

# Live progress (tty only -- overwritten when done)
if [[ $IS_TTY -eq 1 ]]; then
echo -en " ${CYAN}│${RESET} ${DIM}⟳ ${pad} fetching...${RESET}"
local old_disp=$old new_disp=$new
if [[ ${T_TYPE[$i]} == "git-commit" ]]; then
old_disp="${old:0:7}"
new_disp="${new:0:7}"
fi

Comment on lines +173 to 178
# Fetch upstream. `|| true` is load-bearing: a failing command
# substitution in a plain assignment takes its exit status, and under
# `set -e` that aborts the whole run -- which would make the fetch-failed
# branch below unreachable and kill the soft-fail contract. One dead
# registry must degrade to a warning, not end the sweep.
local new_val=""
case $fetch_type in
go) new_val=$(fetch_go_version) || true ;;
npm) new_val=$(fetch_npm_version "$1") || true ;;
git-tag) new_val=$(fetch_latest_git_tag "$1") || true ;;
git-commit) new_val=$(fetch_latest_commit "$1" "$2") || true ;;
webbridge) new_val=$(_webbridge_cdn_latest) || true ;;
cursor) new_val=$(_cursor_installer_latest) || true ;;
case ${T_STATE[$i]} in
pending)
echo -e " ${CYAN}│${RESET} ${DIM}. ${pad} waiting${RESET}${eol}"
;;
running)
local spin=${SPIN_FRAMES:FRAME % 4:1}
echo -e " ${CYAN}│${RESET} ${DIM}${spin} ${pad} checking...${RESET}${eol}"
;;
fail)
echo -e " ${CYAN}│${RESET} ${YELLOW}!${RESET} ${WHITE}${pad}${RESET} ${DIM}${old_disp:-?}${RESET} ${YELLOW}(check failed)${RESET}${eol}"
;;
same)
echo -e " ${CYAN}│${RESET} ${DIM}· ${pad} ${new_disp} (up-to-date)${RESET}${eol}"
;;
bump)
echo -e " ${CYAN}│${RESET} ${GREEN}▲${RESET} ${WHITE}${pad}${RESET} ${RED}${old_disp:-new}${RESET} ${DIM}->${RESET} ${GREEN}${new_disp}${RESET}${eol}"
;;
esac
}

# Clear fetching line
if [[ $IS_TTY -eq 1 ]]; then
echo -en "\r\033[K"
fi
emit_layout_line() {
local item=$1 eol=${2:-}
case $item in
gap)
echo -e " ${CYAN}│${RESET}${eol}"
;;
head1:*|head2:*)
local corner="├─" group=${item#head?:} dashes
[[ $item == head1:* ]] && corner="┌─"
dashes=${DASH_RULE:0:48 - ${#group}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid byte-slicing the UTF-8 rule

When the script runs with LC_ALL=C or LC_ALL=POSIX, Bash applies this substring length in bytes rather than characters. Since each rule glyph occupies three bytes and the calculated lengths are not multiples of three, every heading ends with a truncated UTF-8 sequence, producing malformed non-TTY/CI output. Generate the rule without slicing a multibyte string, or force character-aware handling.

Useful? React with 👍 / 👎.

echo -e " ${CYAN}${corner}${BOLD} ${group} ${RESET}${CYAN}${dashes}${RESET}${eol}"
;;
Comment on lines +206 to +210
tool:*)
tool_line "${item#tool:}" "$eol"
;;
esac
}

# Fetch failed -- keep old value, warn
if [[ -z "$new_val" ]]; then
echo -e " ${CYAN}│${RESET} ${YELLOW}!${RESET} ${WHITE}${pad}${RESET} ${DIM}${old_val:-?}${RESET} ${YELLOW}(fetch failed)${RESET}"
N_FAILED=$((N_FAILED + 1))
return
# TTY: redraw the whole block in place (\033[K clears line residue).
render() {
local item
if [[ $RENDERED_LINES -gt 0 ]]; then
printf '\033[%dA' "$RENDERED_LINES"
fi
for item in "${LAYOUT[@]}"; do
emit_layout_line "$item" $'\033[K'
done
RENDERED_LINES=${#LAYOUT[@]}
}

# Update variable in caller's scope
printf -v "$var" '%s' "$new_val"
# Non-TTY: print finished lines in table order as they become available.
plain_flush() {
while [[ $LAYOUT_POS -lt ${#LAYOUT[@]} ]]; do
# Find the tool item this position leads up to; print the run of
# header/gap/tool items only once that tool has a final state.
local j=$LAYOUT_POS ti=""
while [[ $j -lt ${#LAYOUT[@]} ]]; do
if [[ ${LAYOUT[$j]} == tool:* ]]; then
ti=${LAYOUT[$j]#tool:}
break
fi
j=$((j + 1))
done
[[ -z $ti ]] && break
case ${T_STATE[$ti]} in
pending|running) return 0 ;;
esac
while [[ $LAYOUT_POS -le $j ]]; do
emit_layout_line "${LAYOUT[$LAYOUT_POS]}"
LAYOUT_POS=$((LAYOUT_POS + 1))
done
done
}

# Short hash for commits, semver for everything else
local old_disp=$old_val new_disp=$new_val
if [[ $fetch_type == "git-commit" ]]; then
old_disp="${old_val:0:7}"
new_disp="${new_val:0:7}"
fi
# ── Concurrent dispatcher ────────────────────────────────────────────────
# Jobs only write per-tool files under WORK_DIR; versions.env is written
# once, from the main shell, after a deterministic ordered merge.

run_fetches() {
local next=0 running=0 finished=0 i
while [[ $finished -lt $TOTAL ]]; do
while [[ $next -lt $TOTAL && $running -lt $MAX_JOBS ]]; do
T_STATE[next]=running
fetch_one "$next" "${T_TYPE[$next]}" "${T_ARG1[$next]}" "${T_ARG2[$next]}" &
next=$((next + 1))
running=$((running + 1))
done
for (( i = 0; i < next; i++ )); do
if [[ ${T_STATE[$i]} == running && -f "$WORK_DIR/$i.done" ]]; then
classify "$i"
running=$((running - 1))
finished=$((finished + 1))
fi
done
if [[ $IS_TTY -eq 1 ]]; then
render
FRAME=$((FRAME + 1))
else
plain_flush
fi
if [[ $finished -lt $TOTAL ]]; then
sleep 0.12
fi
done
wait || true
}

if [[ "$old_val" == "$new_val" ]]; then
echo -e " ${CYAN}│${RESET} ${DIM}· ${pad} ${new_disp} (up-to-date)${RESET}"
N_UNCHANGED=$((N_UNCHANGED + 1))
else
echo -e " ${CYAN}│${RESET} ${GREEN}▲${RESET} ${WHITE}${pad}${RESET} ${RED}${old_disp:-new}${RESET} ${DIM}->${RESET} ${GREEN}${new_disp}${RESET}"
N_UPDATED=$((N_UPDATED + 1))
UPDATED_VARS+=("${var}|${old_val}|${new_val}")
# Deterministic ordered merge: counters, UPDATED_VARS, and pin variables
# are applied in table order regardless of job completion order.
tally_results() {
local i
for (( i = 0; i < TOTAL; i++ )); do
case ${T_STATE[$i]} in
fail)
N_FAILED=$((N_FAILED + 1))
;;
same)
N_UNCHANGED=$((N_UNCHANGED + 1))
;;
bump)
N_UPDATED=$((N_UPDATED + 1))
printf -v "${T_VAR[$i]}" '%s' "${T_NEW[$i]}"
UPDATED_VARS+=("${T_VAR[$i]}|${T_OLD[$i]}|${T_NEW[$i]}")
;;
esac
done
}

cleanup() {
local pids
pids=$(jobs -p) || true
if [[ -n $pids ]]; then
# shellcheck disable=SC2086
kill $pids 2>/dev/null || true
fi
if [[ -n $WORK_DIR ]]; then
rm -rf "$WORK_DIR"
fi
if [[ $IS_TTY -eq 1 ]]; then
printf '\033[?25h'
fi
}

Expand Down Expand Up @@ -209,6 +404,9 @@ done

main() {
load_version_pins
init_table
WORK_DIR=$(mktemp -d)
trap cleanup EXIT

echo -e "${CYAN}${BOLD}╔══════════════════════════════════════════════════╗${RESET}"
echo -e "${CYAN}${BOLD}║ Refreshing Version Pins ║${RESET}"
Expand All @@ -217,36 +415,14 @@ main() {
[[ $DRY_RUN -eq 1 ]] && echo -e "${YELLOW}(dry run)${RESET}"
echo ""

# ── Toolchains ───────────────────────────────────────────────────────
echo -e " ${CYAN}┌─${BOLD} Toolchains ${RESET}${CYAN}──────────────────────────────────────${RESET}"

pin "Go" GO_VERSION go
pin "delta" DELTA_VERSION git-tag "https://github.com/dandavison/delta.git"

# ── Agent CLIs ───────────────────────────────────────────────────────
echo -e " ${CYAN}│${RESET}"
echo -e " ${CYAN}├─${BOLD} Agent CLIs ${RESET}${CYAN}──────────────────────────────────────${RESET}"

pin "Claude Code" CLAUDE_CODE_VERSION npm "@anthropic-ai/claude-code"
pin "cctrace" CCTRACE_VERSION npm "@thevibeworks/cctrace"
pin "Codex" CODEX_VERSION npm "@openai/codex"
pin "Gemini CLI" GEMINI_CLI_VERSION npm "@google/gemini-cli"
pin "Grok CLI" GROK_CLI_VERSION npm "@xai-official/grok"
pin "Kimi Code" KIMI_CODE_VERSION npm "@moonshot-ai/kimi-code"
pin "opencode" OPENCODE_VERSION npm "opencode-ai"
pin "pi" PI_CODING_AGENT_VERSION npm "@earendil-works/pi-coding-agent"
pin "dsh" DSH_VERSION npm "@deepseek-ai/dsh"
pin "cursor" CURSOR_CLI_VERSION cursor
pin "CCX" CCX_VERSION git-tag "https://github.com/thevibeworks/ccx.git"
pin "Copilot API" COPILOT_API_VERSION git-commit "https://github.com/ericc-ch/copilot-api.git" "refs/heads/master"

# ── Browser Tools ────────────────────────────────────────────────────
echo -e " ${CYAN}│${RESET}"
echo -e " ${CYAN}├─${BOLD} Browser Tools ${RESET}${CYAN}───────────────────────────────────${RESET}"

pin "Playwright" PLAYWRIGHT_VERSION npm "playwright"
pin "CloakBrowser" CLOAKBROWSER_WRAPPER_VERSION npm "cloakbrowser"
pin "Kimi WebBridge" KIMI_WEBBRIDGE_VERSION webbridge
if [[ $IS_TTY -eq 1 ]]; then
printf '\033[?25l'
fi
run_fetches
if [[ $IS_TTY -eq 1 ]]; then
printf '\033[?25h'
fi
tally_results

# ── Summary footer ───────────────────────────────────────────────────
echo -e " ${CYAN}│${RESET}"
Expand Down
Loading