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

## [Unreleased]

### Added

- **`doctor` INSTALLATION section** — detects Homebrew distribution failures that leave a user silently on a broken or stale install: opt/Cellar symlink integrity (installed-but-unlinked), shell-loaded version vs. installed Cellar keg drift, and flow-cli's own man pages losing a case-insensitive name collision to another formula (the class of bug fixed in homebrew-tap PR #135). Skips silently for non-Homebrew installs.

## [7.14.0] — 2026-07-02 — planning-coordination: shared accessors + dark-ready atlas agenda + .STATUS enforcer

### Added
Expand Down
87 changes: 87 additions & 0 deletions commands/doctor.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ doctor() {
_doctor_check_cmd "git" "" "shell"
_doctor_log_quiet ""

# ──────────────────────────────────────────────────────────────
# INSTALLATION (Homebrew distribution health)
# ──────────────────────────────────────────────────────────────
_doctor_check_installation

# ──────────────────────────────────────────────────────────────
# REQUIRED
# ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1160,6 +1165,88 @@ _doctor_update_docs() {
# HELPER FUNCTIONS
# ============================================================================

# =============================================================================
# INSTALLATION / DISTRIBUTION HEALTH
# =============================================================================
# Catches the class of bug fixed in homebrew-tap PR #135: a Homebrew formula
# install that silently fails to link (man-page name collision, stale Cellar
# keg after a formula fix) leaves the user on an old version with no error
# message — `brew upgrade` reports "already installed" even when broken.
# Checks are read-only and only apply when flow-cli was installed via brew;
# git-clone / plugin-manager installs skip silently (nothing to check).

_doctor_check_installation() {
_doctor_log_quiet "${FLOW_COLORS[bold]}📦 INSTALLATION${FLOW_COLORS[reset]}"

if ! command -v brew >/dev/null 2>&1; then
_doctor_log_quiet " ${FLOW_COLORS[muted]}○ Homebrew not present — skipping${FLOW_COLORS[reset]}"
_doctor_log_quiet ""
return 0
fi

if ! brew list --formula 2>/dev/null | grep -qx "flow-cli"; then
_doctor_log_quiet " ${FLOW_COLORS[muted]}○ flow-cli not installed via Homebrew — skipping${FLOW_COLORS[reset]}"
_doctor_log_quiet ""
return 0
fi

local brew_prefix
brew_prefix=$(brew --prefix 2>/dev/null)
local opt_path="${brew_prefix}/opt/flow-cli"
local installed_version
installed_version=$(brew list --versions flow-cli 2>/dev/null | awk '{print $2}')

# 1. Link integrity — installed-but-unlinked is the exact symptom of the
# man-page-collision bug: `brew install`/`upgrade` "succeeds" but the
# opt symlink is never created, so `source $(brew --prefix)/opt/flow-cli/...` fails.
if [[ -L "$opt_path" && -d "${opt_path}/." ]]; then
_doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} Homebrew link ${FLOW_COLORS[muted]}(v${installed_version})${FLOW_COLORS[reset]}"
else
_doctor_log_quiet " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} flow-cli installed but not linked ${FLOW_COLORS[muted]}← brew link flow-cli (or: brew reinstall flow-cli)${FLOW_COLORS[reset]}"
fi

# 2. Version drift — the shell's already-sourced $FLOW_VERSION vs. what
# Homebrew currently has installed. Catches "reinstalled/upgraded but
# forgot to restart the shell" silently running stale code.
if [[ -n "$installed_version" && -n "$FLOW_VERSION" && "$installed_version" != "$FLOW_VERSION" ]]; then
_doctor_log_quiet " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} Shell has v${FLOW_VERSION} loaded, Homebrew has v${installed_version} installed ${FLOW_COLORS[muted]}← restart your shell${FLOW_COLORS[reset]}"
fi

# 3. Man-page link check — does every man page flow-cli's own Cellar keg
# ships actually resolve (via the shared man1 symlink) back into
# flow-cli's keg? A page that "exists" at the shared path but resolves
# into a DIFFERENT formula's Cellar dir means flow-cli's own page lost
# a case-insensitive name collision and was silently never linked (the
# PR #135 class of bug — deliberately scoped to flow-cli's own pages,
# not a whole-Cellar audit: Homebrew's own keg-only-formula convention
# (e.g. lua vs lua@5.4) produces same-name kegs constantly and is not a
# bug — only formulae that both actually attempt to link collide).
local _doctor_brew_cellar
_doctor_brew_cellar=$(brew --cellar 2>/dev/null)
local flow_man1_dir="${_doctor_brew_cellar}/flow-cli/${installed_version}/share/man/man1"
if [[ -d "$flow_man1_dir" ]]; then
local shared_man1="${brew_prefix}/share/man/man1"
local not_linked=()
# `local` is hoisted OUT of the loop deliberately: redeclaring a local
# var on every glob-driven iteration triggers a spurious "name=value"
# echo to stdout on some zsh builds (reproduced independent of this
# codebase — a fresh `zsh -f` script with the same shape shows it too).
local mp_name link_target
for manpage in "$flow_man1_dir"/*.1(N); do
mp_name="${manpage:t}"
link_target=$(readlink "${shared_man1}/${mp_name}" 2>/dev/null)
[[ "$link_target" == *"/flow-cli/"* ]] || not_linked+=("$mp_name")
done
if (( ${#not_linked[@]} > 0 )); then
_doctor_log_quiet " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} flow-cli man page(s) not linked (collision with another formula) ${FLOW_COLORS[muted]}${not_linked[*]}${FLOW_COLORS[reset]}"
else
_doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} All flow-cli man pages linked cleanly"
fi
fi

_doctor_log_quiet ""
}

_doctor_check_cmd() {
local cmd="$1"
local install_spec="$2" # Format: "brew" or "brew:package" or "npm:package" or "pip"
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/), and this pro

## [Unreleased]

### Added

- **`doctor` INSTALLATION section** — detects Homebrew distribution failures that leave a user silently on a broken or stale install: opt/Cellar symlink integrity (installed-but-unlinked), shell-loaded version vs. installed Cellar keg drift, and flow-cli's own man pages losing a case-insensitive name collision to another formula (the class of bug fixed in homebrew-tap PR #135). Skips silently for non-Homebrew installs.

## [7.14.0] — 2026-07-02 — planning-coordination: shared accessors + dark-ready atlas agenda + .STATUS enforcer

### Added
Expand Down
29 changes: 19 additions & 10 deletions docs/reference/REFCARD-DOCTOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,28 @@ After each run, teach doctor writes `.flow/doctor-status.json`. The health dot s
- `zsh` - Shell
- `git` - Version control

### 2. Required Tools
### 2. Installation (Homebrew distribution health)

- Homebrew opt/Cellar link integrity (installed-but-unlinked detection)
- Shell-loaded version vs. installed Cellar keg version drift
- flow-cli's own man pages resolving cleanly (vs. losing a case-insensitive
name collision to another formula)

Skips silently for non-Homebrew installs (git-clone, plugin manager).

### 3. Required Tools

- `fzf` - Fuzzy finder

### 3. Recommended Tools
### 4. Recommended Tools

- `eza` - Enhanced ls
- `bat` - Enhanced cat
- `zoxide` - Smart cd
- `fd` - Enhanced find
- `rg` (ripgrep) - Enhanced grep

### 4. Optional Tools
### 5. Optional Tools

- `dust` - Disk usage
- `duf` - Disk free
Expand All @@ -117,12 +126,12 @@ After each run, teach doctor writes `.flow/doctor-status.json`. The health dot s
- `gh` - GitHub CLI
- `jq` - JSON processor

### 5. Integrations
### 6. Integrations

- `atlas` - State management
- `radian` - R console (if R exists)

### 6. Email (conditional — when `em` loaded)
### 7. Email (conditional — when `em` loaded)

- `himalaya` - Email CLI backend (required, version >= 1.0.0)
- `w3m`/`lynx`/`pandoc` - HTML rendering (any-of, recommended)
Expand All @@ -132,33 +141,33 @@ After each run, teach doctor writes `.flow/doctor-status.json`. The health dot s
- `claude`/`gemini` - AI backend (conditional on `$FLOW_EMAIL_AI`)
- Config summary: AI backend, timeout, page size, folder, config file

### 7. ZSH Plugin Manager
### 8. ZSH Plugin Manager

Checks:
- antidote/zinit/oh-my-zsh installed
- Plugin bundle file

### 8. ZSH Plugins
### 9. ZSH Plugins

- powerlevel10k
- zsh-autosuggestions
- zsh-syntax-highlighting
- zsh-completions

### 9. flow-cli Status
### 10. flow-cli Status

- Plugin loaded
- Version
- Atlas connection

### 10. GitHub Token
### 11. GitHub Token

- Token configured
- Token validity
- Token expiration
- Token-dependent services (gh CLI, Claude MCP)

### 11. Aliases
### 12. Aliases

- Total alias count
- Shadow detection
Expand Down
Loading