diff --git a/.agents/scripts/linters-local.sh b/.agents/scripts/linters-local.sh index 99eebb775..dacfa3eb7 100755 --- a/.agents/scripts/linters-local.sh +++ b/.agents/scripts/linters-local.sh @@ -36,117 +36,128 @@ readonly MAX_POSITIONAL_ISSUES=300 readonly MAX_STRING_LITERAL_ISSUES=2300 print_header() { - echo -e "${BLUE}Local Linters - Fast Offline Quality Checks${NC}" - echo -e "${BLUE}================================================================${NC}" - return 0 + echo -e "${BLUE}Local Linters - Fast Offline Quality Checks${NC}" + echo -e "${BLUE}================================================================${NC}" + return 0 +} + +# Collect all shell scripts to lint, including modularised subdirectories +# (e.g. memory/, supervisor-modules/) but excluding _archive/. +# Populates the ALL_SH_FILES array for use by check functions. +collect_shell_files() { + ALL_SH_FILES=() + while IFS= read -r -d '' f; do + ALL_SH_FILES+=("$f") + done < <(find .agents/scripts -name "*.sh" -not -path "*/_archive/*" -print0 2>/dev/null | sort -z) + return 0 } check_sonarcloud_status() { - echo -e "${BLUE}Checking SonarCloud Status (remote API)...${NC}" - - local response - if response=$(curl -s "https://sonarcloud.io/api/issues/search?componentKeys=marcusquinn_aidevops&impactSoftwareQualities=MAINTAINABILITY&resolved=false&ps=1"); then - local total_issues - total_issues=$(echo "$response" | jq -r '.total // 0') - - echo "Total Issues: $total_issues" - - if [[ $total_issues -le $MAX_TOTAL_ISSUES ]]; then - print_success "SonarCloud: $total_issues issues (within threshold of $MAX_TOTAL_ISSUES)" - else - print_warning "SonarCloud: $total_issues issues (exceeds threshold of $MAX_TOTAL_ISSUES)" - fi - - # Get detailed breakdown - local breakdown_response - if breakdown_response=$(curl -s "https://sonarcloud.io/api/issues/search?componentKeys=marcusquinn_aidevops&impactSoftwareQualities=MAINTAINABILITY&resolved=false&ps=10&facets=rules"); then - echo "Issue Breakdown:" - echo "$breakdown_response" | jq -r '.facets[0].values[] | " \(.val): \(.count) issues"' - fi - else - print_error "Failed to fetch SonarCloud status" - return 1 - fi - - return 0 + echo -e "${BLUE}Checking SonarCloud Status (remote API)...${NC}" + + local response + if response=$(curl -s "https://sonarcloud.io/api/issues/search?componentKeys=marcusquinn_aidevops&impactSoftwareQualities=MAINTAINABILITY&resolved=false&ps=1"); then + local total_issues + total_issues=$(echo "$response" | jq -r '.total // 0') + + echo "Total Issues: $total_issues" + + if [[ $total_issues -le $MAX_TOTAL_ISSUES ]]; then + print_success "SonarCloud: $total_issues issues (within threshold of $MAX_TOTAL_ISSUES)" + else + print_warning "SonarCloud: $total_issues issues (exceeds threshold of $MAX_TOTAL_ISSUES)" + fi + + # Get detailed breakdown + local breakdown_response + if breakdown_response=$(curl -s "https://sonarcloud.io/api/issues/search?componentKeys=marcusquinn_aidevops&impactSoftwareQualities=MAINTAINABILITY&resolved=false&ps=10&facets=rules"); then + echo "Issue Breakdown:" + echo "$breakdown_response" | jq -r '.facets[0].values[] | " \(.val): \(.count) issues"' + fi + else + print_error "Failed to fetch SonarCloud status" + return 1 + fi + + return 0 } check_return_statements() { - echo -e "${BLUE}Checking Return Statements (S7682)...${NC}" - - local violations=0 - local files_checked=0 - - for file in .agents/scripts/*.sh; do - if [[ -f "$file" ]]; then - ((files_checked++)) - - # Count multi-line functions (exclude one-liners like: func() { echo "x"; }) - # One-liners don't need explicit return statements - local functions_count - functions_count=$(grep -c "^[a-zA-Z_][a-zA-Z0-9_]*() {$" "$file" 2>/dev/null || echo "0") - - # Count all return patterns: return 0, return 1, return $var, return $((expr)) - local return_statements - return_statements=$(grep -cE "return [0-9]+|return \\\$" "$file" 2>/dev/null || echo "0") - - # Also count exit statements at script level (exit 0, exit $?) - local exit_statements - exit_statements=$(grep -cE "^exit [0-9]+|^exit \\\$" "$file" 2>/dev/null || echo "0") - - # Ensure variables are numeric - functions_count=${functions_count//[^0-9]/} - return_statements=${return_statements//[^0-9]/} - exit_statements=${exit_statements//[^0-9]/} - functions_count=${functions_count:-0} - return_statements=${return_statements:-0} - exit_statements=${exit_statements:-0} - - # Total returns = return statements + exit statements (for main) - local total_returns=$((return_statements + exit_statements)) - - if [[ $total_returns -lt $functions_count ]]; then - ((violations++)) - print_warning "Missing return statements in $file" - fi - fi - done - - echo "Files checked: $files_checked" - echo "Files with violations: $violations" - - if [[ $violations -le $MAX_RETURN_ISSUES ]]; then - print_success "Return statements: $violations violations (within threshold)" - else - print_error "Return statements: $violations violations (exceeds threshold of $MAX_RETURN_ISSUES)" - return 1 - fi - - return 0 + echo -e "${BLUE}Checking Return Statements (S7682)...${NC}" + + local violations=0 + local files_checked=0 + + for file in "${ALL_SH_FILES[@]}"; do + [[ -f "$file" ]] || continue + ((files_checked++)) + + # Count multi-line functions (exclude one-liners like: func() { echo "x"; }) + # One-liners don't need explicit return statements + local functions_count + functions_count=$(grep -c "^[a-zA-Z_][a-zA-Z0-9_]*() {$" "$file" 2>/dev/null || echo "0") + + # Count all return patterns: return 0, return 1, return $var, return $((expr)) + local return_statements + return_statements=$(grep -cE "return [0-9]+|return \\\$" "$file" 2>/dev/null || echo "0") + + # Also count exit statements at script level (exit 0, exit $?) + local exit_statements + exit_statements=$(grep -cE "^exit [0-9]+|^exit \\\$" "$file" 2>/dev/null || echo "0") + + # Ensure variables are numeric + functions_count=${functions_count//[^0-9]/} + return_statements=${return_statements//[^0-9]/} + exit_statements=${exit_statements//[^0-9]/} + functions_count=${functions_count:-0} + return_statements=${return_statements:-0} + exit_statements=${exit_statements:-0} + + # Total returns = return statements + exit statements (for main) + local total_returns=$((return_statements + exit_statements)) + + if [[ $total_returns -lt $functions_count ]]; then + ((violations++)) + print_warning "Missing return statements in $file" + fi + done + + echo "Files checked: $files_checked" + echo "Files with violations: $violations" + + if [[ $violations -le $MAX_RETURN_ISSUES ]]; then + print_success "Return statements: $violations violations (within threshold)" + else + print_error "Return statements: $violations violations (exceeds threshold of $MAX_RETURN_ISSUES)" + return 1 + fi + + return 0 } check_positional_parameters() { - echo -e "${BLUE}Checking Positional Parameters (S7679)...${NC}" - - local violations=0 - - # Find direct usage of positional parameters inside functions (not in local assignments) - # Exclude: heredocs (<<), awk scripts, main script body, and local assignments - local tmp_file - tmp_file=$(mktemp) - _save_cleanup_scope; trap '_run_cleanups' RETURN - push_cleanup "rm -f '${tmp_file}'" - - # Only check inside function bodies, exclude heredocs, awk/sed patterns, and comments - for file in .agents/scripts/*.sh; do - if [[ -f "$file" ]]; then - # Use awk to find $1-$9 usage inside functions, excluding: - # - local assignments (local var="$1") - # - heredocs (<> "$tmp_file" - fi - done - - if [[ -s "$tmp_file" ]]; then - violations=$(wc -l < "$tmp_file") - violations=${violations//[^0-9]/} - violations=${violations:-0} - - if [[ $violations -gt 0 ]]; then - print_warning "Found $violations positional parameter violations:" - head -10 "$tmp_file" - if [[ $violations -gt 10 ]]; then - echo "... and $((violations - 10)) more" - fi - fi - fi - - rm -f "$tmp_file" - - if [[ $violations -le $MAX_POSITIONAL_ISSUES ]]; then - print_success "Positional parameters: $violations violations (within threshold)" - else - print_error "Positional parameters: $violations violations (exceeds threshold of $MAX_POSITIONAL_ISSUES)" - return 1 - fi - - return 0 + ' "$file" >>"$tmp_file" + fi + done + + if [[ -s "$tmp_file" ]]; then + violations=$(wc -l <"$tmp_file") + violations=${violations//[^0-9]/} + violations=${violations:-0} + + if [[ $violations -gt 0 ]]; then + print_warning "Found $violations positional parameter violations:" + head -10 "$tmp_file" + if [[ $violations -gt 10 ]]; then + echo "... and $((violations - 10)) more" + fi + fi + fi + + rm -f "$tmp_file" + + if [[ $violations -le $MAX_POSITIONAL_ISSUES ]]; then + print_success "Positional parameters: $violations violations (within threshold)" + else + print_error "Positional parameters: $violations violations (exceeds threshold of $MAX_POSITIONAL_ISSUES)" + return 1 + fi + + return 0 } check_string_literals() { - echo -e "${BLUE}Checking String Literals (S1192)...${NC}" - - local violations=0 - - for file in .agents/scripts/*.sh; do - if [[ -f "$file" ]]; then - # Find strings that appear 3 or more times - local repeated_strings - repeated_strings=$(grep -o '"[^"]*"' "$file" | sort | uniq -c | awk '$1 >= 3 {print $1, $2}' | wc -l) - - if [[ $repeated_strings -gt 0 ]]; then - ((violations += repeated_strings)) - print_warning "$file has $repeated_strings repeated string literals" - fi - fi - done - - if [[ $violations -le $MAX_STRING_LITERAL_ISSUES ]]; then - print_success "String literals: $violations violations (within threshold)" - else - print_error "String literals: $violations violations (exceeds threshold of $MAX_STRING_LITERAL_ISSUES)" - return 1 - fi - - return 0 + echo -e "${BLUE}Checking String Literals (S1192)...${NC}" + + local violations=0 + + for file in "${ALL_SH_FILES[@]}"; do + [[ -f "$file" ]] || continue + # Find strings that appear 3 or more times + local repeated_strings + repeated_strings=$(grep -o '"[^"]*"' "$file" | sort | uniq -c | awk '$1 >= 3 {print $1, $2}' | wc -l) + + if [[ $repeated_strings -gt 0 ]]; then + ((violations += repeated_strings)) + print_warning "$file has $repeated_strings repeated string literals" + fi + done + + if [[ $violations -le $MAX_STRING_LITERAL_ISSUES ]]; then + print_success "String literals: $violations violations (within threshold)" + else + print_error "String literals: $violations violations (exceeds threshold of $MAX_STRING_LITERAL_ISSUES)" + return 1 + fi + + return 0 } run_shfmt() { - 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 - - 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 - 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_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 + 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 + + local violations=0 + local files_checked=0 + + files_checked=${#ALL_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 "${ALL_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_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: find .agents/scripts -name '*.sh' -not -path '*/_archive/*' -exec shfmt -w {} +" + fi + + # shfmt is advisory, not blocking + return 0 } run_shellcheck() { - echo -e "${BLUE}Running ShellCheck Validation...${NC}" - - if ! command -v shellcheck &>/dev/null; then - print_warning "shellcheck not installed (install: brew install shellcheck)" - return 0 - fi - - # 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 - - # 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 - return 1 - fi - - print_success "ShellCheck: ${#sh_files[@]} files passed (no warnings)" - return 0 + echo -e "${BLUE}Running ShellCheck Validation...${NC}" + + if ! command -v shellcheck &>/dev/null; then + print_warning "shellcheck not installed (install: brew install shellcheck)" + return 0 + fi + + if [[ ${#ALL_SH_FILES[@]} -eq 0 ]]; then + print_success "ShellCheck: No shell files to check" + return 0 + fi + + # 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 -x --severity=warning --format=gcc "${ALL_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 + return 1 + fi + + print_success "ShellCheck: ${#ALL_SH_FILES[@]} files passed (no warnings)" + return 0 } # Check for secrets in codebase check_secrets() { - echo -e "${BLUE}Checking for Exposed Secrets (Secretlint)...${NC}" - - local secretlint_script=".agents/scripts/secretlint-helper.sh" - local violations=0 - - # Check if secretlint is available (global, local, or main repo for worktrees) - local secretlint_cmd="" - if command -v secretlint &> /dev/null; then - secretlint_cmd="secretlint" - elif [[ -f "node_modules/.bin/secretlint" ]]; then - secretlint_cmd="./node_modules/.bin/secretlint" - else - # Check main repo node_modules (handles git worktrees) - local repo_root - repo_root=$(git rev-parse --git-common-dir 2>/dev/null | xargs -I{} sh -c 'cd "{}/.." && pwd' 2>/dev/null || echo "") - if [[ -n "$repo_root" ]] && [[ "$repo_root" != "$(pwd)" ]] && [[ -f "$repo_root/node_modules/.bin/secretlint" ]]; then - secretlint_cmd="$repo_root/node_modules/.bin/secretlint" - fi - fi - - if [[ -n "$secretlint_cmd" ]]; then - - if [[ -f ".secretlintrc.json" ]]; then - # Run scan and capture exit code - if $secretlint_cmd "**/*" --format compact 2>/dev/null; then - print_success "Secretlint: No secrets detected" - else - violations=1 - print_error "Secretlint: Potential secrets detected!" - print_info "Run: bash $secretlint_script scan (for detailed results)" - fi - else - print_warning "Secretlint: Configuration not found" - print_info "Run: bash $secretlint_script init" - fi - elif command -v docker &> /dev/null; then - local timeout_sec=60 - # Use gtimeout (macOS) or timeout (Linux) to prevent Docker from hanging - local timeout_cmd="" - if command -v gtimeout &> /dev/null; then - timeout_cmd="gtimeout ${timeout_sec}" - elif command -v timeout &> /dev/null; then - timeout_cmd="timeout ${timeout_sec}" - fi - - if [[ -n "$timeout_cmd" ]]; then - print_info "Secretlint: Using Docker for scan (${timeout_sec}s timeout)..." - else - print_info "Secretlint: Using Docker for scan (no timeout available)..." - fi - - local docker_result - if [[ -n "$timeout_cmd" ]]; then - docker_result=$($timeout_cmd docker run --init -v "$(pwd)":"$(pwd)" -w "$(pwd)" --rm secretlint/secretlint secretlint "**/*" --format compact 2>&1) || true - else - # No timeout available, run without (may hang on large repos) - docker_result=$(docker run --init -v "$(pwd)":"$(pwd)" -w "$(pwd)" --rm secretlint/secretlint secretlint "**/*" --format compact 2>&1) || true - fi - - if [[ -z "$docker_result" ]] || [[ "$docker_result" == *"0 problems"* ]]; then - print_success "Secretlint: No secrets detected" - elif [[ "$docker_result" == *"timed out"* ]] || [[ "$docker_result" == *"timeout"* ]]; then - print_warning "Secretlint: Timed out (skipped)" - print_info "Install native secretlint for faster scans: npm install -g secretlint" - else - violations=1 - print_error "Secretlint: Potential secrets detected!" - fi - else - print_warning "Secretlint: Not installed (install with: npm install secretlint)" - print_info "Run: bash $secretlint_script install" - fi - - return $violations + echo -e "${BLUE}Checking for Exposed Secrets (Secretlint)...${NC}" + + local secretlint_script=".agents/scripts/secretlint-helper.sh" + local violations=0 + + # Check if secretlint is available (global, local, or main repo for worktrees) + local secretlint_cmd="" + if command -v secretlint &>/dev/null; then + secretlint_cmd="secretlint" + elif [[ -f "node_modules/.bin/secretlint" ]]; then + secretlint_cmd="./node_modules/.bin/secretlint" + else + # Check main repo node_modules (handles git worktrees) + local repo_root + repo_root=$(git rev-parse --git-common-dir 2>/dev/null | xargs -I{} sh -c 'cd "{}/.." && pwd' 2>/dev/null || echo "") + if [[ -n "$repo_root" ]] && [[ "$repo_root" != "$(pwd)" ]] && [[ -f "$repo_root/node_modules/.bin/secretlint" ]]; then + secretlint_cmd="$repo_root/node_modules/.bin/secretlint" + fi + fi + + if [[ -n "$secretlint_cmd" ]]; then + + if [[ -f ".secretlintrc.json" ]]; then + # Run scan and capture exit code + if $secretlint_cmd "**/*" --format compact 2>/dev/null; then + print_success "Secretlint: No secrets detected" + else + violations=1 + print_error "Secretlint: Potential secrets detected!" + print_info "Run: bash $secretlint_script scan (for detailed results)" + fi + else + print_warning "Secretlint: Configuration not found" + print_info "Run: bash $secretlint_script init" + fi + elif command -v docker &>/dev/null; then + local timeout_sec=60 + # Use gtimeout (macOS) or timeout (Linux) to prevent Docker from hanging + local timeout_cmd="" + if command -v gtimeout &>/dev/null; then + timeout_cmd="gtimeout ${timeout_sec}" + elif command -v timeout &>/dev/null; then + timeout_cmd="timeout ${timeout_sec}" + fi + + if [[ -n "$timeout_cmd" ]]; then + print_info "Secretlint: Using Docker for scan (${timeout_sec}s timeout)..." + else + print_info "Secretlint: Using Docker for scan (no timeout available)..." + fi + + local docker_result + if [[ -n "$timeout_cmd" ]]; then + docker_result=$($timeout_cmd docker run --init -v "$(pwd)":"$(pwd)" -w "$(pwd)" --rm secretlint/secretlint secretlint "**/*" --format compact 2>&1) || true + else + # No timeout available, run without (may hang on large repos) + docker_result=$(docker run --init -v "$(pwd)":"$(pwd)" -w "$(pwd)" --rm secretlint/secretlint secretlint "**/*" --format compact 2>&1) || true + fi + + if [[ -z "$docker_result" ]] || [[ "$docker_result" == *"0 problems"* ]]; then + print_success "Secretlint: No secrets detected" + elif [[ "$docker_result" == *"timed out"* ]] || [[ "$docker_result" == *"timeout"* ]]; then + print_warning "Secretlint: Timed out (skipped)" + print_info "Install native secretlint for faster scans: npm install -g secretlint" + else + violations=1 + print_error "Secretlint: Potential secrets detected!" + fi + else + print_warning "Secretlint: Not installed (install with: npm install secretlint)" + print_info "Run: bash $secretlint_script install" + fi + + return $violations } # Check AI-Powered Quality CLIs integration check_markdown_lint() { - print_info "Checking Markdown Style..." - - local md_files - local violations=0 - local markdownlint_cmd="" - - # Find markdownlint command - if command -v markdownlint &> /dev/null; then - markdownlint_cmd="markdownlint" - elif [[ -f "node_modules/.bin/markdownlint" ]]; then - markdownlint_cmd="node_modules/.bin/markdownlint" - fi - - # Get markdown files to check: - # 1. Uncommitted changes (staged + unstaged) - BLOCKING - # 2. If no uncommitted, check files changed in current branch vs main - BLOCKING - # 3. Fallback to all tracked .md files in .agents/ - NON-BLOCKING (advisory) - local check_mode="changed" # "changed" = blocking, "all" = advisory - if git rev-parse --git-dir > /dev/null 2>&1; then - # First try uncommitted changes - md_files=$(git diff --name-only --diff-filter=ACMR HEAD -- '*.md' 2>/dev/null) - - # If no uncommitted, check branch diff vs main - if [[ -z "$md_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 - md_files=$(git diff --name-only "$base_branch" HEAD -- '*.md' 2>/dev/null) - fi - fi - - # Fallback: check all .agents/*.md files (advisory only) - if [[ -z "$md_files" ]]; then - md_files=$(git ls-files '.agents/**/*.md' 2>/dev/null) - check_mode="all" - fi - else - md_files=$(find . -name "*.md" -type f 2>/dev/null | grep -v node_modules) - check_mode="all" - fi - - if [[ -z "$md_files" ]]; then - print_success "Markdown: No markdown files to check" - return 0 - fi - - if [[ -n "$markdownlint_cmd" ]]; then - # Run markdownlint and capture output - local lint_output - lint_output=$($markdownlint_cmd $md_files 2>&1) || true - - if [[ -n "$lint_output" ]]; then - # Count violations - ensure single integer (grep -c can fail, use wc -l as fallback) - local violation_count - violation_count=$(echo "$lint_output" | grep -c "MD[0-9]" 2>/dev/null) || violation_count=0 - # Ensure it's a valid integer - if ! [[ "$violation_count" =~ ^[0-9]+$ ]]; then - violation_count=0 - fi - violations=$violation_count - - if [[ $violations -gt 0 ]]; then - # Show violations first (common to both modes) - echo "$lint_output" | head -10 - if [[ $violations -gt 10 ]]; then - echo "... and $((violations - 10)) more" - fi - print_info "Run: markdownlint --fix to auto-fix" - - # Mode-specific message and return code - if [[ "$check_mode" == "changed" ]]; then - print_error "Markdown: $violations style issues in changed files (BLOCKING)" - return 1 - else - print_warning "Markdown: $violations style issues found (advisory)" - return 0 - fi - fi - fi - print_success "Markdown: No style issues found" - else - # Fallback: basic checks without markdownlint - # NOTE: Without markdownlint, we can't reliably detect MD031/MD040 violations - # because we can't distinguish opening fences (need language) from closing fences (always bare) - # So fallback is always advisory-only and recommends installing markdownlint - print_warning "Markdown: markdownlint not installed - cannot perform full lint checks" - print_info "Install: npm install -g markdownlint-cli" - print_info "Then re-run to get blocking checks for changed files" - # Advisory only - don't block without proper tooling - return 0 - fi - - return 0 + print_info "Checking Markdown Style..." + + local md_files + local violations=0 + local markdownlint_cmd="" + + # Find markdownlint command + if command -v markdownlint &>/dev/null; then + markdownlint_cmd="markdownlint" + elif [[ -f "node_modules/.bin/markdownlint" ]]; then + markdownlint_cmd="node_modules/.bin/markdownlint" + fi + + # Get markdown files to check: + # 1. Uncommitted changes (staged + unstaged) - BLOCKING + # 2. If no uncommitted, check files changed in current branch vs main - BLOCKING + # 3. Fallback to all tracked .md files in .agents/ - NON-BLOCKING (advisory) + local check_mode="changed" # "changed" = blocking, "all" = advisory + if git rev-parse --git-dir >/dev/null 2>&1; then + # First try uncommitted changes + md_files=$(git diff --name-only --diff-filter=ACMR HEAD -- '*.md' 2>/dev/null) + + # If no uncommitted, check branch diff vs main + if [[ -z "$md_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 + md_files=$(git diff --name-only "$base_branch" HEAD -- '*.md' 2>/dev/null) + fi + fi + + # Fallback: check all .agents/*.md files (advisory only) + if [[ -z "$md_files" ]]; then + md_files=$(git ls-files '.agents/**/*.md' 2>/dev/null) + check_mode="all" + fi + else + md_files=$(find . -name "*.md" -type f 2>/dev/null | grep -v node_modules) + check_mode="all" + fi + + if [[ -z "$md_files" ]]; then + print_success "Markdown: No markdown files to check" + return 0 + fi + + if [[ -n "$markdownlint_cmd" ]]; then + # Run markdownlint and capture output + local lint_output + lint_output=$($markdownlint_cmd $md_files 2>&1) || true + + if [[ -n "$lint_output" ]]; then + # Count violations - ensure single integer (grep -c can fail, use wc -l as fallback) + local violation_count + violation_count=$(echo "$lint_output" | grep -c "MD[0-9]" 2>/dev/null) || violation_count=0 + # Ensure it's a valid integer + if ! [[ "$violation_count" =~ ^[0-9]+$ ]]; then + violation_count=0 + fi + violations=$violation_count + + if [[ $violations -gt 0 ]]; then + # Show violations first (common to both modes) + echo "$lint_output" | head -10 + if [[ $violations -gt 10 ]]; then + echo "... and $((violations - 10)) more" + fi + print_info "Run: markdownlint --fix to auto-fix" + + # Mode-specific message and return code + if [[ "$check_mode" == "changed" ]]; then + print_error "Markdown: $violations style issues in changed files (BLOCKING)" + return 1 + else + print_warning "Markdown: $violations style issues found (advisory)" + return 0 + fi + fi + fi + print_success "Markdown: No style issues found" + else + # Fallback: basic checks without markdownlint + # NOTE: Without markdownlint, we can't reliably detect MD031/MD040 violations + # because we can't distinguish opening fences (need language) from closing fences (always bare) + # So fallback is always advisory-only and recommends installing markdownlint + print_warning "Markdown: markdownlint not installed - cannot perform full lint checks" + print_info "Install: npm install -g markdownlint-cli" + print_info "Then re-run to get blocking checks for changed files" + # Advisory only - don't block without proper tooling + return 0 + fi + + return 0 } # Check TOON file syntax check_toon_syntax() { - print_info "Checking TOON Syntax..." - - local toon_files - local violations=0 - - # Find .toon files in the repo - if git rev-parse --git-dir > /dev/null 2>&1; then - toon_files=$(git ls-files '*.toon' 2>/dev/null) - else - toon_files=$(find . -name "*.toon" -type f 2>/dev/null | grep -v node_modules) - fi - - if [[ -z "$toon_files" ]]; then - print_success "TOON: No .toon files to check" - return 0 - fi - - local file_count - file_count=$(echo "$toon_files" | wc -l | tr -d ' ') - - # Use toon-lsp check if available, otherwise basic validation - if command -v toon-lsp &> /dev/null; then - while IFS= read -r file; do - if [[ -f "$file" ]]; then - local result - result=$(toon-lsp check "$file" 2>&1) - local exit_code=$? - if [[ $exit_code -ne 0 ]] || [[ "$result" == *"error"* ]]; then - ((violations++)) - print_warning "TOON syntax issue in $file" - fi - fi - done <<< "$toon_files" - else - # Fallback: basic structure validation (non-empty check) - while IFS= read -r file; do - if [[ -f "$file" ]] && [[ ! -s "$file" ]]; then - ((violations++)) - print_warning "TOON: Empty file $file" - fi - done <<< "$toon_files" - fi - - if [[ $violations -eq 0 ]]; then - print_success "TOON: All $file_count files valid" - else - print_warning "TOON: $violations of $file_count files with issues" - fi - - return 0 + print_info "Checking TOON Syntax..." + + local toon_files + local violations=0 + + # Find .toon files in the repo + if git rev-parse --git-dir >/dev/null 2>&1; then + toon_files=$(git ls-files '*.toon' 2>/dev/null) + else + toon_files=$(find . -name "*.toon" -type f 2>/dev/null | grep -v node_modules) + fi + + if [[ -z "$toon_files" ]]; then + print_success "TOON: No .toon files to check" + return 0 + fi + + local file_count + file_count=$(echo "$toon_files" | wc -l | tr -d ' ') + + # Use toon-lsp check if available, otherwise basic validation + if command -v toon-lsp &>/dev/null; then + while IFS= read -r file; do + if [[ -f "$file" ]]; then + local result + result=$(toon-lsp check "$file" 2>&1) + local exit_code=$? + if [[ $exit_code -ne 0 ]] || [[ "$result" == *"error"* ]]; then + ((violations++)) + print_warning "TOON syntax issue in $file" + fi + fi + done <<<"$toon_files" + else + # Fallback: basic structure validation (non-empty check) + while IFS= read -r file; do + if [[ -f "$file" ]] && [[ ! -s "$file" ]]; then + ((violations++)) + print_warning "TOON: Empty file $file" + fi + done <<<"$toon_files" + fi + + if [[ $violations -eq 0 ]]; then + print_success "TOON: All $file_count files valid" + else + print_warning "TOON: $violations of $file_count files with issues" + fi + + return 0 } check_remote_cli_status() { - print_info "Remote Audit CLIs Status (use /code-audit-remote for full analysis)..." - - # Secretlint - local secretlint_script=".agents/scripts/secretlint-helper.sh" - if [[ -f "$secretlint_script" ]]; then - # Check global, local, and main repo node_modules (worktree support) - local sl_found=false - if command -v secretlint &> /dev/null || [[ -f "node_modules/.bin/secretlint" ]]; then - sl_found=true - else - local sl_repo_root - sl_repo_root=$(git rev-parse --git-common-dir 2>/dev/null | xargs -I{} sh -c 'cd "{}/.." && pwd' 2>/dev/null || echo "") - if [[ -n "$sl_repo_root" ]] && [[ "$sl_repo_root" != "$(pwd)" ]] && [[ -f "$sl_repo_root/node_modules/.bin/secretlint" ]]; then - sl_found=true - fi - fi - if [[ "$sl_found" == "true" ]]; then - print_success "Secretlint: Ready" - else - print_info "Secretlint: Available for setup" - fi - fi - - # CodeRabbit CLI - local coderabbit_script=".agents/scripts/coderabbit-cli.sh" - if [[ -f "$coderabbit_script" ]]; then - if bash "$coderabbit_script" status > /dev/null 2>&1; then - print_success "CodeRabbit CLI: Ready" - else - print_info "CodeRabbit CLI: Available for setup" - fi - fi - - # Codacy CLI - local codacy_script=".agents/scripts/codacy-cli.sh" - if [[ -f "$codacy_script" ]]; then - if bash "$codacy_script" status > /dev/null 2>&1; then - print_success "Codacy CLI: Ready" - else - print_info "Codacy CLI: Available for setup" - fi - fi - - # SonarScanner CLI - local sonar_script=".agents/scripts/sonarscanner-cli.sh" - if [[ -f "$sonar_script" ]]; then - if bash "$sonar_script" status > /dev/null 2>&1; then - print_success "SonarScanner CLI: Ready" - else - print_info "SonarScanner CLI: Available for setup" - fi - fi - - return 0 + print_info "Remote Audit CLIs Status (use /code-audit-remote for full analysis)..." + + # Secretlint + local secretlint_script=".agents/scripts/secretlint-helper.sh" + if [[ -f "$secretlint_script" ]]; then + # Check global, local, and main repo node_modules (worktree support) + local sl_found=false + if command -v secretlint &>/dev/null || [[ -f "node_modules/.bin/secretlint" ]]; then + sl_found=true + else + local sl_repo_root + sl_repo_root=$(git rev-parse --git-common-dir 2>/dev/null | xargs -I{} sh -c 'cd "{}/.." && pwd' 2>/dev/null || echo "") + if [[ -n "$sl_repo_root" ]] && [[ "$sl_repo_root" != "$(pwd)" ]] && [[ -f "$sl_repo_root/node_modules/.bin/secretlint" ]]; then + sl_found=true + fi + fi + if [[ "$sl_found" == "true" ]]; then + print_success "Secretlint: Ready" + else + print_info "Secretlint: Available for setup" + fi + fi + + # CodeRabbit CLI + local coderabbit_script=".agents/scripts/coderabbit-cli.sh" + if [[ -f "$coderabbit_script" ]]; then + if bash "$coderabbit_script" status >/dev/null 2>&1; then + print_success "CodeRabbit CLI: Ready" + else + print_info "CodeRabbit CLI: Available for setup" + fi + fi + + # Codacy CLI + local codacy_script=".agents/scripts/codacy-cli.sh" + if [[ -f "$codacy_script" ]]; then + if bash "$codacy_script" status >/dev/null 2>&1; then + print_success "Codacy CLI: Ready" + else + print_info "Codacy CLI: Available for setup" + fi + fi + + # SonarScanner CLI + local sonar_script=".agents/scripts/sonarscanner-cli.sh" + if [[ -f "$sonar_script" ]]; then + if bash "$sonar_script" status >/dev/null 2>&1; then + print_success "SonarScanner CLI: Ready" + else + print_info "SonarScanner CLI: Available for setup" + fi + fi + + return 0 } main() { - print_header + print_header + + local exit_code=0 - local exit_code=0 + # Collect shell files once (includes modularised subdirectories, excludes _archive/) + collect_shell_files - # Run all local quality checks - check_sonarcloud_status || exit_code=1 - echo "" + # Run all local quality checks + check_sonarcloud_status || exit_code=1 + echo "" - check_return_statements || exit_code=1 - echo "" + check_return_statements || exit_code=1 + echo "" - check_positional_parameters || exit_code=1 - echo "" + check_positional_parameters || exit_code=1 + echo "" - check_string_literals || exit_code=1 - echo "" + check_string_literals || exit_code=1 + echo "" - run_shfmt - echo "" + run_shfmt + echo "" - run_shellcheck || exit_code=1 - echo "" + run_shellcheck || exit_code=1 + echo "" - check_secrets || exit_code=1 - echo "" + check_secrets || exit_code=1 + echo "" - check_markdown_lint || exit_code=1 - echo "" + check_markdown_lint || exit_code=1 + echo "" - check_toon_syntax || exit_code=1 - echo "" + check_toon_syntax || exit_code=1 + echo "" - check_remote_cli_status - echo "" + check_remote_cli_status + echo "" - # Final summary - if [[ $exit_code -eq 0 ]]; then - print_success "ALL LOCAL CHECKS PASSED!" - print_info "For remote auditing, run: /code-audit-remote" - else - print_error "QUALITY ISSUES DETECTED. Please address violations before committing." - fi + # Final summary + if [[ $exit_code -eq 0 ]]; then + print_success "ALL LOCAL CHECKS PASSED!" + print_info "For remote auditing, run: /code-audit-remote" + else + print_error "QUALITY ISSUES DETECTED. Please address violations before committing." + fi - return $exit_code + return $exit_code } main "$@" diff --git a/.agents/scripts/quality-fix.sh b/.agents/scripts/quality-fix.sh index 8556d0bda..510102b9c 100755 --- a/.agents/scripts/quality-fix.sh +++ b/.agents/scripts/quality-fix.sh @@ -17,174 +17,175 @@ readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color print_header() { - echo -e "${BLUE}🔧 AI DevOps Framework - Universal Quality Fix${NC}" - echo -e "${BLUE}==========================================================${NC}" - return 0 + echo -e "${BLUE}🔧 AI DevOps Framework - Universal Quality Fix${NC}" + echo -e "${BLUE}==========================================================${NC}" + return 0 } print_success() { - local message="$1" - echo -e "${GREEN}✅ $message${NC}" - return 0 + local message="$1" + echo -e "${GREEN}✅ $message${NC}" + return 0 } print_warning() { - local message="$1" - echo -e "${YELLOW}âš ī¸ $message${NC}" - return 0 + local message="$1" + echo -e "${YELLOW}âš ī¸ $message${NC}" + return 0 } print_error() { - local message="$1" - echo -e "${RED}❌ $message${NC}" - return 0 + local message="$1" + echo -e "${RED}❌ $message${NC}" + return 0 } print_info() { - local message="$1" - echo -e "${BLUE}â„šī¸ $message${NC}" - return 0 + local message="$1" + echo -e "${BLUE}â„šī¸ $message${NC}" + return 0 } backup_files() { - print_info "Creating backup of provider files..." + print_info "Creating backup of provider files..." - local backup_dir="backups/$(date +%Y%m%d_%H%M%S)" - mkdir -p "$backup_dir" + local backup_dir="backups/$(date +%Y%m%d_%H%M%S)" + mkdir -p "$backup_dir" - cp .agents/scripts/*.sh "$backup_dir/" - print_success "Backup created in $backup_dir" - return 0 + cp .agents/scripts/*.sh "$backup_dir/" + # Also backup modularised subdirectory scripts + find .agents/scripts -mindepth 2 -name "*.sh" -not -path "*/_archive/*" -exec cp --parents {} "$backup_dir/" \; 2>/dev/null || true + print_success "Backup created in $backup_dir" + return 0 } fix_return_statements() { - print_info "Fixing missing return statements (S7682)..." - - local files_fixed=0 - - for file in .agents/scripts/*.sh; do - if [[ -f "$file" ]]; then - # Find functions that don't end with return statement - local temp_file - temp_file=$(mktemp) - local in_function=false - local function_name="" - local brace_count=0 - local fixed_functions=0 - - while IFS= read -r line; do - echo "$line" >> "$temp_file" - - # Detect function start - if [[ $line =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\)[[:space:]]*\{ ]]; then - in_function=true - function_name=$(echo "$line" | sed 's/().*//') - brace_count=1 - elif [[ $in_function == true ]]; then - # Count braces to track function scope - local open_braces - open_braces=$(echo "$line" | grep -o '{' | wc -l 2>/dev/null || echo "0") - local close_braces - close_braces=$(echo "$line" | grep -o '}' | wc -l 2>/dev/null || echo "0") - - # Ensure variables are numeric - open_braces=${open_braces//[^0-9]/} - close_braces=${close_braces//[^0-9]/} - open_braces=${open_braces:-0} - close_braces=${close_braces:-0} - - # Fix arithmetic expansion - local diff - diff=$((open_braces - close_braces)) - brace_count=$((brace_count + diff)) - - # Check if function is ending - if [[ $brace_count -eq 0 && $line == "}" ]]; then - # Check if previous line has return statement - local last_line - last_line=$(tail -2 "$temp_file" | head -1) - - if [[ ! $last_line =~ return[[:space:]]+[01] ]]; then - # Remove the closing brace and add return statement - sed_inplace '$ d' "$temp_file" - echo " return 0" >> "$temp_file" - echo "}" >> "$temp_file" - ((fixed_functions++)) - print_info "Fixed function: $function_name" - fi - - in_function=false - function_name="" - fi - fi - done < "$file" - - if [[ $fixed_functions -gt 0 ]]; then - mv "$temp_file" "$file" - ((files_fixed++)) - print_success "Fixed $fixed_functions functions in $file" - else - rm -f "$temp_file" - fi - fi - done - - print_success "Return statements: Fixed $files_fixed files" - return 0 + print_info "Fixing missing return statements (S7682)..." + + local files_fixed=0 + + while IFS= read -r -d '' file; do + if [[ -f "$file" ]]; then + # Find functions that don't end with return statement + local temp_file + temp_file=$(mktemp) + local in_function=false + local function_name="" + local brace_count=0 + local fixed_functions=0 + + while IFS= read -r line; do + echo "$line" >>"$temp_file" + + # Detect function start + if [[ $line =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\)[[:space:]]*\{ ]]; then + in_function=true + function_name=$(echo "$line" | sed 's/().*//') + brace_count=1 + elif [[ $in_function == true ]]; then + # Count braces to track function scope + local open_braces + open_braces=$(echo "$line" | grep -o '{' | wc -l 2>/dev/null || echo "0") + local close_braces + close_braces=$(echo "$line" | grep -o '}' | wc -l 2>/dev/null || echo "0") + + # Ensure variables are numeric + open_braces=${open_braces//[^0-9]/} + close_braces=${close_braces//[^0-9]/} + open_braces=${open_braces:-0} + close_braces=${close_braces:-0} + + # Fix arithmetic expansion + local diff + diff=$((open_braces - close_braces)) + brace_count=$((brace_count + diff)) + + # Check if function is ending + if [[ $brace_count -eq 0 && $line == "}" ]]; then + # Check if previous line has return statement + local last_line + last_line=$(tail -2 "$temp_file" | head -1) + + if [[ ! $last_line =~ return[[:space:]]+[01] ]]; then + # Remove the closing brace and add return statement + sed_inplace '$ d' "$temp_file" + echo " return 0" >>"$temp_file" + echo "}" >>"$temp_file" + ((fixed_functions++)) + print_info "Fixed function: $function_name" + fi + + in_function=false + function_name="" + fi + fi + done <"$file" + + if [[ $fixed_functions -gt 0 ]]; then + mv "$temp_file" "$file" + ((files_fixed++)) + print_success "Fixed $fixed_functions functions in $file" + else + rm -f "$temp_file" + fi + fi + done < <(find .agents/scripts -name "*.sh" -not -path "*/_archive/*" -print0 2>/dev/null) + + print_success "Return statements: Fixed $files_fixed files" + return 0 } fix_positional_parameters() { - print_info "Fixing positional parameter violations (S7679)..." - - local files_fixed=0 - - for file in .agents/scripts/*.sh; do - if [[ -f "$file" ]]; then - local temp_file - temp_file=$(mktemp) - - - # Process main() functions specifically - if grep -q "^main() {" "$file"; then - # Add local variable assignments to main function - sed '/^main() {/,/^}$/ { + print_info "Fixing positional parameter violations (S7679)..." + + local files_fixed=0 + + while IFS= read -r -d '' file; do + if [[ -f "$file" ]]; then + local temp_file + temp_file=$(mktemp) + + # Process main() functions specifically + if grep -q "^main() {" "$file"; then + # Add local variable assignments to main function + sed '/^main() {/,/^}$/ { /^main() {/a\ # Assign positional parameters to local variables\ local command="${1:-help}"\ local account_name="$2"\ local target="$3"\ local options="$4" - }' "$file" > "$temp_file" - - # Replace direct positional parameter usage in case statements - sed_inplace 's/\$_arg1/$command/g; s/\$2/$account_name/g; s/\$3/$target/g; s/\$4/$options/g' "$temp_file" - - if ! diff -q "$file" "$temp_file" > /dev/null; then - mv "$temp_file" "$file" - ((files_fixed++)) - print_success "Fixed positional parameters in main() function of $file" - else - rm -f "$temp_file" - fi - fi - fi - done - - print_success "Positional parameters: Fixed $files_fixed files" - return 0 + }' "$file" >"$temp_file" + + # Replace direct positional parameter usage in case statements + sed_inplace 's/\$_arg1/$command/g; s/\$2/$account_name/g; s/\$3/$target/g; s/\$4/$options/g' "$temp_file" + + if ! diff -q "$file" "$temp_file" >/dev/null; then + mv "$temp_file" "$file" + ((files_fixed++)) + print_success "Fixed positional parameters in main() function of $file" + else + rm -f "$temp_file" + fi + fi + fi + done < <(find .agents/scripts -name "*.sh" -not -path "*/_archive/*" -print0 2>/dev/null) + + print_success "Positional parameters: Fixed $files_fixed files" + return 0 } analyze_string_literals() { - print_info "Analyzing string literals for constants (S1192)..." - - local constants_file - constants_file=$(mktemp) - - for file in .agents/scripts/*.sh; do - if [[ -f "$file" ]]; then - echo "=== $file ===" >> "$constants_file" - - # Find repeated strings (3+ occurrences) - (grep -o '"[^"]*"' "$file" || true) | sort | uniq -c | sort -nr | awk '$_arg1 >= 3 { + print_info "Analyzing string literals for constants (S1192)..." + + local constants_file + constants_file=$(mktemp) + + while IFS= read -r -d '' file; do + if [[ -f "$file" ]]; then + echo "=== $file ===" >>"$constants_file" + + # Find repeated strings (3+ occurrences) + (grep -o '"[^"]*"' "$file" || true) | sort | uniq -c | sort -nr | awk '$_arg1 >= 3 { gsub(/"/, "", $2) constant_name = toupper($2) gsub(/[^A-Z0-9_]/, "_", constant_name) @@ -193,74 +194,74 @@ analyze_string_literals() { if (length(constant_name) > 0) { printf "readonly %s=\"%s\" # Used %d times\n", constant_name, $2, $_arg1 } - }' >> "$constants_file" - - echo "" >> "$constants_file" - fi - done - - if [[ -s "$constants_file" ]]; then - print_warning "String literal constants needed:" - cat "$constants_file" - print_info "Add these constants to the top of respective files and replace string literals" - else - print_success "No repeated string literals found" - fi - - rm -f "$constants_file" - return 0 + }' >>"$constants_file" + + echo "" >>"$constants_file" + fi + done < <(find .agents/scripts -name "*.sh" -not -path "*/_archive/*" -print0 2>/dev/null) + + if [[ -s "$constants_file" ]]; then + print_warning "String literal constants needed:" + cat "$constants_file" + print_info "Add these constants to the top of respective files and replace string literals" + else + print_success "No repeated string literals found" + fi + + rm -f "$constants_file" + return 0 } validate_fixes() { - print_info "Validating fixes with ShellCheck..." - - local validation_errors=0 - - for file in .agents/scripts/*.sh; do - if [[ -f "$file" ]] && ! shellcheck "$file" > /dev/null 2>&1; then - ((validation_errors++)) - print_warning "ShellCheck issues remain in $file" - fi - done - - if [[ $validation_errors -eq 0 ]]; then - print_success "All files pass ShellCheck validation" - else - print_warning "$validation_errors files still have ShellCheck issues" - fi - return 0 + print_info "Validating fixes with ShellCheck..." + + local validation_errors=0 + + while IFS= read -r -d '' file; do + if [[ -f "$file" ]] && ! shellcheck -x "$file" >/dev/null 2>&1; then + ((validation_errors++)) + print_warning "ShellCheck issues remain in $file" + fi + done < <(find .agents/scripts -name "*.sh" -not -path "*/_archive/*" -print0 2>/dev/null) + + if [[ $validation_errors -eq 0 ]]; then + print_success "All files pass ShellCheck validation" + else + print_warning "$validation_errors files still have ShellCheck issues" + fi + return 0 } main() { - print_header - - # Ensure we're in the right directory - if [[ ! -d "providers" ]]; then - print_error "Must be run from the repository root directory" - exit 1 - fi - - # Create backup before making changes - backup_files - echo "" - - # Apply fixes - fix_return_statements - echo "" - - fix_positional_parameters - echo "" - - analyze_string_literals - echo "" - - validate_fixes - echo "" - - print_success "🎉 Universal quality fixes completed!" - print_info "Review changes and run linters-local.sh to validate improvements" - print_info "Commit changes with: git add . && git commit -m 'đŸŽ¯ Universal quality fixes'" - return 0 + print_header + + # Ensure we're in the right directory + if [[ ! -d "providers" ]]; then + print_error "Must be run from the repository root directory" + exit 1 + fi + + # Create backup before making changes + backup_files + echo "" + + # Apply fixes + fix_return_statements + echo "" + + fix_positional_parameters + echo "" + + analyze_string_literals + echo "" + + validate_fixes + echo "" + + print_success "🎉 Universal quality fixes completed!" + print_info "Review changes and run linters-local.sh to validate improvements" + print_info "Commit changes with: git add . && git commit -m 'đŸŽ¯ Universal quality fixes'" + return 0 } main "$@" diff --git a/setup.sh b/setup.sh index 24afdae0e..29d7ab566 100755 --- a/setup.sh +++ b/setup.sh @@ -286,18 +286,18 @@ create_backup_with_rotation() { # Returns 0 if valid, 1 if invalid # Valid: alphanumeric, dash, underscore, forward slash (no .., no shell metacharacters) validate_namespace() { - local ns="$1" - # Reject empty - [[ -z "$ns" ]] && return 1 - # Reject path traversal - [[ "$ns" == *".."* ]] && return 1 - # Reject shell metacharacters and dangerous characters - [[ "$ns" =~ [^a-zA-Z0-9/_-] ]] && return 1 - # Reject absolute paths - [[ "$ns" == /* ]] && return 1 - # Reject trailing slash (causes issues with rsync/tar exclusions) - [[ "$ns" == */ ]] && return 1 - return 0 + local ns="$1" + # Reject empty + [[ -z "$ns" ]] && return 1 + # Reject path traversal + [[ "$ns" == *".."* ]] && return 1 + # Reject shell metacharacters and dangerous characters + [[ "$ns" =~ [^a-zA-Z0-9/_-] ]] && return 1 + # Reject absolute paths + [[ "$ns" == /* ]] && return 1 + # Reject trailing slash (causes issues with rsync/tar exclusions) + [[ "$ns" == */ ]] && return 1 + return 0 } # Remove deprecated agent paths that have been moved @@ -2713,6 +2713,8 @@ set_permissions() { # Make scripts executable (suppress errors for missing paths) chmod +x ./*.sh 2>/dev/null || true chmod +x .agents/scripts/*.sh 2>/dev/null || true + # Also handle modularised subdirectories (e.g. memory/, supervisor-modules/) + find .agents/scripts -mindepth 2 -name "*.sh" -exec chmod +x {} + 2>/dev/null || true chmod +x ssh/*.sh 2>/dev/null || true # Secure configuration files @@ -3159,8 +3161,9 @@ deploy_aidevops_agents() { if [[ "$deploy_ok" == "true" ]]; then print_success "Deployed agents to $target_dir" - # Set permissions on scripts + # Set permissions on scripts (top-level and modularised subdirectories) chmod +x "$target_dir/scripts/"*.sh 2>/dev/null || true + find "$target_dir/scripts" -mindepth 2 -name "*.sh" -exec chmod +x {} + 2>/dev/null || true # Count what was deployed local agent_count diff --git a/tests/test-smoke-help.sh b/tests/test-smoke-help.sh index f56ec8521..3595df244 100644 --- a/tests/test-smoke-help.sh +++ b/tests/test-smoke-help.sh @@ -21,36 +21,36 @@ SKIP_COUNT=0 TOTAL_COUNT=0 pass() { - PASS_COUNT=$((PASS_COUNT + 1)) - TOTAL_COUNT=$((TOTAL_COUNT + 1)) - if [[ "$VERBOSE" == "--verbose" ]]; then - printf " \033[0;32mPASS\033[0m %s\n" "$1" - fi - return 0 + PASS_COUNT=$((PASS_COUNT + 1)) + TOTAL_COUNT=$((TOTAL_COUNT + 1)) + if [[ "$VERBOSE" == "--verbose" ]]; then + printf " \033[0;32mPASS\033[0m %s\n" "$1" + fi + return 0 } fail() { - FAIL_COUNT=$((FAIL_COUNT + 1)) - TOTAL_COUNT=$((TOTAL_COUNT + 1)) - printf " \033[0;31mFAIL\033[0m %s\n" "$1" - if [[ -n "${2:-}" ]]; then - printf " %s\n" "$2" - fi - return 0 + FAIL_COUNT=$((FAIL_COUNT + 1)) + TOTAL_COUNT=$((TOTAL_COUNT + 1)) + printf " \033[0;31mFAIL\033[0m %s\n" "$1" + if [[ -n "${2:-}" ]]; then + printf " %s\n" "$2" + fi + return 0 } skip() { - SKIP_COUNT=$((SKIP_COUNT + 1)) - TOTAL_COUNT=$((TOTAL_COUNT + 1)) - if [[ "$VERBOSE" == "--verbose" ]]; then - printf " \033[0;33mSKIP\033[0m %s\n" "$1" - fi - return 0 + SKIP_COUNT=$((SKIP_COUNT + 1)) + TOTAL_COUNT=$((TOTAL_COUNT + 1)) + if [[ "$VERBOSE" == "--verbose" ]]; then + printf " \033[0;33mSKIP\033[0m %s\n" "$1" + fi + return 0 } section() { - echo "" - printf "\033[1m=== %s ===\033[0m\n" "$1" + echo "" + printf "\033[1m=== %s ===\033[0m\n" "$1" } # ============================================================ @@ -62,20 +62,20 @@ syntax_pass=0 syntax_fail=0 while IFS= read -r script; do - abs_path="$REPO_DIR/$script" - name=$(basename "$script") - - if bash -n "$abs_path" 2>/dev/null; then - pass "syntax: $name" - syntax_pass=$((syntax_pass + 1)) - else - fail "syntax: $name" "bash -n failed" - syntax_fail=$((syntax_fail + 1)) - fi -done < <(git -C "$REPO_DIR" ls-files '.agents/scripts/*.sh' | grep -v '_archive/') + abs_path="$REPO_DIR/$script" + name=$(basename "$script") + + if bash -n "$abs_path" 2>/dev/null; then + pass "syntax: $name" + syntax_pass=$((syntax_pass + 1)) + else + fail "syntax: $name" "bash -n failed" + syntax_fail=$((syntax_fail + 1)) + fi +done < <(git -C "$REPO_DIR" ls-files '.agents/scripts/*.sh' '.agents/scripts/**/*.sh' | grep -v '_archive/') printf " Syntax: %d passed, %d failed (of %d non-archived scripts)\n" \ - "$syntax_pass" "$syntax_fail" "$((syntax_pass + syntax_fail))" + "$syntax_pass" "$syntax_fail" "$((syntax_pass + syntax_fail))" # ============================================================ # SECTION 2: Help command smoke tests @@ -85,66 +85,72 @@ section "Help Command Smoke Tests" # Scripts known to NOT support a help subcommand (libraries, hooks, utilities) # These are sourced or run without arguments, not invoked with "help" SKIP_HELP=( - "shared-constants.sh" - "loop-common.sh" - "pre-commit-hook.sh" - "cron-dispatch.sh" - "aidevops-update-check.sh" - "auto-version-bump.sh" - "validate-version-consistency.sh" - "extract-opencode-prompts.sh" - "generate-opencode-commands.sh" - "generate-skills.sh" - "opencode-prompt-drift-check.sh" - "quality-fix.sh" - "sonarcloud-autofix.sh" - "monitor-code-review.sh" - "code-audit-helper.sh" - "session-time-helper.sh" - "planning-commit-helper.sh" - "log-issue-helper.sh" - "humanise-update-helper.sh" - "dns-helper.sh" - "closte-helper.sh" - "cloudron-helper.sh" - "hetzner-helper.sh" - "hostinger-helper.sh" - "coolify-helper.sh" - "ses-helper.sh" - "servers-helper.sh" - "pagespeed-helper.sh" - "tool-version-check.sh" - "todo-ready.sh" - "mcp-diagnose.sh" - "localhost-helper.sh" - "linters-local.sh" - "markdown-lint-fix.sh" - "setup-mcp-integrations.sh" - "generate-opencode-agents.sh" - "setup-local-api-keys.sh" - "stagehand-setup.sh" - "stagehand-python-setup.sh" - "test-stagehand-integration.sh" - "test-stagehand-python-integration.sh" - "test-stagehand-both-integration.sh" - "crawl4ai-examples.sh" - "ampcode-cli.sh" - "agno-setup.sh" - "sonarscanner-cli.sh" - "codacy-cli.sh" - "codacy-cli-chunked.sh" - "coderabbit-pro-analysis.sh" - "snyk-helper.sh" - "verify-mirrors.sh" - "webhosting-verify.sh" + "shared-constants.sh" + "loop-common.sh" + "pre-commit-hook.sh" + "cron-dispatch.sh" + "aidevops-update-check.sh" + "auto-version-bump.sh" + "validate-version-consistency.sh" + "extract-opencode-prompts.sh" + "generate-opencode-commands.sh" + "generate-skills.sh" + "opencode-prompt-drift-check.sh" + "quality-fix.sh" + "sonarcloud-autofix.sh" + "monitor-code-review.sh" + "code-audit-helper.sh" + "session-time-helper.sh" + "planning-commit-helper.sh" + "log-issue-helper.sh" + "humanise-update-helper.sh" + "dns-helper.sh" + "closte-helper.sh" + "cloudron-helper.sh" + "hetzner-helper.sh" + "hostinger-helper.sh" + "coolify-helper.sh" + "ses-helper.sh" + "servers-helper.sh" + "pagespeed-helper.sh" + "tool-version-check.sh" + "todo-ready.sh" + "mcp-diagnose.sh" + "localhost-helper.sh" + "linters-local.sh" + "markdown-lint-fix.sh" + "setup-mcp-integrations.sh" + "generate-opencode-agents.sh" + "setup-local-api-keys.sh" + "stagehand-setup.sh" + "stagehand-python-setup.sh" + "test-stagehand-integration.sh" + "test-stagehand-python-integration.sh" + "test-stagehand-both-integration.sh" + "crawl4ai-examples.sh" + "ampcode-cli.sh" + "agno-setup.sh" + "sonarscanner-cli.sh" + "codacy-cli.sh" + "codacy-cli-chunked.sh" + "coderabbit-pro-analysis.sh" + "snyk-helper.sh" + "verify-mirrors.sh" + "webhosting-verify.sh" + # Modularised scripts (sourced by parent, not standalone) + "_common.sh" + "store.sh" + "recall.sh" + "maintenance.sh" + "verification.sh" ) is_skip_help() { - local name="$1" - for s in "${SKIP_HELP[@]}"; do - [[ "$name" == "$s" ]] && return 0 - done - return 1 + local name="$1" + for s in "${SKIP_HELP[@]}"; do + [[ "$name" == "$s" ]] && return 0 + done + return 1 } help_pass=0 @@ -152,46 +158,46 @@ help_fail=0 help_skip=0 while IFS= read -r script; do - abs_path="$REPO_DIR/$script" - name=$(basename "$script") - - # Skip archived scripts - [[ "$script" == *"_archive/"* ]] && continue - - # Skip scripts that don't support help - if is_skip_help "$name"; then - skip "help: $name (not a help-command script)" - help_skip=$((help_skip + 1)) - continue - fi - - # Check if script defines a help function - if ! grep -qE 'cmd_help\(\)|show_help\(\)|show_usage\(\)|usage\(\)' "$abs_path" 2>/dev/null; then - skip "help: $name (no help function defined)" - help_skip=$((help_skip + 1)) - continue - fi - - # Run help command with timeout (5s max) and capture output - help_output=$(timeout 5 bash "$abs_path" help 2>&1) || true - help_exit=$? - - # Some scripts exit 0 on help, some exit 1 (usage error) - both are acceptable - # as long as they produce output and don't hang/crash - if [[ -n "$help_output" ]]; then - pass "help: $name" - help_pass=$((help_pass + 1)) - elif [[ $help_exit -eq 124 ]]; then - fail "help: $name" "Timed out after 5 seconds" - help_fail=$((help_fail + 1)) - else - fail "help: $name" "No output produced (exit=$help_exit)" - help_fail=$((help_fail + 1)) - fi -done < <(git -C "$REPO_DIR" ls-files '.agents/scripts/*.sh') + abs_path="$REPO_DIR/$script" + name=$(basename "$script") + + # Skip archived scripts + [[ "$script" == *"_archive/"* ]] && continue + + # Skip scripts that don't support help + if is_skip_help "$name"; then + skip "help: $name (not a help-command script)" + help_skip=$((help_skip + 1)) + continue + fi + + # Check if script defines a help function + if ! grep -qE 'cmd_help\(\)|show_help\(\)|show_usage\(\)|usage\(\)' "$abs_path" 2>/dev/null; then + skip "help: $name (no help function defined)" + help_skip=$((help_skip + 1)) + continue + fi + + # Run help command with timeout (5s max) and capture output + help_output=$(timeout 5 bash "$abs_path" help 2>&1) || true + help_exit=$? + + # Some scripts exit 0 on help, some exit 1 (usage error) - both are acceptable + # as long as they produce output and don't hang/crash + if [[ -n "$help_output" ]]; then + pass "help: $name" + help_pass=$((help_pass + 1)) + elif [[ $help_exit -eq 124 ]]; then + fail "help: $name" "Timed out after 5 seconds" + help_fail=$((help_fail + 1)) + else + fail "help: $name" "No output produced (exit=$help_exit)" + help_fail=$((help_fail + 1)) + fi +done < <(git -C "$REPO_DIR" ls-files '.agents/scripts/*.sh' '.agents/scripts/**/*.sh') printf " Help: %d passed, %d failed, %d skipped\n" \ - "$help_pass" "$help_fail" "$help_skip" + "$help_pass" "$help_fail" "$help_skip" # ============================================================ # SECTION 3: ShellCheck on critical scripts (errors only) @@ -199,38 +205,38 @@ printf " Help: %d passed, %d failed, %d skipped\n" \ section "ShellCheck (errors only) - Critical Scripts" CRITICAL_SCRIPTS=( - "supervisor-helper.sh" - "memory-helper.sh" - "mail-helper.sh" - "runner-helper.sh" - "full-loop-helper.sh" - "ralph-loop-helper.sh" - "quality-loop-helper.sh" - "pre-edit-check.sh" - "worktree-helper.sh" - "credential-helper.sh" - "secret-helper.sh" + "supervisor-helper.sh" + "memory-helper.sh" + "mail-helper.sh" + "runner-helper.sh" + "full-loop-helper.sh" + "ralph-loop-helper.sh" + "quality-loop-helper.sh" + "pre-edit-check.sh" + "worktree-helper.sh" + "credential-helper.sh" + "secret-helper.sh" ) if command -v shellcheck &>/dev/null; then - for name in "${CRITICAL_SCRIPTS[@]}"; do - script_path="$SCRIPTS_DIR/$name" - if [[ ! -f "$script_path" ]]; then - skip "shellcheck: $name (not found)" - continue - fi - - sc_output=$(shellcheck -S error "$script_path" 2>&1 || true) - sc_errors=$(echo "$sc_output" | grep -c "error" || true) - if [[ "$sc_errors" -eq 0 ]]; then - pass "shellcheck: $name (0 errors)" - else - fail "shellcheck: $name ($sc_errors errors)" \ - "$(echo "$sc_output" | head -5)" - fi - done + for name in "${CRITICAL_SCRIPTS[@]}"; do + script_path="$SCRIPTS_DIR/$name" + if [[ ! -f "$script_path" ]]; then + skip "shellcheck: $name (not found)" + continue + fi + + sc_output=$(shellcheck -S error "$script_path" 2>&1 || true) + sc_errors=$(echo "$sc_output" | grep -c "error" || true) + if [[ "$sc_errors" -eq 0 ]]; then + pass "shellcheck: $name (0 errors)" + else + fail "shellcheck: $name ($sc_errors errors)" \ + "$(echo "$sc_output" | head -5)" + fi + done else - skip "shellcheck not installed" + skip "shellcheck not installed" fi # ============================================================ @@ -239,15 +245,15 @@ fi echo "" echo "========================================" printf " \033[1mResults: %d total, \033[0;32m%d passed\033[0m, \033[0;31m%d failed\033[0m, \033[0;33m%d skipped\033[0m\n" \ - "$TOTAL_COUNT" "$PASS_COUNT" "$FAIL_COUNT" "$SKIP_COUNT" + "$TOTAL_COUNT" "$PASS_COUNT" "$FAIL_COUNT" "$SKIP_COUNT" echo "========================================" if [[ "$FAIL_COUNT" -gt 0 ]]; then - echo "" - printf "\033[0;31mFAILURES DETECTED - review output above\033[0m\n" - exit 1 + echo "" + printf "\033[0;31mFAILURES DETECTED - review output above\033[0m\n" + exit 1 else - echo "" - printf "\033[0;32mAll tests passed.\033[0m\n" - exit 0 + echo "" + printf "\033[0;32mAll tests passed.\033[0m\n" + exit 0 fi