agent-orchestrator/scripts/ao-doctor.sh

350 lines
9.3 KiB
Bash
Executable File

#!/bin/bash
set -uo pipefail
FIX_MODE=false
while [ $# -gt 0 ]; do
case "$1" in
--fix)
FIX_MODE=true
;;
-h|--help)
cat <<'EOF'
Usage: ao doctor [--fix]
Checks install, PATH, binaries, service health, stale temp files, and runtime sanity.
Options:
--fix Apply safe fixes for missing launcher links, missing support dirs, and stale temp files
EOF
exit 0
;;
*)
printf 'Unknown option: %s\n' "$1" >&2
exit 1
;;
esac
shift
done
REPO_ROOT="${AO_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
DEFAULT_CONFIG_HOME="${HOME:-$REPO_ROOT}"
PASS_COUNT=0
WARN_COUNT=0
FAIL_COUNT=0
FIX_COUNT=0
pass() {
PASS_COUNT=$((PASS_COUNT + 1))
printf 'PASS %s\n' "$1"
}
warn() {
WARN_COUNT=$((WARN_COUNT + 1))
printf 'WARN %s\n' "$1"
}
fail() {
FAIL_COUNT=$((FAIL_COUNT + 1))
printf 'FAIL %s\n' "$1"
}
fixed() {
FIX_COUNT=$((FIX_COUNT + 1))
printf 'FIXED %s\n' "$1"
}
expand_home() {
case "$1" in
~/*)
printf '%s/%s' "$DEFAULT_CONFIG_HOME" "${1#~/}"
;;
*)
printf '%s' "$1"
;;
esac
}
find_config() {
if [ -n "${AO_CONFIG_PATH:-}" ] && [ -f "$AO_CONFIG_PATH" ]; then
printf '%s\n' "$AO_CONFIG_PATH"
return 0
fi
local current_dir="$PWD"
while [ "$current_dir" != "/" ]; do
if [ -f "$current_dir/agent-orchestrator.yaml" ]; then
printf '%s\n' "$current_dir/agent-orchestrator.yaml"
return 0
fi
if [ -f "$current_dir/agent-orchestrator.yml" ]; then
printf '%s\n' "$current_dir/agent-orchestrator.yml"
return 0
fi
current_dir="$(dirname "$current_dir")"
done
if [ -f "$REPO_ROOT/agent-orchestrator.yaml" ]; then
printf '%s\n' "$REPO_ROOT/agent-orchestrator.yaml"
return 0
fi
if [ -f "$DEFAULT_CONFIG_HOME/.agent-orchestrator.yaml" ]; then
printf '%s\n' "$DEFAULT_CONFIG_HOME/.agent-orchestrator.yaml"
return 0
fi
return 1
}
read_config_value() {
local key="$1"
local file="$2"
local raw
local value
raw="$(grep -E "^[[:space:]]*${key}:" "$file" | head -n 1 | cut -d: -f2- || true)"
raw="${raw%%[[:space:]]#*}"
value="$(printf '%s' "$raw" | tr -d '"' | xargs 2>/dev/null || true)"
printf '%s' "$value"
}
ensure_dir() {
local dir_path="$1"
local label="$2"
local fix_hint="$3"
if [ -d "$dir_path" ]; then
pass "$label exists at $dir_path"
return 0
fi
if [ "$FIX_MODE" = true ]; then
if mkdir -p "$dir_path"; then
fixed "$label created at $dir_path"
return 0
fi
fail "$label could not be created at $dir_path. Fix: $fix_hint"
return 1
fi
warn "$label is missing at $dir_path. Fix: $fix_hint"
}
check_command() {
local name="$1"
local required="$2"
local fix_hint="$3"
local command_path
command_path="$(command -v "$name" 2>/dev/null || true)"
if [ -z "$command_path" ]; then
if [ "$required" = "required" ]; then
fail "$name is not in PATH. Fix: $fix_hint"
else
warn "$name is not in PATH. Fix: $fix_hint"
fi
return 1
fi
pass "$name resolves to $command_path"
return 0
}
check_node() {
if ! check_command "node" "required" "install Node.js 20+ and reopen your shell"; then
return
fi
local version major
version="$(node --version 2>/dev/null || true)"
major="${version#v}"
major="${major%%.*}"
if [ -z "$major" ] || [ "$major" -lt 20 ]; then
fail "Node.js 20+ is required, found ${version:-unknown}. Fix: install Node.js 20+"
return
fi
pass "Node.js version ${version} is supported"
}
check_git() {
if ! check_command "git" "required" "install git 2.25+ and reopen your shell"; then
return
fi
local version major minor
version="$(git --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n 1)"
major="${version%%.*}"
minor="${version#*.}"
minor="${minor%%.*}"
if [ -z "$version" ] || [ "$major" -lt 2 ] || { [ "$major" -eq 2 ] && [ "$minor" -lt 25 ]; }; then
fail "git 2.25+ is required, found ${version:-unknown}. Fix: upgrade git"
return
fi
pass "git version ${version} supports worktrees"
}
check_pnpm() {
if ! check_command "pnpm" "required" "enable corepack or run npm install -g pnpm"; then
return
fi
local version
version="$(pnpm --version 2>/dev/null || true)"
pass "pnpm version ${version:-unknown} is available"
}
check_launcher() {
local ao_path
ao_path="$(command -v ao 2>/dev/null || true)"
if [ -n "$ao_path" ]; then
pass "ao launcher resolves to $ao_path"
return
fi
if [ "$FIX_MODE" = true ] && command -v npm >/dev/null 2>&1 && [ -d "$REPO_ROOT/packages/ao" ]; then
if (cd "$REPO_ROOT/packages/ao" && npm link >/dev/null 2>&1) && command -v ao >/dev/null 2>&1; then
fixed "ao launcher refreshed with npm link"
return
fi
if [ -t 0 ]; then
printf ' Permission denied. Retrying with sudo...\n'
if (cd "$REPO_ROOT/packages/ao" && sudo npm link >/dev/null 2>&1) && command -v ao >/dev/null 2>&1; then
fixed "ao launcher refreshed with sudo npm link"
return
fi
fi
warn "ao launcher refresh failed. Fix: cd $REPO_ROOT/packages/ao && sudo npm link"
return
fi
warn "ao launcher is not in PATH. Fix: cd $REPO_ROOT && bash scripts/setup.sh"
}
check_tmux() {
if ! command -v tmux >/dev/null 2>&1; then
warn "tmux is not installed. Fix: install tmux for the default runtime"
return
fi
if tmux -V >/dev/null 2>&1 && tmux start-server >/dev/null 2>&1; then
pass "tmux is installed and the server can start"
return
fi
warn "tmux is installed but failed a basic server health check. Fix: restart tmux or reinstall it"
}
check_gh() {
if ! command -v gh >/dev/null 2>&1; then
warn "GitHub CLI is not installed. Fix: install gh from https://cli.github.com/"
return
fi
if gh auth status >/dev/null 2>&1; then
pass "gh is installed and authenticated"
return
fi
warn "gh is installed but not authenticated. Fix: run gh auth login"
}
check_install_layout() {
if [ -d "$REPO_ROOT/node_modules" ]; then
pass "dependencies are installed at $REPO_ROOT/node_modules"
else
fail "dependencies are missing at $REPO_ROOT/node_modules. Fix: run pnpm install"
fi
if [ -f "$REPO_ROOT/packages/core/dist/index.js" ]; then
pass "core package is built"
else
fail "core package is not built. Fix: run pnpm --filter @aoagents/ao-core build"
fi
if [ -f "$REPO_ROOT/packages/cli/dist/index.js" ]; then
pass "CLI package is built"
else
fail "CLI package is not built. Fix: run pnpm --filter @aoagents/ao-cli build"
fi
}
check_runtime_sanity() {
if [ ! -f "$REPO_ROOT/packages/agent-orchestrator/bin/ao.js" ]; then
fail "launcher entrypoint is missing. Fix: reinstall from a clean checkout"
return
fi
if node "$REPO_ROOT/packages/agent-orchestrator/bin/ao.js" --version >/dev/null 2>&1; then
pass "launcher runtime sanity check passed (ao --version)"
else
fail "launcher runtime sanity check failed. Fix: run pnpm build and refresh the launcher"
fi
}
check_config_dirs() {
local config_path data_dir worktree_dir
config_path="$(find_config || true)"
if [ -z "$config_path" ]; then
warn "No agent-orchestrator config was found. Fix: run ao init --auto in a target repo"
return
fi
pass "config found at $config_path"
data_dir="$(read_config_value dataDir "$config_path")"
worktree_dir="$(read_config_value worktreeDir "$config_path")"
if [ -z "$data_dir" ]; then
data_dir="$DEFAULT_CONFIG_HOME/.agent-orchestrator"
fi
if [ -z "$worktree_dir" ]; then
worktree_dir="$DEFAULT_CONFIG_HOME/.worktrees"
fi
data_dir="$(expand_home "$data_dir")"
worktree_dir="$(expand_home "$worktree_dir")"
ensure_dir "$data_dir" "metadata directory" "mkdir -p $data_dir"
ensure_dir "$worktree_dir" "worktree directory" "mkdir -p $worktree_dir"
}
check_stale_temp_files() {
local temp_root stale_count deleted_count
temp_root="${AO_DOCTOR_TMP_ROOT:-${TMPDIR:-/tmp}/agent-orchestrator}"
if [ ! -d "$temp_root" ]; then
pass "temp root exists check skipped because $temp_root does not exist"
return
fi
stale_count="$(find "$temp_root" -maxdepth 1 -type f -mmin +60 \( -name 'ao-*.tmp' -o -name 'ao-*.pid' -o -name 'ao-*.lock' \) | wc -l | tr -d ' ')"
if [ "$stale_count" = "0" ]; then
pass "no stale temp files were detected under $temp_root"
return
fi
if [ "$FIX_MODE" = true ]; then
deleted_count="$(find "$temp_root" -maxdepth 1 -type f -mmin +60 \( -name 'ao-*.tmp' -o -name 'ao-*.pid' -o -name 'ao-*.lock' \) -delete -print | wc -l | tr -d ' ')"
if [ "$deleted_count" = "$stale_count" ]; then
fixed "$deleted_count stale temp files removed from $temp_root"
return
fi
warn "Only removed $deleted_count of $stale_count stale temp files from $temp_root. Fix: inspect that directory manually"
return
fi
warn "$stale_count stale temp files older than 60 minutes found under $temp_root. Fix: rerun ao doctor --fix"
}
printf 'Agent Orchestrator Doctor\n\n'
check_node
check_git
check_pnpm
check_launcher
check_tmux
check_gh
check_config_dirs
check_stale_temp_files
check_install_layout
check_runtime_sanity
printf '\nResults: %s PASS, %s WARN, %s FAIL, %s FIXED\n' "$PASS_COUNT" "$WARN_COUNT" "$FAIL_COUNT" "$FIX_COUNT"
if [ "$FAIL_COUNT" -gt 0 ]; then
printf 'Environment needs attention before AO is safe to update or run.\n' >&2
exit 1
fi
printf 'Environment looks healthy enough to run Agent Orchestrator.\n'