output.sh 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. # net-ops :: _lib/output.sh
  2. # Output mode handling for probe scripts. Three renderings of the same stream:
  3. # - panel (default at a TTY): the term.sh enclosing panel — section sub-headers
  4. # on the │ rail, colored term_mark check rows, a footer health indicator.
  5. # - text (piped / non-TTY): legacy [PASS]/[FAIL] lines + a SUMMARY block. Kept
  6. # byte-stable because humans AND LLMs scan it for the first [FAIL]; tests and
  7. # pipes depend on it.
  8. # - json (--json): newline-delimited JSON, one record per check + a summary.
  9. #
  10. # The panel is the human default (per docs/TERMINAL-DESIGN.md); it never touches
  11. # the piped/--json data product, so stream behaviour is unchanged for consumers.
  12. #
  13. # Usage in a probe script:
  14. # source "$(dirname "$0")/../_lib/output.sh"
  15. # PANEL_TITLE="net · linux probe" # optional; titles the panel header
  16. # parse_output_flags "$@"
  17. # # then use section / pass / fail / info / emit_summary as before
  18. JSON_MODE="${JSON_MODE:-0}"
  19. # Shared terminal toolkit (skills/_lib/term.sh) for the panel rendering. Absent ->
  20. # the legacy text path is used, so this degrades cleanly with no behaviour change.
  21. __net_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../_lib" 2>/dev/null && pwd || true)"
  22. if [ -n "${__net_lib:-}" ] && [ -f "$__net_lib/term.sh" ]; then
  23. . "$__net_lib/term.sh"
  24. __NET_HAVE_TERM=1
  25. else
  26. __NET_HAVE_TERM=0
  27. fi
  28. # Panel header title — a probe script may override before the first section().
  29. PANEL_TITLE="${PANEL_TITLE:-net-ops}"
  30. __PANEL_OPEN=0
  31. parse_output_flags() {
  32. for a in "$@"; do
  33. [[ "$a" == "--json" ]] && JSON_MODE=1
  34. done
  35. }
  36. # Panel applies in text mode only, when stdout is a TTY (or FORCE_COLOR forces a
  37. # render for verification). Piped/non-TTY text consumers get the legacy format.
  38. _panel_active() {
  39. [[ "$JSON_MODE" -eq 1 || "$__NET_HAVE_TERM" -eq 0 ]] && return 1
  40. [ -t 1 ] || [ -n "${FORCE_COLOR:-}" ]
  41. }
  42. # Lazily open the panel frame (term_init + header + a breath row) on first use.
  43. _panel_open() {
  44. [[ "$__PANEL_OPEN" -eq 1 ]] && return 0
  45. term_init
  46. term_panel_open net-ops "$PANEL_TITLE"
  47. term_panel_vert
  48. __PANEL_OPEN=1
  49. }
  50. # JSON-safe string escaper. Handles backslash, double-quote, and control chars.
  51. _json_escape() {
  52. local s="$1"
  53. s="${s//\\/\\\\}"
  54. s="${s//\"/\\\"}"
  55. s="${s//$'\n'/\\n}"
  56. s="${s//$'\r'/\\r}"
  57. s="${s//$'\t'/\\t}"
  58. printf '%s' "$s"
  59. }
  60. # These are the public API. They route to panel / text / JSON per mode.
  61. PASS_COUNT=0
  62. FAIL_COUNT=0
  63. FIRST_FAIL=""
  64. CURRENT_SECTION=""
  65. section() {
  66. CURRENT_SECTION="$1"
  67. if [[ "$JSON_MODE" -eq 1 ]]; then
  68. printf '{"type":"section","name":"%s"}\n' "$(_json_escape "$1")"
  69. elif _panel_active; then
  70. _panel_open
  71. term_panel_vert
  72. term_panel_line "$(term_color cyan "$1")"
  73. else
  74. echo
  75. echo "=== $1 ==="
  76. fi
  77. }
  78. pass() {
  79. PASS_COUNT=$((PASS_COUNT + 1))
  80. if [[ "$JSON_MODE" -eq 1 ]]; then
  81. printf '{"type":"check","section":"%s","label":"%s","status":"pass","detail":"%s"}\n' \
  82. "$(_json_escape "$CURRENT_SECTION")" "$(_json_escape "$1")" "$(_json_escape "${2:-}")"
  83. elif _panel_active; then
  84. _panel_open
  85. term_status_row ok "$1" "${2:-}"
  86. else
  87. echo "[PASS] $1${2:+ :: $2}"
  88. fi
  89. }
  90. fail() {
  91. FAIL_COUNT=$((FAIL_COUNT + 1))
  92. [[ -z "$FIRST_FAIL" ]] && FIRST_FAIL="[$CURRENT_SECTION] $1"
  93. if [[ "$JSON_MODE" -eq 1 ]]; then
  94. printf '{"type":"check","section":"%s","label":"%s","status":"fail","detail":"%s"}\n' \
  95. "$(_json_escape "$CURRENT_SECTION")" "$(_json_escape "$1")" "$(_json_escape "${2:-}")"
  96. elif _panel_active; then
  97. _panel_open
  98. term_status_row bad "$1" "${2:-}"
  99. else
  100. echo "[FAIL] $1${2:+ :: $2}"
  101. fi
  102. }
  103. # Call from end of probe to emit summary record / block / panel footer.
  104. emit_summary() {
  105. if [[ "$JSON_MODE" -eq 1 ]]; then
  106. printf '{"type":"summary","pass":%d,"fail":%d,"first_fail":"%s"}\n' \
  107. "$PASS_COUNT" "$FAIL_COUNT" "$(_json_escape "$FIRST_FAIL")"
  108. elif _panel_active; then
  109. _panel_open
  110. [[ -n "$FIRST_FAIL" ]] && { term_panel_vert; term_panel_line "$(term_color dim "first fail: $FIRST_FAIL")"; }
  111. term_panel_vert
  112. local state="healthy" health="$PASS_COUNT ok"
  113. if [[ "$FAIL_COUNT" -gt 0 ]]; then
  114. state="critical"; health="$FAIL_COUNT fail ${TERM_DOT} $PASS_COUNT ok"
  115. fi
  116. term_panel_close "--json for data ${TERM_DOT} --redact to mask" "$(term_health "$state" "$health")"
  117. else
  118. echo
  119. echo "=== SUMMARY ==="
  120. echo " PASS: $PASS_COUNT FAIL: $FAIL_COUNT"
  121. if [[ -n "$FIRST_FAIL" ]]; then
  122. echo " First failure: $FIRST_FAIL"
  123. else
  124. echo " No failures."
  125. fi
  126. fi
  127. }
  128. # Helper for scripts that want to suppress informational/diagnostic output
  129. # (the non-PASS/FAIL annotations like scutil dumps) in JSON mode.
  130. info() {
  131. if [[ "$JSON_MODE" -eq 1 ]]; then
  132. # Optional: emit info records. Keep silent for cleaner JSON parsing.
  133. return 0
  134. elif _panel_active; then
  135. term_panel_line "$(term_color dim "$*")"
  136. else
  137. echo "$@"
  138. fi
  139. }