#!/usr/bin/env bash # run-quiet — wrap a long-running command so its output stays out of agent # context. Redirects stdout+stderr to a unique log under # ${KANDEV_RUN_QUIET_DIR:-${TMPDIR:-/tmp}}, prints a one-line # success summary, and on failure auto-greps for the relevant error pattern # based on the tag. Exits with the underlying command's exit code. # # Usage: # scripts/run-quiet -- # scripts/run-quiet [--tail N] -- # # Tags drive the error-extraction pattern (see grep_for_tag below). Unknown # tags fall back to a generic error/FAIL grep, then to the last 40 lines if # nothing matches. # KANDEV_RUN_QUIET_DIR overrides TMPDIR for log placement; /tmp is the final # fallback. # # Example: # scripts/run-quiet backend-test -- make -C apps/backend test lint # # Concurrency: each invocation gets a unique mktemp path, so parallel agents # never clobber each other. set -u # How many lines of failure context to surface inline. TAIL_LINES=40 # Tag is required, must come first. if [[ $# -lt 1 ]]; then echo "usage: run-quiet [--tail N] -- " >&2 exit 2 fi TAG="$1" shift # Parse optional flags before the `--` separator. while [[ $# -gt 0 ]]; do case "$1" in --tail) TAIL_LINES="$2" shift 2 ;; --) shift break ;; *) echo "run-quiet: expected -- before command, got '$1'" >&2 exit 2 ;; esac done if [[ $# -eq 0 ]]; then echo "run-quiet: no command supplied after --" >&2 exit 2 fi # Sanitize tag for filename use: keep alnum, dash, underscore. SAFE_TAG="$(printf '%s' "$TAG" | tr -c 'a-zA-Z0-9_-' '-')" LOG_DIR="${KANDEV_RUN_QUIET_DIR:-${TMPDIR:-/tmp}}" if ! mkdir -p "$LOG_DIR" || [[ ! -w "$LOG_DIR" ]]; then echo "run-quiet: cannot create log directory: $LOG_DIR" >&2 exit 1 fi # Best-effort cleanup of old logs (>24h). Failure is non-fatal. find "$LOG_DIR" -maxdepth 1 -name 'kandev-run.*.log' -mtime +0 -delete 2>/dev/null || true LOG="$(mktemp "$LOG_DIR/kandev-run.${SAFE_TAG}.XXXXXXXX")" mv "$LOG" "${LOG}.log" LOG="${LOG}.log" # Run the command. Keep stdin attached so things like `pnpm exec` that # probe TTY don't misbehave; redirect both stdout and stderr to LOG. "$@" > "$LOG" 2>&1 RC=$? EXTRACT_LOG_ON_SUCCESS=false case "$TAG" in gh-run|ci|gh-run-view) EXTRACT_LOG_ON_SUCCESS=true ;; esac if [[ $RC -eq 0 && "$EXTRACT_LOG_ON_SUCCESS" != true ]]; then echo "exit=0 log=$LOG" exit 0 fi echo "exit=$RC log=$LOG" # Strip ANSI colour codes once so grep patterns work on `\033[…m`-rich logs # (eslint, playwright, golangci, gh). PLAIN="$(mktemp "$LOG_DIR/kandev-run.${SAFE_TAG}.plain.XXXXXXXX")" mv "$PLAIN" "${PLAIN}.log" PLAIN="${PLAIN}.log" # shellcheck disable=SC2016 # we want $0 inside awk, not shell expansion sed $'s/\033\\[[0-9;]*[a-zA-Z]//g' "$LOG" > "$PLAIN" 2>/dev/null || cp "$LOG" "$PLAIN" grep_for_tag() { case "$TAG" in backend-test|go-test) # `go test` failures: FAIL , --- FAIL, panic, compile errors # (file:line:), build failures. Do NOT match `ok` (passing packages) # or `PASS` (passing tests). grep -nE '^(FAIL\b|--- FAIL|panic:|.+\.go:[0-9]+:.*|.*\[build failed\])' "$PLAIN" ;; backend-lint|golangci) # golangci-lint output: file:line:col: message (linter). grep -nE '^[^[:space:]].*:[0-9]+:[0-9]+:|^level=error|^Error:' "$PLAIN" ;; web-lint|eslint) # ESLint compact-ish format: " N:N error/warning message rule" grep -nE '^\s+[0-9]+:[0-9]+\s+(error|warning)|^/.*\.(ts|tsx|js|jsx)$|^✖ ' "$PLAIN" ;; web-test|vitest) grep -nE '(FAIL|✗|AssertionError|Expected|Received|Test Files\s+[0-9]+ failed)' "$PLAIN" ;; e2e|playwright) grep -nE '(✘|×|Error:|TimeoutError|expect\(.*\)|.*\.spec\.ts:[0-9]+|.*failed \([0-9]+\.[0-9]+s\))' "$PLAIN" ;; gh-run|ci|gh-run-view) # GitHub Actions log lines: "JOB\tSTEP\t...". Pull anything that # mentions error/failed/FAIL, plus the surrounding job context line. # `gh run view --log-failed` exits zero when it retrieved a log, even # when that log contains failed tests. Include Go's JSON, test, and # goleak markers so those failures are surfaced in that success case. grep -nE '^\S+\s+\S+\s+.*(error|failed|FAIL|Error:|exit code|golangci-lint run|[A-Za-z0-9_./-]+\.go:[0-9]+:[0-9]+:|[0-9]+ issues?:|apps/backend/test-results\.json|Process completed with exit code 1)|"Action":"fail"|--- FAIL:|goleak:' "$PLAIN" ;; *) grep -nE '(error|Error|FAIL|panic:|Failed|✘|✗)' "$PLAIN" ;; esac } MATCHES="$(grep_for_tag | head -n "$TAIL_LINES")" if [[ -n "$MATCHES" ]]; then echo "--- failures ($(printf '%s\n' "$MATCHES" | wc -l | tr -d ' ') lines, of tag=$TAG) ---" printf '%s\n' "$MATCHES" elif [[ $RC -eq 0 ]]; then # Successful GitHub commands with no failure markers remain compact. rm -f "$PLAIN" exit 0 else # Fallback: grep found nothing useful; surface the last N lines verbatim. echo "--- no tag matches; tail $TAIL_LINES lines of $LOG ---" tail -n "$TAIL_LINES" "$PLAIN" fi rm -f "$PLAIN" exit "$RC"