Skip to content
Open
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
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ The system combines:
## Prerequisites

Before working on this codebase, ensure:
- **Bash 3.2+** — All hook scripts and installer are compatible with the stock `/bin/bash` on macOS (3.2). `BASH_REMATCH` and `set -euo pipefail` are available since Bash 3.0/2.0 respectively.
- **jq 1.6+** — Required for all hook scripts and validation. Install: `brew install jq`
- **Node.js 20+ + npm** — Required only for dashboard development (`infra/gcp/dashboard/`)
- **Bash 4+** — All scripts use `set -euo pipefail` and POSIX-compatible tools
- **Git** — Version control and release tagging
- **Tested platforms** — macOS and Linux. CI runs on both via `ci.yml`. WSL supported via installer detection.
- **Tested platforms** — macOS 10.15+ and Linux. CI runs on both via `ci.yml`. WSL supported via installer detection.

## Development Environment Setup

Expand Down Expand Up @@ -853,7 +853,7 @@ Add tests for any new feature or bug fix.
## Version Compatibility

**Minimum requirements:**
- Bash 4.0+ (macOS ships with Bash 3.2 — install via `brew install bash` if needed)
- Bash 3.2+ (works with macOS's stock `/bin/bash`)
- jq 1.6+ (released 2018; widely available)
- Git 2.0+
- Python 3.12 (for Cloud Functions only; hooks are pure Bash)
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

[![Pure Bash](https://img.shields.io/badge/Pure%20Bash-Yes-green?style=flat-square)](https://github.com/renatobardi/hapai)
[![CI/CD](https://img.shields.io/github/actions/workflow/status/renatobardi/hapai/ci.yml?style=flat-square&branch=main)](https://github.com/renatobardi/hapai/actions/workflows/ci.yml)
[![Tests](https://img.shields.io/badge/Tests-133%2F133-green?style=flat-square)](tests/run-tests.sh)
[![Tests](https://img.shields.io/badge/Tests-135%2F135-green?style=flat-square)](tests/run-tests.sh)
[![License](https://img.shields.io/github/license/renatobardi/hapai?style=flat-square)](LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/renatobardi/hapai?style=flat-square)](https://github.com/renatobardi/hapai/releases)

Expand Down Expand Up @@ -374,10 +374,10 @@ Pure bash assertions (no test framework). ~200 assertions covering:

## Requirements

- **bash** (macOS default bash 3.2+ works fine)
- **jq** (JSON parser)
- **bash 3.2+** (works with macOS's stock `/bin/bash`)
- **jq 1.6+** (JSON parser)
- **git** (for guard scripts)
- **Node.js 24+** (for GitHub Actions workflows only)
- **Node.js 20+** (for dashboard development only; not required for hooks)

For cloud logging (optional):
- **gcloud CLI** (Cloud Storage, Cloud Functions, BigQuery)
Expand Down
65 changes: 54 additions & 11 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,36 @@ setup_path() {
local dir="$1"
[[ ":$PATH:" == *":$dir:"* ]] && return 0

# ~/.profile is sourced by bash, zsh, sh, and dash as a login shell — no
# shell-specific config needed
local profile="$HOME/.profile"
# Detect shell and choose appropriate RC file for PATH setup
# For zsh: ~/.zprofile (login shell), or ~/.zshrc (interactive shell)
# For bash: ~/.bash_profile, then ~/.profile, then ~/.bashrc
# Fallback: ~/.profile (POSIX standard)
local profile

if [[ "$SHELL" == *"zsh"* ]]; then
# zsh users: prefer ~/.zprofile (login shell) over ~/.profile
if [[ -f "$HOME/.zprofile" ]]; then
profile="$HOME/.zprofile"
else
profile="$HOME/.zshrc"
fi
elif [[ "$SHELL" == *"bash"* ]]; then
# bash users: check ~/.bash_profile (login shell), fallback to ~/.bashrc
if [[ -f "$HOME/.bash_profile" ]]; then
profile="$HOME/.bash_profile"
else
profile="$HOME/.bashrc"
fi
else
# Other shells: use POSIX ~/.profile
profile="$HOME/.profile"
fi

if ! grep -qF "$dir" "$profile" 2>/dev/null; then
printf '\n# hapai — added by installer\nexport PATH="%s:$PATH"\n' "$dir" >> "$profile"
if ! printf '\n# hapai — added by installer\nexport PATH="%s:$PATH"\n' "$dir" >> "$profile" 2>/dev/null; then
log_warn "Could not update $profile (check file permissions)"
return 0
fi
log_ok "Added $dir to PATH in $profile"
fi
}
Expand Down Expand Up @@ -86,7 +111,7 @@ check_deps() {
exit 1
fi

log_ok "Dependencies: bash, git, jq — OK"
log_ok "Dependencies: git, jq — OK"
}

# ─── Resolve version ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -123,9 +148,18 @@ install_from_source() {
cp -r "$src_dir/templates" "$HAPAI_HOME/"
cp -r "$src_dir/exporters" "$HAPAI_HOME/" 2>/dev/null || true

# Install binary
mkdir -p "$INSTALL_DIR"
cp "$src_dir/bin/hapai" "$INSTALL_DIR/hapai"
# Install binary with fallback on permission error
if ! mkdir -p "$INSTALL_DIR" 2>/dev/null || ! cp "$src_dir/bin/hapai" "$INSTALL_DIR/hapai" 2>/dev/null; then
log_warn "Cannot write to $INSTALL_DIR (permission denied)"

# Fallback: use ~/.local/bin
INSTALL_DIR="$HOME/.local/bin"
log_info "Falling back to $INSTALL_DIR"

mkdir -p "$INSTALL_DIR" || die "Cannot create $INSTALL_DIR either"
cp "$src_dir/bin/hapai" "$INSTALL_DIR/hapai" || die "Failed to copy binary to $INSTALL_DIR"
fi

chmod +x "$INSTALL_DIR/hapai"
setup_path "$INSTALL_DIR"
}
Expand Down Expand Up @@ -188,9 +222,18 @@ install_from_github() {
cp -r "$extracted_dir/templates" "$HAPAI_HOME/"
cp -r "$extracted_dir/exporters" "$HAPAI_HOME/" 2>/dev/null || true

# Install binary
mkdir -p "$INSTALL_DIR"
cp "$extracted_dir/bin/hapai" "$INSTALL_DIR/hapai"
# Install binary with fallback on permission error
if ! mkdir -p "$INSTALL_DIR" 2>/dev/null || ! cp "$extracted_dir/bin/hapai" "$INSTALL_DIR/hapai" 2>/dev/null; then
log_warn "Cannot write to $INSTALL_DIR (permission denied)"

# Fallback: use ~/.local/bin
INSTALL_DIR="$HOME/.local/bin"
log_info "Falling back to $INSTALL_DIR"

mkdir -p "$INSTALL_DIR" || die "Cannot create $INSTALL_DIR either"
cp "$extracted_dir/bin/hapai" "$INSTALL_DIR/hapai" || die "Failed to copy binary to $INSTALL_DIR"
fi

chmod +x "$INSTALL_DIR/hapai"
setup_path "$INSTALL_DIR"
}
Expand Down
63 changes: 63 additions & 0 deletions tests/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,69 @@
rm -f "$WORKFLOW_CONFIG"
cd "$MOCK_REPO" && git checkout main -q 2>/dev/null || true

# ═══════════════════════════════════════════════════════════════════════════
# Installer Tests (install.sh)
# ═══════════════════════════════════════════════════════════════════════════

echo ""
echo -e "${BOLD}Testing installer (install.sh)${NC}"

# Helper: build a sourceable file containing the real setup_path() from install.sh
# Stubs out log_ok/log_warn so their output can be captured cleanly.
_installer_func_file() {

Check warning on line 1359 in tests/run-tests.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=renatobardi_hapai&issues=AZ3Xqxp1j0OJL_jXgxwS&open=AZ3Xqxp1j0OJL_jXgxwS&pullRequest=76
local out="$1"
{
echo 'log_ok() { echo "$*"; }'
echo 'log_warn() { echo "WARN: $*"; }'
awk '/^setup_path\(\) \{/{p=1} p{print} /^\}/{if(p){p=0;exit}}' "$HAPAI_ROOT/install.sh"
} > "$out"
}

# Test: setup_path() detects zsh and uses .zprofile when it exists
test_zsh_path_detection() {

Check warning on line 1369 in tests/run-tests.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=renatobardi_hapai&issues=AZ3Xqxp1j0OJL_jXgxwT&open=AZ3Xqxp1j0OJL_jXgxwT&pullRequest=76
local temp_home install_dir func_file output
temp_home="$(mktemp -d)"
install_dir="$(mktemp -d)" # unique dir guaranteed not to be in PATH
func_file="$(mktemp)"
_installer_func_file "$func_file"

touch "$temp_home/.zprofile"

output="$(HOME="$temp_home" SHELL="/bin/zsh" \
bash -c "source '$func_file'; setup_path '$install_dir'" 2>/dev/null)"

assert_contains "$output" ".zprofile" "setup_path() detects zsh and uses .zprofile"
rm -rf "$temp_home" "$install_dir" "$func_file"
}

# Test: setup_path() uses .bash_profile for bash when it exists, falls back to .bashrc
test_bash_path_detection() {

Check warning on line 1386 in tests/run-tests.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=renatobardi_hapai&issues=AZ3Xqxp1j0OJL_jXgxwU&open=AZ3Xqxp1j0OJL_jXgxwU&pullRequest=76
local temp_home install_dir func_file output_with output_without
temp_home="$(mktemp -d)"
func_file="$(mktemp)"
_installer_func_file "$func_file"

# Branch 1: .bash_profile present — should use it
install_dir="$(mktemp -d)"
touch "$temp_home/.bash_profile"
output_with="$(HOME="$temp_home" SHELL="/bin/bash" \
bash -c "source '$func_file'; setup_path '$install_dir'" 2>/dev/null)"
assert_contains "$output_with" ".bash_profile" "setup_path() uses .bash_profile for bash when present"

# Branch 2: no .bash_profile — should fall back to .bashrc
install_dir="$(mktemp -d)"
rm -f "$temp_home/.bash_profile"
output_without="$(HOME="$temp_home" SHELL="/bin/bash" \
bash -c "source '$func_file'; setup_path '$install_dir'" 2>/dev/null)"
assert_contains "$output_without" ".bashrc" "setup_path() falls back to .bashrc when .bash_profile absent"

rm -rf "$temp_home" "$install_dir" "$func_file"
}

# Run installer tests
test_zsh_path_detection
test_bash_path_detection

# ═══════════════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════════════
Expand Down
Loading