diff --git a/README.md b/README.md index 77169d3..4b23c21 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ By default, Codex makes the change, Claude Code reviews it, and Codex retries un ## Install -Prereqs: Bash, git, and the agent CLIs you want to use. The default pairing requires `codex` and `claude`. -For the full interactive UI, install [`gum`](https://github.com/charmbracelet/gum) and [`fzf`](https://github.com/junegunn/fzf). They are optional; `devloop` falls back to plain terminal output when they are missing. +Required dependencies: Bash, git, Homebrew, `codex`, `claude`, `gum`, and `fzf`. +`install.sh` installs missing `gum` and `fzf` with Homebrew. Install the Codex and Claude Code CLIs before running a loop, then verify everything with `devloop doctor`. ```sh git clone https://github.com/satyaborg/devloop.git @@ -139,11 +139,11 @@ When stdout is a terminal, running `devloop` without arguments opens a menu: - `Continue a run`: pick a prior `.codex/tracks/*.md` and continue in that worktree. - `Open reports`: pick a prior report from any registered worktree. - `Settings`: view or change the shared spec path and set the run timeout. -- `Doctor`: verify required commands, optional UI tools, and installed skills. +- `Doctor`: verify required dependencies and installed skills. Nested menu screens keep `Back` as the final option, and Esc/cancel also returns to the previous menu without exiting Devloop. Interactive screens redraw in place instead of appending a fresh UI after each selection. -`gum` powers the branded help screen, prompts, confirmations, status output, paging, and setup screens. `fzf` powers searchable pickers for specs, tracks, and reports. +`gum` powers the branded help screen, prompts, confirmations, status output, paging, and setup screens. `fzf` powers searchable pickers for specs, tracks, and reports. Both are required and installed by `install.sh` when missing. ## What Devloop Does diff --git a/devloop b/devloop index 0510c1f..1923f0d 100755 --- a/devloop +++ b/devloop @@ -207,7 +207,7 @@ welcome_tui() { gum style --foreground "$UI_ACCENT_COLOR" --bold "Usage" printf ' devloop [options] [max=5]\n\n' gum style --foreground "$UI_ACCENT_COLOR" --bold "Common commands" - printf ' %-42s %s\n' "devloop doctor" "check required and optional tools" + printf ' %-42s %s\n' "devloop doctor" "check required tools" printf ' %-42s %s\n' "devloop reports" "open previous run reports" printf ' %-42s %s\n' "devloop status" "summarize tracked runs" printf ' %-42s %s\n' "devloop clean" "show safe cleanup candidates" diff --git a/install.sh b/install.sh index 301603b..b596fa4 100755 --- a/install.sh +++ b/install.sh @@ -18,6 +18,39 @@ BIN_DIR="${DEVLOOP_BIN_DIR:-$HOME/.local/bin}" TARGET="$BIN_DIR/devloop" SOURCE="$ROOT/devloop" SKILL_STATUS=0 +TOOL_STATUS=0 + +install_required_ui_tools() { + local missing=() + local tool + + for tool in gum fzf; do + if ! command -v "$tool" >/dev/null 2>&1; then + missing+=("$tool") + fi + done + + if [ "${#missing[@]}" -eq 0 ]; then + echo "required UI tools ready" + return 0 + fi + + if ! command -v brew >/dev/null 2>&1; then + echo "missing required UI tools: ${missing[*]}" >&2 + echo "install Homebrew, then rerun ./install.sh" >&2 + return 1 + fi + + echo "installing required UI tools: ${missing[*]}" + brew install "${missing[@]}" + + for tool in "${missing[@]}"; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "failed to install required UI tool: $tool" >&2 + return 1 + fi + done +} if [ ! -f "$SOURCE" ]; then echo "missing devloop executable: $SOURCE" >&2 @@ -29,6 +62,7 @@ chmod +x "$SOURCE" ln -sfn "$SOURCE" "$TARGET" echo "installed devloop -> $SOURCE" +install_required_ui_tools || TOOL_STATUS=$? devloop_install_skills "$ROOT" || SKILL_STATUS=$? case ":${PATH:-}:" in @@ -42,4 +76,7 @@ esac echo echo "try: devloop doctor" -exit "$SKILL_STATUS" +if [ "$TOOL_STATUS" -ne 0 ] || [ "$SKILL_STATUS" -ne 0 ]; then + exit 1 +fi +exit 0 diff --git a/skill_helpers.sh b/skill_helpers.sh index 6ec51e0..9ffaaf5 100644 --- a/skill_helpers.sh +++ b/skill_helpers.sh @@ -156,18 +156,6 @@ devloop_doctor_command() { return 1 } -devloop_doctor_optional_command() { - local command="$1" - local detail="$2" - local resolved - resolved="$(command -v "$command" 2>/dev/null || true)" - if [ -n "$resolved" ]; then - printf '[ok] optional %s: %s (%s)\n' "$command" "$resolved" "$detail" - return 0 - fi - printf '[skip] optional %s: %s\n' "$command" "$detail" -} - devloop_doctor_skills() { local root="$1" local skills_dir status @@ -241,14 +229,13 @@ devloop_doctor() { local status=0 printf 'devloop doctor\n' - printf 'Required\n' + printf 'Required dependencies\n' devloop_doctor_command devloop || status=1 devloop_doctor_command git || status=1 devloop_doctor_command codex || status=1 devloop_doctor_command claude || status=1 - printf '\nOptional UI\n' - devloop_doctor_optional_command gum "rich prompts, confirmations, status, and paging" - devloop_doctor_optional_command fzf "searchable spec, track, and report pickers" + devloop_doctor_command gum || status=1 + devloop_doctor_command fzf || status=1 printf '\nSkills\n' devloop_doctor_skills "$root" || status=1 diff --git a/tests/devloop_test.sh b/tests/devloop_test.sh index 10bf8f1..0446591 100755 --- a/tests/devloop_test.sh +++ b/tests/devloop_test.sh @@ -19,6 +19,13 @@ contains() { [[ "$haystack" == *"$needle"* ]] || fail "$label missing: $needle" } +not_contains() { + local haystack="$1" + local needle="$2" + local label="$3" + [[ "$haystack" != *"$needle"* ]] || fail "$label should not contain: $needle" +} + equals() { local actual="$1" local expected="$2" @@ -492,9 +499,31 @@ ok "release helpers" bin_dir="$work/bin" install_home="$work/install-home" -DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$REPO_ROOT/install.sh" >/tmp/devloop-install-test.out +tool_bin="$work/tool-bin" +mkdir -p "$tool_bin" +cat > "$tool_bin/brew" <<'BREW' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" != "install" ]; then exit 1; fi +shift +tool_dir="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)" +for formula in "$@"; do + case "$formula" in + gum|fzf) + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/$formula" + chmod +x "$tool_dir/$formula" + ;; + *) exit 1 ;; + esac +done +BREW +chmod +x "$tool_bin/brew" +install_path="$tool_bin:/usr/bin:/bin:/usr/sbin:/sbin" +DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-test.out [[ -x "$REPO_ROOT/devloop" ]] || fail "devloop is not executable" [[ -L "$bin_dir/devloop" ]] || fail "installer did not create symlink" +PATH="$install_path" command -v gum >/dev/null 2>&1 || fail "installer did not make gum available" +PATH="$install_path" command -v fzf >/dev/null 2>&1 || fail "installer did not make fzf available" [[ -f "$install_home/.agents/skills/devloop-spec/SKILL.md" ]] || fail "installer did not install Codex spec skill" [[ -f "$install_home/.agents/skills/devloop-spec/references/spec-template.md" ]] || fail "installer did not install Codex spec template reference" [[ -f "$install_home/.agents/skills/devloop-review/SKILL.md" ]] || fail "installer did not install Codex review skill" @@ -507,11 +536,11 @@ contains "$(cat /tmp/devloop-help-test.out)" "Spec-driven code and review loop." ok "installer" printf '%s\n' "user edit" >> "$install_home/.agents/skills/devloop-review/SKILL.md" -DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$REPO_ROOT/install.sh" >/tmp/devloop-install-skip.out 2>&1 +DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-skip.out 2>&1 contains "$(cat /tmp/devloop-install-skip.out)" "skipping modified skill" "installer modified skill guard" contains "$(cat /tmp/devloop-install-skip.out)" "try: devloop doctor" "installer guidance after skill skip" contains "$(cat "$install_home/.agents/skills/devloop-review/SKILL.md")" "user edit" "installer modified skill preserved" -DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$REPO_ROOT/install.sh" >/tmp/devloop-install-force.out +DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-force.out if grep -q "user edit" "$install_home/.agents/skills/devloop-review/SKILL.md"; then fail "installer force did not restore skill"; fi ok "installer skill updates" @@ -520,10 +549,15 @@ mkdir -p "$fake_bin" printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/codex" printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/claude" chmod +x "$fake_bin/codex" "$fake_bin/claude" -doctor_output="$(HOME="$install_home" PATH="$bin_dir:$fake_bin:$PATH" "$bin_dir/devloop" doctor 2>&1)" +doctor_output="$(HOME="$install_home" PATH="$bin_dir:$tool_bin:$fake_bin:$PATH" "$bin_dir/devloop" doctor 2>&1)" contains "$doctor_output" "devloop doctor: ready" "doctor" +contains "$doctor_output" "Required dependencies" "doctor" +contains "$doctor_output" "[ok] codex:" "doctor" +contains "$doctor_output" "[ok] claude:" "doctor" contains "$doctor_output" "[ok] skill devloop-spec" "doctor" -contains "$doctor_output" "Optional UI" "doctor" +contains "$doctor_output" "[ok] gum:" "doctor" +contains "$doctor_output" "[ok] fzf:" "doctor" +not_contains "$doctor_output" "Optional UI" "doctor" contains "$doctor_output" "$install_home/.agents/skills/devloop-spec" "doctor Codex skill" contains "$doctor_output" "$install_home/.claude/skills/devloop-spec" "doctor Claude skill" ok "doctor"