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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [7.17.1] — 2026-08-23 — alias-proof shipped pipelines

### Fixed

- **A user alias on a coreutil could hijack shipped pipelines.** Dispatchers run in the user's
*interactive* shell, where aliases are live and expand at parse time. On a machine with
`tr='track-activity report'`, every `... | tr -d ' '` in `lib/` ran that command instead, and its
output landed in the variable where a count belonged — `teach deploy --dry-run` printed
`Would deploy Monthly Terminal Report (2026-08):` instead of `Would deploy 675 files:`. All 101
pipeline sites across 34 files now use `command tr`, which bypasses aliases and functions.
Guarded by `tests/test-alias-shadowing.zsh`, including a negative control so the test cannot pass
vacuously if the environment stops reproducing the hijack.

## [7.17.0] — 2026-08-23 — teach deploy safety + CI coverage

### Added
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This file provides guidance to Claude Code when working with code in this reposi
**flow-cli** - Pure ZSH plugin for ADHD-optimized workflow management. Zero dependencies. Standalone (works without Oh-My-Zsh or any plugin manager).

- **Architecture:** Pure ZSH plugin (no Node.js runtime required)
- **Current Version:** v7.17.0
- **Current Version:** v7.17.1
- **Install:** Homebrew (recommended), or any plugin manager
- **Source:** `source /opt/homebrew/opt/flow-cli/flow.plugin.zsh` (via Homebrew)
- **Optional:** Atlas integration for enhanced state management
Expand Down Expand Up @@ -217,7 +217,7 @@ export FLOW_FORCE_DISPATCHER_OBS=1 # Force-keep one dispatcher (FLOW_F

## Current Status

**Version:** v7.17.0 | **Tests:** 12000+ (75/75 suite, 1 skipped — tool absence) | **Docs:** https://Data-Wise.github.io/flow-cli/
**Version:** v7.17.1 | **Tests:** 12000+ (75/75 suite, 1 skipped — tool absence) | **Docs:** https://Data-Wise.github.io/flow-cli/

---

Expand Down
13 changes: 13 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/), and this pro

## [Unreleased]

## [7.17.1] — 2026-08-23 — alias-proof shipped pipelines

### Fixed

- **A user alias on a coreutil could hijack shipped pipelines.** Dispatchers run in the user's
*interactive* shell, where aliases are live and expand at parse time. On a machine with
`tr='track-activity report'`, every `... | tr -d ' '` in `lib/` ran that command instead, and its
output landed in the variable where a count belonged — `teach deploy --dry-run` printed
`Would deploy Monthly Terminal Report (2026-08):` instead of `Would deploy 675 files:`. All 101
pipeline sites across 34 files now use `command tr`, which bypasses aliases and functions.
Guarded by `tests/test-alias-shadowing.zsh`, including a negative control so the test cannot pass
vacuously if the environment stops reproducing the hijack.

## [7.17.0] — 2026-08-23 — teach deploy safety + CI coverage

### Added
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ tags:
**New here?** `setup` walks you through configuration interactively, or run `tutorial` for
12 hands-on lessons at your own pace — both are guided, no docs required to start.

!!! success "🎉 What's New in v7.17.0"
!!! success "🎉 What's New in v7.17.1"
**`teach deploy --dry-run` is genuinely read-only** — it no longer aborts in CI mode, no longer prompts to commit (a pty wrapper used to auto-accept that and create a real commit), and it now names the uncommitted files its plan excludes.
**`teach deploy --direct` merges with `--no-ff`** — every deploy is one revertable commit again, so `teach deploy --rollback` can undo a multi-commit deploy as a unit.
**`em undo`** — single-step undo of the last `em star`/`flag`/`unflag`/`move`, plus `em move --recent` for quick folder re-picks.
**Shipped pipelines are alias-proof** — a user alias on a coreutil (e.g. `tr`) could hijack internal pipelines and replace counts with unrelated output; all 101 sites now use `command tr`.
Full details → [Changelog](CHANGELOG.md).

---
Expand Down Expand Up @@ -326,4 +327,4 @@ ref # Quick-reference card (forgot the syntax? this is faster than

---

**v7.17.0** · Pure ZSH · Zero Dependencies · MIT License
**v7.17.1** · Pure ZSH · Zero Dependencies · MIT License
2 changes: 1 addition & 1 deletion flow.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ _flow_plugin_init

# Export loaded marker
export FLOW_PLUGIN_LOADED=1
export FLOW_VERSION="7.17.0"
export FLOW_VERSION="7.17.1"

# Register exit hook for plugin cleanup
add-zsh-hook zshexit _flow_plugin_cleanup
Expand Down
2 changes: 1 addition & 1 deletion lib/atlas-bridge.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,7 @@ _flow_where_fallback() {
done

if [[ -n "$status_dir" ]]; then
local proj_status=$(_flow_status_field "$status_dir" "Status" | tr -d ' ')
local proj_status=$(_flow_status_field "$status_dir" "Status" | command tr -d ' ')
local focus=$(_flow_status_field "$status_dir" "Focus")

[[ -n "$proj_status" ]] && echo " Status: $proj_status"
Expand Down
6 changes: 3 additions & 3 deletions lib/backup-helpers.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ _teach_count_backups() {
return 0
fi

find "$backup_dir" -maxdepth 1 -type d -name "*.20*" 2>/dev/null | wc -l | tr -d ' '
find "$backup_dir" -maxdepth 1 -type d -name "*.20*" 2>/dev/null | wc -l | command tr -d ' '
}

# =============================================================================
Expand Down Expand Up @@ -603,7 +603,7 @@ _teach_confirm_delete() {
fi

# Count files
local file_count=$(find "$backup_path" -type f 2>/dev/null | wc -l | tr -d ' ')
local file_count=$(find "$backup_path" -type f 2>/dev/null | wc -l | command tr -d ' ')
echo " Files: $file_count"

echo ""
Expand Down Expand Up @@ -669,7 +669,7 @@ _teach_preview_cleanup() {
echo ""

local backups=$(_teach_list_backups "$content_path")
local backup_count=$(echo "$backups" | wc -l | tr -d ' ')
local backup_count=$(echo "$backups" | wc -l | command tr -d ' ')

if [[ "$backup_count" -eq 0 ]]; then
echo " ${FLOW_COLORS[dim]}No backups to clean${FLOW_COLORS[reset]}"
Expand Down
4 changes: 2 additions & 2 deletions lib/cache-analysis.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ _analyze_cache_size() {
fi

# Count files
local file_count=$(find "$cache_dir" -type f 2>/dev/null | wc -l | tr -d ' ')
local file_count=$(find "$cache_dir" -type f 2>/dev/null | wc -l | command tr -d ' ')

# Get size in bytes (portable: use du -sk for KB, then convert)
local size_kb=0
Expand Down Expand Up @@ -109,7 +109,7 @@ _analyze_cache_by_directory() {
local dir_size_human=$(_cache_format_bytes "$dir_size_bytes")

# Count files
local dir_files=$(find "$subdir" -type f 2>/dev/null | wc -l | tr -d ' ')
local dir_files=$(find "$subdir" -type f 2>/dev/null | wc -l | command tr -d ' ')

# Calculate percentage
local percentage=0
Expand Down
6 changes: 3 additions & 3 deletions lib/cache-helpers.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ _cache_status() {
fi

# Count files
local file_count=$(find "$freeze_dir" -type f 2>/dev/null | wc -l | tr -d ' ')
local file_count=$(find "$freeze_dir" -type f 2>/dev/null | wc -l | command tr -d ' ')

# Get size
local size_bytes=0
Expand Down Expand Up @@ -587,7 +587,7 @@ _cache_analyze() {
while IFS= read -r subdir; do
local subdir_name=$(basename "$subdir")
local subdir_size=$(du -sh "$subdir" 2>/dev/null | awk '{print $1}')
local subdir_files=$(find "$subdir" -type f 2>/dev/null | wc -l | tr -d ' ')
local subdir_files=$(find "$subdir" -type f 2>/dev/null | wc -l | command tr -d ' ')

printf "${FLOW_COLORS[header]}│${FLOW_COLORS[reset]} %-30s %8s (%s files)\n" \
"$subdir_name" "$subdir_size" "$subdir_files"
Expand Down Expand Up @@ -705,7 +705,7 @@ _cache_clean() {
dirs_to_delete+=("_site")
if command -v du &>/dev/null; then
local site_size=$(du -sh "$site_dir" 2>/dev/null | awk '{print $1}')
local site_files=$(find "$site_dir" -type f 2>/dev/null | wc -l | tr -d ' ')
local site_files=$(find "$site_dir" -type f 2>/dev/null | wc -l | command tr -d ' ')
total_files=$((total_files + site_files))
fi
fi
Expand Down
20 changes: 10 additions & 10 deletions lib/concept-extraction.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ _extract_concepts_from_frontmatter() {
concepts_json=$(echo "$frontmatter" | yq eval -o json '.concepts // ""' - 2>/dev/null)
# Return empty if concepts is null, empty string, empty array, or array with only empty strings
local trimmed
trimmed=$(echo "$concepts_json" | tr -d '\n' | xargs)
trimmed=$(echo "$concepts_json" | command tr -d '\n' | xargs)
# Check if trimmed result is empty, "[]", or "[""]"
if [[ -z "$trimmed" || "$trimmed" == "[]" || "$trimmed" == "[\"\"]" ]]; then
echo -n ""
Expand Down Expand Up @@ -74,12 +74,12 @@ _parse_introduced_concepts() {
if [[ "$concepts_json" =~ '^\[.*\]$' ]]; then
# Array of objects format: extract .id from each object
local introduced
introduced=$(echo "$concepts_json" | yq eval -o json '.[] | .id' - 2>/dev/null | sed 's/"//g' | tr '\n' ' ' | xargs)
introduced=$(echo "$concepts_json" | yq eval -o json '.[] | .id' - 2>/dev/null | sed 's/"//g' | command tr '\n' ' ' | xargs)
echo "$introduced"
else
# Simple format: extract .introduces array
local introduced
introduced=$(echo "$concepts_json" | yq eval -o json '.introduces // [] | .[]' - 2>/dev/null | sed 's/"//g' | tr '\n' ' ' | xargs)
introduced=$(echo "$concepts_json" | yq eval -o json '.introduces // [] | .[]' - 2>/dev/null | sed 's/"//g' | command tr '\n' ' ' | xargs)
echo "$introduced"
fi
}
Expand All @@ -102,12 +102,12 @@ _parse_required_concepts() {
if [[ "$concepts_json" =~ '^\[.*\]$' ]]; then
# Array of objects format: extract all .prerequisites arrays and flatten
local required
required=$(echo "$concepts_json" | yq eval -o json '.[] | .prerequisites // [] | .[]' - 2>/dev/null | sed 's/"//g' | sort -u | tr '\n' ' ' | xargs)
required=$(echo "$concepts_json" | yq eval -o json '.[] | .prerequisites // [] | .[]' - 2>/dev/null | sed 's/"//g' | sort -u | command tr '\n' ' ' | xargs)
echo "$required"
else
# Simple format: extract .requires array
local required
required=$(echo "$concepts_json" | yq eval -o json '.requires // [] | .[]' - 2>/dev/null | sed 's/"//g' | tr '\n' ' ' | xargs)
required=$(echo "$concepts_json" | yq eval -o json '.requires // [] | .[]' - 2>/dev/null | sed 's/"//g' | command tr '\n' ' ' | xargs)
echo "$required"
fi
}
Expand Down Expand Up @@ -231,7 +231,7 @@ _extract_concepts_from_frontmatter() {
return 0
fi
concepts_json=$(echo "$frontmatter" | yq eval -o json '.concepts // ""' - 2>/dev/null)
trimmed=$(echo "$concepts_json" | tr -d '\n' | xargs)
trimmed=$(echo "$concepts_json" | command tr -d '\n' | xargs)
if [[ -z "$trimmed" || "$trimmed" == "[]" || "$trimmed" == "[\"\"]" ]]; then
echo -n ""
else
Expand All @@ -248,9 +248,9 @@ _parse_introduced_concepts() {
fi
# Check if array format: [{id: "...", ...}]
if [[ "$concepts_json" =~ '^\[.*\]$' ]]; then
introduced=$(echo "$concepts_json" | yq eval -o json '.[] | .id' - 2>/dev/null | sed 's/"//g' | tr '\n' ' ' | xargs)
introduced=$(echo "$concepts_json" | yq eval -o json '.[] | .id' - 2>/dev/null | sed 's/"//g' | command tr '\n' ' ' | xargs)
else
introduced=$(echo "$concepts_json" | yq eval -o json '.introduces // [] | .[]' - 2>/dev/null | sed 's/"//g' | tr '\n' ' ' | xargs)
introduced=$(echo "$concepts_json" | yq eval -o json '.introduces // [] | .[]' - 2>/dev/null | sed 's/"//g' | command tr '\n' ' ' | xargs)
fi
echo "$introduced"
}
Expand All @@ -264,9 +264,9 @@ _parse_required_concepts() {
fi
# Check if array format: [{prerequisites: [...], ...}]
if [[ "$concepts_json" =~ '^\[.*\]$' ]]; then
required=$(echo "$concepts_json" | yq eval -o json '.[] | .prerequisites // [] | .[]' - 2>/dev/null | sed 's/"//g' | sort -u | tr '\n' ' ' | xargs)
required=$(echo "$concepts_json" | yq eval -o json '.[] | .prerequisites // [] | .[]' - 2>/dev/null | sed 's/"//g' | sort -u | command tr '\n' ' ' | xargs)
else
required=$(echo "$concepts_json" | yq eval -o json '.requires // [] | .[]' - 2>/dev/null | sed 's/"//g' | tr '\n' ' ' | xargs)
required=$(echo "$concepts_json" | yq eval -o json '.requires // [] | .[]' - 2>/dev/null | sed 's/"//g' | command tr '\n' ' ' | xargs)
fi
echo "$required"
}
Expand Down
2 changes: 1 addition & 1 deletion lib/core.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,7 @@ _flow_tty_handoff_cleanup() {
#
# Example:
# _flow_status_field "$project_path" "Focus"
# _flow_status_field "$project_path" "Progress" | tr -d '%' # site strips %
# _flow_status_field "$project_path" "Progress" | command tr -d '%' # site strips %
#
# Notes:
# - Handles both the "## Field:" markdown dialect and the plain "field:"
Expand Down
6 changes: 3 additions & 3 deletions lib/date-parser.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ _date_parse_quarto_yaml() {

# Extract date value from YAML frontmatter
local date_value
date_value=$(yq eval ".${field} // \"\"" "$file" 2>/dev/null | tr -d '"')
date_value=$(yq eval ".${field} // \"\"" "$file" 2>/dev/null | command tr -d '"')

# Return empty if not found or null
if [[ -z "$date_value" || "$date_value" == "null" ]]; then
Expand Down Expand Up @@ -601,7 +601,7 @@ _date_load_config() {

if [[ -n "$exam_date" && "$exam_date" != "null" ]]; then
# Normalize exam name to key (lowercase, spaces to underscores)
local exam_key=$(echo "$exam_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')
local exam_key=$(echo "$exam_name" | command tr '[:upper:]' '[:lower:]' | command tr ' ' '_')
printf 'CONFIG_DATES[exam_%s]="%s"\n' "$exam_key" "$exam_date"
fi
done
Expand Down Expand Up @@ -650,7 +650,7 @@ _date_load_config() {
holiday_date=$(yq eval ".semester_info.holidays[$i].date" "$config_file" 2>/dev/null)

if [[ -n "$holiday_date" && "$holiday_date" != "null" ]]; then
local holiday_key=$(echo "$holiday_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')
local holiday_key=$(echo "$holiday_name" | command tr '[:upper:]' '[:lower:]' | command tr ' ' '_')
printf 'CONFIG_DATES[holiday_%s]="%s"\n' "$holiday_key" "$holiday_date"
fi
done
Expand Down
2 changes: 1 addition & 1 deletion lib/deploy-rollback-helpers.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ _deploy_perform_rollback() {
# Record rollback in deploy history
if typeset -f _deploy_history_append >/dev/null 2>&1; then
local file_count=0
file_count=$(git diff --name-only "${full_hash}^" "$full_hash" 2>/dev/null | wc -l | tr -d ' ')
file_count=$(git diff --name-only "${full_hash}^" "$full_hash" 2>/dev/null | wc -l | command tr -d ' ')
_deploy_history_append \
"rollback" \
"$commit_after" \
Expand Down
2 changes: 1 addition & 1 deletion lib/dispatchers/dot-doctor-integration.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ _dots_doctor_integration() {
# Check for uncommitted changes
local status_output=$(chezmoi status 2>/dev/null)
if [[ -n "$status_output" ]]; then
local count=$(echo "$status_output" | wc -l | tr -d ' ')
local count=$(echo "$status_output" | wc -l | command tr -d ' ')
_flow_log_warning "$count uncommitted changes"
else
_flow_log_success "No uncommitted changes"
Expand Down
20 changes: 10 additions & 10 deletions lib/dispatchers/dots-dispatcher.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ _dots_size() {
echo "${FLOW_COLORS[header]}│${FLOW_COLORS[reset]} ${FLOW_COLORS[header]}│${FLOW_COLORS[reset]}"

# Check for nested .git directories
local git_count=$(find "$chezmoi_dir" -name ".git" -type d -not -path "$chezmoi_dir/.git" 2>/dev/null | wc -l | tr -d ' ')
local git_dotf_count=$(find "$chezmoi_dir" -name "dot_git" -type d 2>/dev/null | wc -l | tr -d ' ')
local git_count=$(find "$chezmoi_dir" -name ".git" -type d -not -path "$chezmoi_dir/.git" 2>/dev/null | wc -l | command tr -d ' ')
local git_dotf_count=$(find "$chezmoi_dir" -name "dot_git" -type d 2>/dev/null | wc -l | command tr -d ' ')
local total_git_dirs=$((git_count + git_dotf_count))

if (( total_git_dirs > 0 )); then
Expand All @@ -311,7 +311,7 @@ _dots_size() {
fi

# Check for large files (>100KB)
local large_count=$(find "$chezmoi_dir" -type f -not -path "$chezmoi_dir/.git/*" -size +100k 2>/dev/null | wc -l | tr -d ' ')
local large_count=$(find "$chezmoi_dir" -type f -not -path "$chezmoi_dir/.git/*" -size +100k 2>/dev/null | wc -l | command tr -d ' ')
if (( large_count > 0 )); then
echo "${FLOW_COLORS[header]}│${FLOW_COLORS[reset]} ${FLOW_COLORS[warning]}⚠️ Found $large_count files larger than 100KB${FLOW_COLORS[reset]} ${FLOW_COLORS[header]}│${FLOW_COLORS[reset]}"
echo "${FLOW_COLORS[header]}│${FLOW_COLORS[reset]} ${FLOW_COLORS[muted]}Review with: dots size | grep '⚠️'${FLOW_COLORS[reset]} ${FLOW_COLORS[header]}│${FLOW_COLORS[reset]}"
Expand Down Expand Up @@ -648,7 +648,7 @@ _dots_show_file_diff() {
echo ""
echo "${FLOW_COLORS[header]}─────────────────────────────────────────────────${FLOW_COLORS[reset]}"
chezmoi diff "$file" 2>/dev/null | head -20
local line_count=$(chezmoi diff "$file" 2>/dev/null | wc -l | tr -d ' ')
local line_count=$(chezmoi diff "$file" 2>/dev/null | wc -l | command tr -d ' ')
if [[ $line_count -gt 20 ]]; then
echo "${FLOW_COLORS[muted]}... (${line_count} lines total, showing first 20)${FLOW_COLORS[reset]}"
fi
Expand Down Expand Up @@ -1268,7 +1268,7 @@ _dots_doctor_check_chezmoi_health() {
fi

# 5. Check managed file count
local managed_count=$(chezmoi managed 2>/dev/null | wc -l | tr -d ' ')
local managed_count=$(chezmoi managed 2>/dev/null | wc -l | command tr -d ' ')
if (( managed_count > 0 )); then
echo "${FLOW_COLORS[header]}│${FLOW_COLORS[reset]} ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} $managed_count files managed ${FLOW_COLORS[header]}│${FLOW_COLORS[reset]}"
else
Expand All @@ -1295,7 +1295,7 @@ _dots_doctor_check_chezmoi_health() {
local large_files=$(find "$chezmoi_dir" -type f -not -path "$chezmoi_dir/.git/*" -size +100k 2>/dev/null)
local large_count=0
if [[ -n "$large_files" ]]; then
large_count=$(echo "$large_files" | wc -l | tr -d ' ')
large_count=$(echo "$large_files" | wc -l | command tr -d ' ')
fi

if (( large_count > 0 )); then
Expand Down Expand Up @@ -1323,10 +1323,10 @@ _dots_doctor_check_chezmoi_health() {
local git_count=0
local git_dotf_count=0
if [[ -n "$git_dirs" ]]; then
git_count=$(echo "$git_dirs" | wc -l | tr -d ' ')
git_count=$(echo "$git_dirs" | wc -l | command tr -d ' ')
fi
if [[ -n "$git_dotf_dirs" ]]; then
git_dotf_count=$(echo "$git_dotf_dirs" | wc -l | tr -d ' ')
git_dotf_count=$(echo "$git_dotf_dirs" | wc -l | command tr -d ' ')
fi
local total_git=$((git_count + git_dotf_count))

Expand Down Expand Up @@ -1573,7 +1573,7 @@ fi

for name in "${selected_secrets[@]}"; do
# Convert to SCREAMING_SNAKE_CASE
local env_name=$(echo "${name}" | tr '[:lower:]-' '[:upper:]_')
local env_name=$(echo "${name}" | command tr '[:lower:]-' '[:upper:]_')
envrc_content+="export ${env_name}=\$(sec get ${name})
"
done
Expand All @@ -1600,7 +1600,7 @@ fi
echo ""
echo "Environment variables:"
for name in "${selected_secrets[@]}"; do
local env_name=$(echo "${name}" | tr '[:lower:]-' '[:upper:]_')
local env_name=$(echo "${name}" | command tr '[:lower:]-' '[:upper:]_')
echo " ${FLOW_COLORS[accent]}\$${env_name}${FLOW_COLORS[reset]}"
done
echo ""
Expand Down
4 changes: 2 additions & 2 deletions lib/dispatchers/email-dispatcher.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ _em_safety_gate() {
body_text=$(echo "$draft_content" | awk '/^$/{found=1;next} found{print}')

# Check for empty body
if [[ -z "$body_text" || "$(echo "$body_text" | tr -d '[:space:]')" == "" ]]; then
if [[ -z "$body_text" || "$(echo "$body_text" | command tr -d '[:space:]')" == "" ]]; then
_flow_log_warning "Empty email body"
fi

Expand All @@ -646,7 +646,7 @@ _em_safety_gate() {
if [[ -n "$body_text" ]]; then
echo "$body_text" | head -15
local total_lines
total_lines=$(echo "$body_text" | wc -l | tr -d ' ')
total_lines=$(echo "$body_text" | wc -l | command tr -d ' ')
(( total_lines > 15 )) && echo -e " ${_C_DIM}... ($((total_lines - 15)) more lines)${_C_NC}"
fi
echo -e "${_C_DIM}$(printf '%.0s─' {1..60})${_C_NC}"
Expand Down
Loading