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
207 changes: 69 additions & 138 deletions .agents/scripts/linters-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,11 @@
# Use this for pre-commit checks and fast feedback during development.
#
# Checks performed:
# - shfmt for shell script formatting (pre-pass, non-blocking)
# - ShellCheck for shell scripts (batch mode for speed)
# - ShellCheck for shell scripts
# - Secretlint for exposed secrets
# - Pattern validation (return statements, positional parameters)
# - Markdown formatting
#
# Environment variables:
# LINTERS_DIFF_ONLY=true - Only check modified files (faster for large repos)
#
# Usage:
# ./linters-local.sh # Full check
# LINTERS_DIFF_ONLY=true ./linters-local.sh # Check only modified files
#
# For remote auditing (CodeRabbit, Codacy, SonarCloud), use:
# /code-audit-remote or code-audit-helper.sh
# =============================================================================
Expand Down Expand Up @@ -235,152 +227,91 @@ check_string_literals() {
}

run_shfmt() {
echo -e "${BLUE}Running shfmt Format Check...${NC}"

local violations=0
local diff_only="${LINTERS_DIFF_ONLY:-false}"
local files_to_check=()

# Check if shfmt is installed
if ! command -v shfmt &> /dev/null; then
print_warning "shfmt not installed - skipping format check"
print_info "Install: brew install shfmt"
return 0
fi

# Determine which files to check
if [[ "$diff_only" == "true" ]] && git rev-parse --git-dir > /dev/null 2>&1; then
# Diff-only mode: check only modified .sh files
print_info "Diff-only mode: checking modified .sh files"

local changed_files
changed_files=$(git diff --name-only --diff-filter=ACMR HEAD -- '*.sh' 2>/dev/null || echo "")

if [[ -z "$changed_files" ]]; then
local base_branch
base_branch=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null || echo "")
if [[ -n "$base_branch" ]]; then
changed_files=$(git diff --name-only "$base_branch" HEAD -- '*.sh' 2>/dev/null || echo "")
fi
fi

while IFS= read -r file; do
[[ -n "$file" ]] && [[ -f "$file" ]] && files_to_check+=("$file")
done <<< "$changed_files"

if [[ ${#files_to_check[@]} -eq 0 ]]; then
print_success "shfmt: No modified .sh files to check"
return 0
fi
else
# Full mode: check all .sh files in .agents/scripts/
while IFS= read -r file; do
[[ -f "$file" ]] && files_to_check+=("$file")
done < <(find .agents/scripts/ -name "*.sh" -type f 2>/dev/null)
fi

if [[ ${#files_to_check[@]} -eq 0 ]]; then
print_success "shfmt: No files to check"
echo -e "${BLUE}Running shfmt Syntax Check (fast pre-pass)...${NC}"

if ! command -v shfmt &>/dev/null; then
print_warning "shfmt not installed (install: brew install shfmt)"
return 0
fi

print_info "Checking ${#files_to_check[@]} file(s) for formatting..."

# Run shfmt in diff mode to check formatting
local unformatted_files=()
for file in "${files_to_check[@]}"; do
if ! shfmt -d "$file" > /dev/null 2>&1; then
unformatted_files+=("$file")
fi

local violations=0
local files_checked=0

# Collect shell files
local sh_files=()
for file in .agents/scripts/*.sh; do
[[ -f "$file" ]] && sh_files+=("$file")
done

if [[ ${#unformatted_files[@]} -gt 0 ]]; then
violations=${#unformatted_files[@]}
print_warning "shfmt: $violations file(s) need formatting"
for file in "${unformatted_files[@]}"; do
echo " - $file"
done
print_info "Fix with: shfmt -w ${unformatted_files[*]}"
# Don't fail on formatting issues, just warn
files_checked=${#sh_files[@]}

if [[ $files_checked -eq 0 ]]; then
print_success "shfmt: No shell files to check"
return 0
fi

# Batch check: shfmt -l lists files that differ from formatted output (syntax errors)
local result
result=$(shfmt -l "${sh_files[@]}" 2>&1) || true
if [[ -n "$result" ]]; then
violations=$(echo "$result" | wc -l | tr -d ' ')
fi

if [[ $violations -eq 0 ]]; then
print_success "shfmt: $files_checked files passed syntax check"
else
print_success "shfmt: All files properly formatted"
print_warning "shfmt: $violations files have formatting differences (advisory)"
echo "$result" | head -5
if [[ $violations -gt 5 ]]; then
echo "... and $((violations - 5)) more"
fi
print_info "Auto-fix: shfmt -w .agents/scripts/*.sh"
fi


# shfmt is advisory, not blocking
return 0
}

run_shellcheck() {
echo -e "${BLUE}Running ShellCheck Validation...${NC}"

local violations=0
local diff_only="${LINTERS_DIFF_ONLY:-false}"
local files_to_check=()

# Determine which files to check
if [[ "$diff_only" == "true" ]] && git rev-parse --git-dir > /dev/null 2>&1; then
# Diff-only mode: check only modified .sh files
print_info "Diff-only mode: checking modified .sh files"

# Get uncommitted changes (staged + unstaged)
local changed_files
changed_files=$(git diff --name-only --diff-filter=ACMR HEAD -- '*.sh' 2>/dev/null || echo "")

# If no uncommitted changes, check branch diff vs main
if [[ -z "$changed_files" ]]; then
local base_branch
base_branch=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null || echo "")
if [[ -n "$base_branch" ]]; then
changed_files=$(git diff --name-only "$base_branch" HEAD -- '*.sh' 2>/dev/null || echo "")
fi
fi

# Convert to array
while IFS= read -r file; do
[[ -n "$file" ]] && [[ -f "$file" ]] && files_to_check+=("$file")
done <<< "$changed_files"

if [[ ${#files_to_check[@]} -eq 0 ]]; then
print_success "ShellCheck: No modified .sh files to check"
return 0
fi
else
# Full mode: check all .sh files in .agents/scripts/
while IFS= read -r file; do
[[ -f "$file" ]] && files_to_check+=("$file")
done < <(find .agents/scripts/ -name "*.sh" -type f 2>/dev/null)
if ! command -v shellcheck &>/dev/null; then
print_warning "shellcheck not installed (install: brew install shellcheck)"
return 0
fi

if [[ ${#files_to_check[@]} -eq 0 ]]; then
print_success "ShellCheck: No files to check"

# Collect shell files
local sh_files=()
for file in .agents/scripts/*.sh; do
[[ -f "$file" ]] && sh_files+=("$file")
done

if [[ ${#sh_files[@]} -eq 0 ]]; then
print_success "ShellCheck: No shell files to check"
return 0
fi

print_info "Checking ${#files_to_check[@]} file(s)..."

# Batch shellcheck: pass all files at once for faster execution
# shellcheck disable=SC2086
if command -v shellcheck &> /dev/null; then
local result
result=$(shellcheck --severity=warning -x "${files_to_check[@]}" 2>&1) || true

if [[ -n "$result" ]]; then
# Count files with violations
violations=$(echo "$result" | grep -c "^In " || echo "0")
print_error "ShellCheck: $violations file(s) with violations"
echo "$result" | head -20
if [[ $(echo "$result" | wc -l) -gt 20 ]]; then
echo "... (output truncated, run shellcheck directly for full results)"
fi
return 1
else
print_success "ShellCheck: No violations found"

# Batch mode: pass all files to a single shellcheck invocation
# This is significantly faster than per-file invocation (one process vs N)
local violations=0
local result
result=$(shellcheck --severity=warning --format=gcc "${sh_files[@]}" 2>&1) || true

if [[ -n "$result" ]]; then
# Count unique files with violations
violations=$(echo "$result" | cut -d: -f1 | sort -u | wc -l | tr -d ' ')
local issue_count
issue_count=$(echo "$result" | wc -l | tr -d ' ')

print_error "ShellCheck: $violations files with $issue_count issues"
# Show first few issues
echo "$result" | head -10
if [[ $issue_count -gt 10 ]]; then
echo "... and $((issue_count - 10)) more"
fi
else
print_warning "ShellCheck not installed - skipping"
print_info "Install: brew install shellcheck"
return 1
fi

print_success "ShellCheck: ${#sh_files[@]} files passed (no warnings)"
return 0
}

Expand Down Expand Up @@ -688,7 +619,7 @@ main() {
check_string_literals || exit_code=1
echo ""

run_shfmt || exit_code=1
run_shfmt
echo ""

run_shellcheck || exit_code=1
Expand Down
Loading