Skip to content
Merged
Show file tree
Hide file tree
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
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test-and-shellcheck:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install ShellCheck and jq
run: sudo apt-get update && sudo apt-get install -y shellcheck jq

- name: Run tests/test_*.sh
run: |
set -e
fail=0
for t in tests/test_*.sh; do
echo "::group::$t"
if ! bash "$t"; then
echo "::error::$t failed"
fail=1
fi
echo "::endgroup::"
done
exit "$fail"

- name: ShellCheck
run: |
# Policy: .shellcheckrc disables the reviewed leftover set
# (SC2155, SC2034 from #21; remaining codes in .shellcheckrc).
shellcheck -x install.sh barista.sh modules/*.sh lib/*.sh
6 changes: 5 additions & 1 deletion .shellcheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
#
# SC2034: "Variable appears unused." Often legitimately so in shell modules
# where consumers may source the file and read configuration variables.
disable=SC2155,SC2034
#
# Remaining baseline (tracked, not cleaned yet — CI allowlist):
# SC2059 (printf format), SC2001 (sed), SC2012 (ls), SC1090 (dynamic source),
# SC2129 (redirect), SC2086 (unquoted expansion), SC2002 (useless cat).
disable=SC2155,SC2034,SC2059,SC2001,SC2012,SC1090,SC2129,SC2086,SC2002
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,34 @@ brew install jq

### Homebrew (recommended)

The tap formula does **not** ship `install.sh`. After `brew install barista`, configure Claude Code yourself and use `brew upgrade` for updates.

```bash
brew tap pstuart/tap
brew install barista
# Point Claude Code at the formula, or run the bundled installer once:
# $(brew --prefix)/opt/barista/libexec/install.sh --defaults
barista config # post-install module/theme TUI
```

Formula: [pstuart/homebrew-tap](https://github.com/pstuart/homebrew-tap). From-source install remains fully supported.
Set `statusLine.command` in `~/.claude/settings.json` to the **opt** path (it follows upgrades):

```json
{
"statusLine": {
"type": "command",
"command": "$(brew --prefix)/opt/barista/libexec/barista.sh"
}
}
```

Replace `$(brew --prefix)` with the output of that command (Homebrew does not expand it inside JSON). Typical values: `/opt/homebrew` (Apple Silicon) or `/usr/local` (Intel).

Then:

```bash
barista config # module/theme TUI
brew upgrade barista
```

Formula: [pstuart/homebrew-tap](https://github.com/pstuart/homebrew-tap). Caveats match this README: `install.sh` is from-source only.

### From source

Expand Down Expand Up @@ -212,10 +231,11 @@ The interactive installer features:
./install.sh --no-emoji # Install without emojis (ASCII mode)
./install.sh --no-color # Install without colors

# Updates
./install.sh --check-update # Check if a newer version is available
./install.sh --update # Download and install the latest version
./install.sh --version # Show current version
# Updates (from-source / git clone only — not shipped by Homebrew)
./install.sh --check-update # Check the latest GitHub *release* tag
./install.sh --update # Install that tagged release (not floating main)
./install.sh --skip-update-check # Skip the automatic update check during install
./install.sh --version # Show current version

# Other
./install.sh --uninstall # Uninstall and restore previous statusline
Expand All @@ -224,12 +244,11 @@ The interactive installer features:

### Staying Up to Date

Barista automatically checks for updates when you run the installer. You can also manually check:
**Homebrew:** `brew upgrade barista`. The formula pins a tagged tarball (`vX.Y.Z` + sha256). Do not run `./install.sh --update` against a Cellar/opt prefix — the installer refuses that path so a checksum-pinned brew install is not overwritten with a zip.

```bash
./install.sh --check-update # Check for updates
./install.sh --update # Update to latest version
```
**From source (git clone or a copied tree):** `./install.sh --check-update` and `./install.sh --update` use GitHub `releases/latest`, the same tag source as the statusline `update` module and the Homebrew `livecheck`. They do **not** track unpinned `main`.

Barista also checks for a newer *release* when you run the from-source installer (skip with `--skip-update-check`).

### Manual Brew

Expand Down Expand Up @@ -503,6 +522,10 @@ bash tests/test_utils.sh # or any other tests/test_*.sh

## Uninstall

**Homebrew:** `brew uninstall barista` (then remove `statusLine` from `settings.json` if you added it).

**From source:**

```bash
./install.sh --uninstall
```
Expand Down
169 changes: 118 additions & 51 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,21 @@ BACKUP_MANIFEST="$BACKUP_DIR/manifest.json"
# VERSION MANAGEMENT
# =============================================================================
GITHUB_REPO="pstuart/Barista"
GITHUB_RAW_URL="https://raw.githubusercontent.com/$GITHUB_REPO/main"
LOCAL_VERSION=""
REMOTE_VERSION=""
UPDATE_AVAILABLE="false"

# Homebrew formula pins tagged tarballs; never overwrite Cellar/opt via --update.
_is_homebrew_prefix() {
local dir="${1:-$SCRIPT_DIR}"
case "$dir" in
*/Cellar/barista/*|*/opt/barista|*/opt/barista/*)
return 0
;;
esac
return 1
}

# Read local version
get_local_version() {
if [ -f "$SCRIPT_DIR/VERSION" ]; then
Expand All @@ -90,43 +100,72 @@ get_local_version() {
echo "$LOCAL_VERSION"
}

# Check GitHub for latest version
# Latest published GitHub release (same source as modules/update.sh and brew livecheck).
check_remote_version() {
if ! command -v curl &> /dev/null; then
return 1
fi

# Fetch remote VERSION file (timeout 5 seconds, HTTPS only, limited redirects)
REMOTE_VERSION=$(curl -s --connect-timeout 5 --max-redirs 3 --proto =https "$GITHUB_RAW_URL/VERSION" 2>/dev/null | tr -d '[:space:]')
local response http_code body tag
response=$(curl -s --connect-timeout 5 --max-time 8 --max-redirs 3 --proto =https \
-w "\n%{http_code}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null)

if [ -z "$REMOTE_VERSION" ] || [[ ! "$REMOTE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')

if [ "$http_code" != "200" ] || [ -z "$body" ]; then
return 1
fi

if command -v jq &>/dev/null; then
tag=$(echo "$body" | jq -r '.tag_name // empty' 2>/dev/null)
else
tag=$(echo "$body" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
fi
tag="${tag#v}"
tag=$(echo "$tag" | tr -d '[:space:]')

if [ -z "$tag" ] || [[ ! "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
return 1
fi

# Strip prerelease/build from the displayed pin; comparator sanitizes too.
REMOTE_VERSION="${tag%%[-+]*}"
echo "$REMOTE_VERSION"
}

# Compare semantic versions (returns 0 if v1 < v2)
# Leading digits of one semver component (safe for bash arithmetic).
_version_numeric_component() {
local value="${1:-}"
value="${value%%[!0-9]*}"
printf '%s\n' "${value:-0}"
}

# Compare semantic versions (returns 0 if v1 < v2). Matches modules/update.sh
# _version_gt inverted: numeric major/minor/patch only; suffixes ignored.
version_lt() {
local v1="$1"
local v2="$2"

# Split versions into arrays
local IFS='.'
read -ra V1 <<< "$v1"
read -ra V2 <<< "$v2"

# Compare each component
for i in 0 1 2; do
local n1="${V1[$i]:-0}"
local n2="${V2[$i]:-0}"
if [ "$n1" -lt "$n2" ] 2>/dev/null; then
return 0
elif [ "$n1" -gt "$n2" ] 2>/dev/null; then
return 1
fi
done
return 1 # Equal versions
local v1_major v1_minor v1_patch v2_major v2_minor v2_patch

IFS=. read -r v1_major v1_minor v1_patch _ <<< "$v1"
IFS=. read -r v2_major v2_minor v2_patch _ <<< "$v2"

v1_major=$(_version_numeric_component "$v1_major")
v1_minor=$(_version_numeric_component "$v1_minor")
v1_patch=$(_version_numeric_component "$v1_patch")
v2_major=$(_version_numeric_component "$v2_major")
v2_minor=$(_version_numeric_component "$v2_minor")
v2_patch=$(_version_numeric_component "$v2_patch")

if [ "$v1_major" -lt "$v2_major" ]; then return 0; fi
if [ "$v1_major" -gt "$v2_major" ]; then return 1; fi
if [ "$v1_minor" -lt "$v2_minor" ]; then return 0; fi
if [ "$v1_minor" -gt "$v2_minor" ]; then return 1; fi
if [ "$v1_patch" -lt "$v2_patch" ]; then return 0; fi
return 1
}

# Check for updates
Expand Down Expand Up @@ -221,39 +260,58 @@ interactive_update_check() {
return 0
}

# Perform the update
# Test seams: tests redefine these to record argv / skip network.
_reexec_installer() {
exec "$SCRIPT_DIR/install.sh" "$@"
}

_barista_git() {
git "$@"
}

# Perform the update from the latest GitHub *release* tag (not unpinned main).
do_update() {
print_info "Updating Barista..."

if _is_homebrew_prefix "$SCRIPT_DIR"; then
print_error "This install is Homebrew-managed. Use: brew upgrade barista"
return 1
fi

if [ -z "$REMOTE_VERSION" ]; then
if ! check_remote_version >/dev/null; then
print_error "Could not resolve the latest GitHub release."
return 1
fi
fi

local tag="v${REMOTE_VERSION}"

# Check if we're in a git repo
if [ -d "$SCRIPT_DIR/.git" ]; then
print_info "Pulling latest changes from GitHub..."
print_info "Checking out release $tag..."
cd "$SCRIPT_DIR" || { print_error "Could not cd to $SCRIPT_DIR"; return 1; }

# Stash any local changes
git stash -q 2>/dev/null

# Pull latest
if git pull origin main 2>/dev/null; then
_barista_git stash -q 2>/dev/null
if _barista_git fetch origin --tags --force 2>/dev/null \
&& _barista_git checkout -q "$tag" 2>/dev/null; then
print_success "Updated to version $REMOTE_VERSION"
echo ""
print_info "Restarting installer with new version..."
echo ""

# Re-execute the installer
exec "$SCRIPT_DIR/install.sh" "$@"
_reexec_installer "$@"
else
print_error "Git pull failed. Try manually: cd $SCRIPT_DIR && git pull"
print_error "Could not check out $tag. Try: cd $SCRIPT_DIR && git fetch --tags && git checkout $tag"
return 1
fi
else
# Not a git repo - download via curl
print_info "Downloading latest version..."
print_info "Downloading release $tag..."

local tmp_dir=$(mktemp -d)
local zip_url="https://github.com/$GITHUB_REPO/archive/refs/heads/main.zip"
local tmp_dir
tmp_dir=$(mktemp -d)
local zip_url="https://github.com/$GITHUB_REPO/archive/refs/tags/${tag}.zip"
local extract_dir="Barista-${REMOTE_VERSION}"

# Validate URL is from expected GitHub origin (prevent open-redirect abuse)
case "$zip_url" in
https://github.com/*)
;;
Expand All @@ -264,10 +322,7 @@ do_update() {
;;
esac

# --max-redirs 3: limit redirect following to prevent redirect chains
# --proto =https: only allow HTTPS (no downgrade to HTTP)
if curl -sL --max-redirs 3 --proto =https "$zip_url" -o "$tmp_dir/barista.zip" 2>/dev/null; then
# Validate the downloaded file is actually a ZIP archive
if ! unzip -tq "$tmp_dir/barista.zip" >/dev/null 2>&1; then
print_error "Downloaded file is not a valid ZIP archive. Aborting update."
rm -rf "$tmp_dir"
Expand All @@ -277,32 +332,42 @@ do_update() {
cd "$tmp_dir" || { print_error "Could not cd to temp dir"; rm -rf "$tmp_dir"; return 1; }
unzip -q barista.zip 2>/dev/null

if [ -d "Barista-main" ]; then
# Verify essential files exist in the download
if [ ! -f "Barista-main/barista.sh" ] || [ ! -f "Barista-main/VERSION" ]; then
# GitHub source zips use "Barista-<tag without v>/"
if [ ! -d "$extract_dir" ] && [ -d "Barista-${tag}" ]; then
extract_dir="Barista-${tag}"
fi

if [ -d "$extract_dir" ]; then
if [ ! -f "$extract_dir/barista.sh" ] || [ ! -f "$extract_dir/VERSION" ]; then
print_error "Downloaded archive is missing essential files. Aborting update."
rm -rf "$tmp_dir"
return 1
fi

# Backup current and replace
local archive_ver
archive_ver=$(tr -d '[:space:]' < "$extract_dir/VERSION")
if [ "$archive_ver" != "$REMOTE_VERSION" ]; then
print_error "Archive VERSION ($archive_ver) does not match release $REMOTE_VERSION."
rm -rf "$tmp_dir"
return 1
fi

cp -r "$SCRIPT_DIR" "$SCRIPT_DIR.backup.$$"
cp -r Barista-main/* "$SCRIPT_DIR/"
cp -r "$extract_dir"/* "$SCRIPT_DIR/"
rm -rf "$SCRIPT_DIR.backup.$$"

print_success "Updated to version $REMOTE_VERSION"
echo ""
print_info "Restarting installer with new version..."
echo ""

# Cleanup and re-execute
rm -rf "$tmp_dir"
exec "$SCRIPT_DIR/install.sh" "$@"
_reexec_installer "$@"
fi
fi

rm -rf "$tmp_dir"
print_error "Update failed. Please update manually from GitHub."
print_error "Update failed. Please update manually from a GitHub release."
return 1
fi
}
Expand Down Expand Up @@ -2187,4 +2252,6 @@ main() {
esac
}

main "$@"
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
main "$@"
fi
Loading
Loading