From f9fa7b00bd359e56ef67fd3c74828998808c3cce Mon Sep 17 00:00:00 2001 From: satyaborg Date: Tue, 30 Jun 2026 17:47:19 +1000 Subject: [PATCH] feat: install missing dependencies by default --- CONTRIBUTING.md | 4 +- README.md | 1 + scripts/devloop_test.sh | 132 ++++++++++++++++++++++++++++++-- scripts/install.remote.sh | 156 ++++++++++++++++++++++++++------------ scripts/install.sh | 46 ++++++++--- site/public/install | 2 +- 6 files changed, 268 insertions(+), 73 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39b29b1..33819df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,11 +7,11 @@ Thanks for your interest in improving devloop. This is a single-file Bash CLI wi ```sh git clone https://github.com/satyaborg/devloop.git cd devloop -./scripts/install.sh # symlinks the checkout, installs gum/fzf and bundled skills +./scripts/install.sh # symlinks the checkout, installs missing dependencies and bundled skills devloop doctor # verify required dependencies ``` -Required to run a loop: Bash, git, `gum`, `fzf`, and the `codex` and `claude` CLIs. The test suite itself needs only Bash, git, and coreutils, so you can develop and test without the agent CLIs installed. +Required to run a loop: Bash, git, `glow`, `gum`, `fzf`, and the `codex` and `claude` CLIs. The install scripts use Homebrew to install missing git/UI tools and the Codex/Claude Code casks when `brew` is available. The test suite itself needs only Bash, git, and coreutils, so you can develop and test without the agent CLIs installed. ## Development loop diff --git a/README.md b/README.md index 49c690c..aef3033 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ cd devloop ``` > Requires Bash, git, `codex`, `claude`, `glow`, `gum`, and `fzf`. Run `devloop doctor` to check. +> The installers use Homebrew to install missing git/UI tools and the Codex/Claude Code casks when `brew` is available. Uninstall with `./scripts/uninstall.sh` (`--dry-run` to preview). diff --git a/scripts/devloop_test.sh b/scripts/devloop_test.sh index d9d9854..6b448c1 100755 --- a/scripts/devloop_test.sh +++ b/scripts/devloop_test.sh @@ -201,7 +201,7 @@ bootstrap_output="$( PATH="$bootstrap_bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/site/public/install" --dry-run )" -contains "$bootstrap_output" "installer args: <--version> <9.8.7> <--dry-run>" "site install bootstrap" +contains "$bootstrap_output" "installer args: <--yes> <--version> <9.8.7> <--dry-run>" "site install bootstrap" contains "$(cat "$bootstrap_log")" "https://version.example/devloop" "site install bootstrap version" contains "$(cat "$bootstrap_log")" "https://raw.example/devloop/v9.8.7/scripts/install.remote.sh" "site install bootstrap installer" : > "$bootstrap_log" @@ -212,7 +212,7 @@ bootstrap_pinned_output="$( PATH="$bootstrap_bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/site/public/install" --version=1.2.3 --dry-run )" -contains "$bootstrap_pinned_output" "installer args: <--version> <1.2.3> <--dry-run>" "site install bootstrap pinned" +contains "$bootstrap_pinned_output" "installer args: <--yes> <--version> <1.2.3> <--dry-run>" "site install bootstrap pinned" not_contains "$(cat "$bootstrap_log")" "https://version.example/devloop" "site install bootstrap pinned" contains "$(cat "$bootstrap_log")" "https://raw.example/devloop/v1.2.3/scripts/install.remote.sh" "site install bootstrap pinned" : > "$bootstrap_log" @@ -1022,10 +1022,10 @@ contains "$remote_dry_output" "verify: $remote_release_base/v$remote_version/dev contains "$remote_dry_output" "install: $remote_custom_root/$remote_version" "remote dry run install dir" contains "$remote_dry_output" "link: $remote_custom_bin/devloop -> $remote_custom_root/$remote_version/devloop" "remote dry run bin dir" contains "$remote_dry_output" "skills: $work/remote-dry-home/.agents/skills, $work/remote-dry-home/.claude/skills" "remote dry run skills" -contains "$remote_dry_output" "missing UI tools: glow gum fzf" "remote missing UI guidance" +contains "$remote_dry_output" "missing required dependencies: glow gum fzf" "remote missing UI guidance" contains "$remote_dry_output" "install with: brew install glow gum fzf" "remote missing UI guidance" -contains "$remote_dry_output" "missing agent CLIs: codex claude" "remote missing agent guidance" -contains "$remote_dry_output" "Devloop does not install codex or claude automatically." "remote missing agent guidance" +contains "$remote_dry_output" "missing required cask dependencies: codex claude-code" "remote missing agent guidance" +contains "$remote_dry_output" "install with: brew install --cask codex claude-code" "remote missing agent guidance" [[ ! -e "$remote_custom_root" ]] || fail "remote dry run created install root" [[ ! -e "$remote_custom_bin" ]] || fail "remote dry run created bin dir" ok "remote installer dry run" @@ -1072,6 +1072,56 @@ contains "$tampered_output" "checksum mismatch" "remote checksum mismatch" [[ ! -e "$tampered_home/.local/bin/devloop" ]] || fail "checksum mismatch created devloop symlink" ok "remote installer rejects checksum mismatch" +remote_no_brew_home="$work/remote-no-brew-home" +if remote_no_brew_output="$( + HOME="$remote_no_brew_home" PATH="$remote_no_tools_path" bash "$REMOTE_INSTALLER" \ + --yes \ + --version "$remote_version" \ + --release-base-url "$remote_release_base" \ + 2>&1 +)"; then + printf '%s\n' "$remote_no_brew_output" >&2 + fail "remote installer accepted missing dependencies without Homebrew" +fi +contains "$remote_no_brew_output" "install Homebrew, then rerun the installer." "remote installer missing Homebrew" +ok "remote installer fails when Homebrew is unavailable" + +remote_needs_yes_bin="$work/remote-needs-yes-bin" +mkdir -p "$remote_needs_yes_bin" +printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$remote_needs_yes_bin/brew" +chmod +x "$remote_needs_yes_bin/brew" +remote_needs_yes_path="$remote_needs_yes_bin:/usr/bin:/bin:/usr/sbin:/sbin" +if remote_needs_yes_output="$( + HOME="$work/remote-needs-yes-home" PATH="$remote_needs_yes_path" bash "$REMOTE_INSTALLER" \ + --version "$remote_version" \ + --release-base-url "$remote_release_base" \ + 2>&1 +)"; then + printf '%s\n' "$remote_needs_yes_output" >&2 + fail "remote installer installed dependencies without --yes in a non-TTY" +fi +contains "$remote_needs_yes_output" "pass --yes to install missing dependencies without a prompt." "remote installer non-TTY --yes guard" +not_contains "$remote_needs_yes_output" "installing required dependencies" "remote installer non-TTY --yes guard" +ok "remote installer requires --yes for non-TTY dependency installs" + +remote_noop_brew_bin="$work/remote-noop-brew-bin" +mkdir -p "$remote_noop_brew_bin" +printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$remote_noop_brew_bin/brew" +chmod +x "$remote_noop_brew_bin/brew" +remote_noop_brew_path="$remote_noop_brew_bin:/usr/bin:/bin:/usr/sbin:/sbin" +if remote_noop_brew_output="$( + HOME="$work/remote-noop-brew-home" PATH="$remote_noop_brew_path" bash "$REMOTE_INSTALLER" \ + --yes \ + --version "$remote_version" \ + --release-base-url "$remote_release_base" \ + 2>&1 +)"; then + printf '%s\n' "$remote_noop_brew_output" >&2 + fail "remote installer accepted dependency install that left commands missing" +fi +contains "$remote_noop_brew_output" "still missing required dependencies:" "remote installer verifies dependency installs" +ok "remote installer fails when dependencies remain missing" + remote_tool_bin="$work/remote-tool-bin" mkdir -p "$remote_tool_bin" for tool in glow gum fzf codex claude; do @@ -1221,6 +1271,52 @@ contains "$prompt_failure_output" "menu after failed check" "automatic update pr not_contains "$prompt_failure_output" "unexpected prompt" "automatic update prompt resolver skip" ok "automatic update prompt skip paths" +remote_bootstrap_bin="$work/remote-bootstrap-bin" +mkdir -p "$remote_bootstrap_bin" +cat > "$remote_bootstrap_bin/brew" <<'BREW' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" != "install" ]; then exit 1; fi +shift +if [ "${1:-}" = "--cask" ]; then shift; fi +tool_dir="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)" +for formula in "$@"; do + case "$formula" in + git|glow|gum|fzf|codex) + command_name="$formula" + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/$command_name" + chmod +x "$tool_dir/$command_name" + ;; + claude-code) + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/claude" + chmod +x "$tool_dir/claude" + ;; + *) exit 1 ;; + esac +done +BREW +chmod +x "$remote_bootstrap_bin/brew" +remote_bootstrap_home="$work/remote-bootstrap-home" +remote_bootstrap_path="$remote_bootstrap_bin:/usr/bin:/bin:/usr/sbin:/sbin" +if ! remote_bootstrap_output="$( + HOME="$remote_bootstrap_home" PATH="$remote_bootstrap_path" bash "$REMOTE_INSTALLER" \ + --yes \ + --version "$remote_version" \ + --release-base-url "$remote_release_base" \ + 2>&1 +)"; then + printf '%s\n' "$remote_bootstrap_output" >&2 + fail "remote installer dependency bootstrap failed" +fi +contains "$remote_bootstrap_output" "installing required dependencies: glow gum fzf" "remote installer installs UI dependencies" +contains "$remote_bootstrap_output" "installing required cask dependencies: codex claude-code" "remote installer installs agent dependencies" +PATH="$remote_bootstrap_path" command -v glow >/dev/null 2>&1 || fail "remote installer did not make glow available" +PATH="$remote_bootstrap_path" command -v gum >/dev/null 2>&1 || fail "remote installer did not make gum available" +PATH="$remote_bootstrap_path" command -v fzf >/dev/null 2>&1 || fail "remote installer did not make fzf available" +PATH="$remote_bootstrap_path" command -v codex >/dev/null 2>&1 || fail "remote installer did not make codex available" +PATH="$remote_bootstrap_path" command -v claude >/dev/null 2>&1 || fail "remote installer did not make claude available" +ok "remote installer bootstraps missing dependencies" + remote_home="$work/remote-home" remote_install_output="$( HOME="$remote_home" PATH="$remote_path" bash "$REMOTE_INSTALLER" \ @@ -1317,6 +1413,18 @@ contains "$uninstall_again_output" "devloop uninstalled" "uninstall idempotent" not_contains "$uninstall_again_output" "removed symlink" "uninstall idempotent no-op" ok "uninstall script removes installed footprint" +install_missing_brew_home="$work/install-missing-brew-home" +install_missing_brew_bin="$work/install-missing-brew-bin" +if install_missing_brew_output="$( + DEVLOOP_BIN_DIR="$install_missing_brew_bin" HOME="$install_missing_brew_home" PATH="/usr/bin:/bin:/usr/sbin:/sbin" "$SCRIPTS_DIR/install.sh" 2>&1 +)"; then + printf '%s\n' "$install_missing_brew_output" >&2 + fail "installer accepted missing dependencies without Homebrew" +fi +contains "$install_missing_brew_output" "install Homebrew, then rerun ./scripts/install.sh" "installer missing Homebrew" +not_contains "$install_missing_brew_output" "missing required dependencies: " "installer missing dependency spacing" +ok "installer fails when Homebrew is unavailable" + bin_dir="$work/bin" install_home="$work/install-home" tool_bin="$work/tool-bin" @@ -1326,12 +1434,18 @@ cat > "$tool_bin/brew" <<'BREW' set -euo pipefail if [ "${1:-}" != "install" ]; then exit 1; fi shift +if [ "${1:-}" = "--cask" ]; then shift; fi tool_dir="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)" for formula in "$@"; do case "$formula" in - glow|gum|fzf) - printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/$formula" - chmod +x "$tool_dir/$formula" + git|glow|gum|fzf|codex) + command_name="$formula" + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/$command_name" + chmod +x "$tool_dir/$command_name" + ;; + claude-code) + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/claude" + chmod +x "$tool_dir/claude" ;; *) exit 1 ;; esac @@ -1346,6 +1460,8 @@ contains "$(cat /tmp/devloop-install-test.out)" "gh auth login" "installer optio PATH="$install_path" command -v glow >/dev/null 2>&1 || fail "installer did not make glow available" 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" +PATH="$install_path" command -v codex >/dev/null 2>&1 || fail "installer did not make codex available" +PATH="$install_path" command -v claude >/dev/null 2>&1 || fail "installer did not make claude 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" [[ ! -e "$install_home/.agents/skills/devloop-spec/scripts/render.sh" ]] || fail "installer installed removed Codex spec renderer" diff --git a/scripts/install.remote.sh b/scripts/install.remote.sh index d77d7ab..ce5d21e 100755 --- a/scripts/install.remote.sh +++ b/scripts/install.remote.sh @@ -44,7 +44,7 @@ Primary install: curl -fsSL https://devloop.sh/install | bash Options: - --yes Run without optional prompts. + --yes Install missing dependencies without prompting. --version Install a specific tagged version, for example 0.2.0. --no-skills Install only the devloop CLI. --dry-run Print planned actions without changing files. @@ -200,61 +200,117 @@ missing_commands() { printf '%s\n' "${missing[*]}" } -check_ui_tools() { - local missing_text="$1" - local missing_items=() - local reply - if [ -z "$missing_text" ]; then - info "[ok] glow: $(command -v glow)" - info "[ok] gum: $(command -v gum)" - info "[ok] fzf: $(command -v fzf)" +missing_agent_casks() { + local missing=() + if ! command -v codex >/dev/null 2>&1; then + missing+=(codex) + fi + if ! command -v claude >/dev/null 2>&1; then + missing+=(claude-code) + fi + if [ "${#missing[@]}" -eq 0 ]; then + printf '\n' return 0 fi + printf '%s\n' "${missing[*]}" +} - read -r -a missing_items <<< "$missing_text" - info "missing UI tools: $missing_text" - if command -v brew >/dev/null 2>&1; then - if [ "$YES" = false ] && [ -t 0 ]; then - printf 'Install missing UI tools with Homebrew now? [y/N] ' - read -r reply - case "$reply" in - y|Y|yes|YES) - brew install "${missing_items[@]}" - ;; - *) - info "install with: brew install $missing_text" - return 0 - ;; - esac - else - info "install with: brew install $missing_text" - return 0 +print_dependency_status() { + local tool + for tool in git glow gum fzf codex claude; do + if command -v "$tool" >/dev/null 2>&1; then + info "[ok] $tool: $(command -v "$tool")" fi - else - info "install with: brew install $missing_text" + done +} + +report_required_dependencies() { + local formula_missing_text="$1" + local cask_missing_text="$2" + + if [ -z "$formula_missing_text" ] && [ -z "$cask_missing_text" ]; then + print_dependency_status return 0 fi - missing_text="$(missing_commands glow gum fzf)" - if [ -n "$missing_text" ]; then - info "still missing UI tools: $missing_text" - else - info "[ok] glow: $(command -v glow)" - info "[ok] gum: $(command -v gum)" - info "[ok] fzf: $(command -v fzf)" + if [ -n "$formula_missing_text" ]; then + info "missing required dependencies: $formula_missing_text" + info "install with: brew install $formula_missing_text" + fi + if [ -n "$cask_missing_text" ]; then + info "missing required cask dependencies: $cask_missing_text" + info "install with: brew install --cask $cask_missing_text" fi } -check_agent_tools() { - local agent_missing_text="$1" - if [ -z "$agent_missing_text" ]; then - info "[ok] codex: $(command -v codex)" - info "[ok] claude: $(command -v claude)" +confirm_dependency_install() { + local formula_missing_text="$1" + local cask_missing_text="$2" + local reply + + if [ "$YES" = true ]; then return 0 fi + report_required_dependencies "$formula_missing_text" "$cask_missing_text" + if [ ! -t 0 ]; then + info "pass --yes to install missing dependencies without a prompt." + return 1 + fi - info "missing agent CLIs: $agent_missing_text" - info "Devloop does not install codex or claude automatically." + printf 'Install missing dependencies with Homebrew now? [y/N] ' + read -r reply + case "$reply" in + y|Y|yes|YES) return 0 ;; + *) + info "skipping dependency install" + return 1 + ;; + esac +} + +install_required_dependencies() { + local formula_missing_text="$1" + local cask_missing_text="$2" + local missing_formulas=() + local missing_casks=() + local still_missing + + if [ -z "$formula_missing_text" ] && [ -z "$cask_missing_text" ]; then + print_dependency_status + return 0 + fi + + if [ -n "$formula_missing_text" ]; then + read -r -a missing_formulas <<< "$formula_missing_text" + fi + if [ -n "$cask_missing_text" ]; then + read -r -a missing_casks <<< "$cask_missing_text" + fi + + if ! command -v brew >/dev/null 2>&1; then + report_required_dependencies "$formula_missing_text" "$cask_missing_text" + info "install Homebrew, then rerun the installer." + return 1 + fi + if ! confirm_dependency_install "$formula_missing_text" "$cask_missing_text"; then + return 1 + fi + + if [ "${#missing_formulas[@]}" -gt 0 ]; then + info "installing required dependencies: ${missing_formulas[*]}" + brew install "${missing_formulas[@]}" + fi + if [ "${#missing_casks[@]}" -gt 0 ]; then + info "installing required cask dependencies: ${missing_casks[*]}" + brew install --cask "${missing_casks[@]}" + fi + + still_missing="$(missing_commands git glow gum fzf codex claude)" + if [ -n "$still_missing" ]; then + info "still missing required dependencies: $still_missing" + return 1 + fi + print_dependency_status } print_path_guidance() { @@ -347,24 +403,24 @@ dry_run() { } main() { - local version ui_missing agent_missing tmp archive checksum installed_root + local version formula_missing cask_missing tmp archive checksum installed_root dependency_status parse_args "$@" if [ -z "$VERSION" ]; then VERSION="$(resolve_latest_version)" fi version="$(normalize_version "$VERSION")" - ui_missing="$(missing_commands glow gum fzf)" - agent_missing="$(missing_commands codex claude)" + formula_missing="$(missing_commands git glow gum fzf)" + cask_missing="$(missing_agent_casks)" if [ "$DRY_RUN" = true ]; then dry_run "$version" - check_ui_tools "$ui_missing" - check_agent_tools "$agent_missing" + report_required_dependencies "$formula_missing" "$cask_missing" print_path_guidance return 0 fi + dependency_status=0 tmp="$(mktemp -d "${TMPDIR:-/tmp}/devloop-download.XXXXXX")" archive="$tmp/$(asset_name "$version")" checksum="$archive.sha256" @@ -376,10 +432,10 @@ main() { installed_root="$INSTALL_ROOT/$version" install_skills "$installed_root" - check_ui_tools "$ui_missing" - check_agent_tools "$agent_missing" + install_required_dependencies "$formula_missing" "$cask_missing" || dependency_status=$? print_path_guidance print_banner "$version" + return "$dependency_status" } main "$@" diff --git a/scripts/install.sh b/scripts/install.sh index 6b3214b..9fb58fa 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -46,33 +46,55 @@ EOF printf ' %stry:%s %s%sdevloop%s\n\n' "$C_DIM" "$C_RESET" "$C_BOLD" "$C_ACCENT" "$C_RESET" } -install_required_ui_tools() { - local missing=() +install_required_dependencies() { + local missing_formulas=() + local missing_casks=() + local missing_text local tool - for tool in glow gum fzf; do + for tool in git glow gum fzf; do if ! command -v "$tool" >/dev/null 2>&1; then - missing+=("$tool") + missing_formulas+=("$tool") fi done + if ! command -v codex >/dev/null 2>&1; then + missing_casks+=(codex) + fi + if ! command -v claude >/dev/null 2>&1; then + missing_casks+=(claude-code) + fi - if [ "${#missing[@]}" -eq 0 ]; then - echo "required UI tools ready" + if [ "${#missing_formulas[@]}" -eq 0 ] && [ "${#missing_casks[@]}" -eq 0 ]; then + echo "required dependencies ready" return 0 fi if ! command -v brew >/dev/null 2>&1; then - echo "missing required UI tools: ${missing[*]}" >&2 + missing_text="${missing_formulas[*]}" + if [ "${#missing_casks[@]}" -gt 0 ]; then + if [ -n "$missing_text" ]; then + missing_text="$missing_text ${missing_casks[*]}" + else + missing_text="${missing_casks[*]}" + fi + fi + echo "missing required dependencies: $missing_text" >&2 echo "install Homebrew, then rerun ./scripts/install.sh" >&2 return 1 fi - echo "installing required UI tools: ${missing[*]}" - brew install "${missing[@]}" + if [ "${#missing_formulas[@]}" -gt 0 ]; then + echo "installing required dependencies: ${missing_formulas[*]}" + brew install "${missing_formulas[@]}" + fi + if [ "${#missing_casks[@]}" -gt 0 ]; then + echo "installing required cask dependencies: ${missing_casks[*]}" + brew install --cask "${missing_casks[@]}" + fi - for tool in "${missing[@]}"; do + for tool in git glow gum fzf codex claude; do if ! command -v "$tool" >/dev/null 2>&1; then - echo "failed to install required UI tool: $tool" >&2 + echo "failed to install required dependency: $tool" >&2 return 1 fi done @@ -88,7 +110,7 @@ chmod +x "$SOURCE" ln -sfn "$SOURCE" "$TARGET" echo "installed devloop -> $SOURCE" -install_required_ui_tools || TOOL_STATUS=$? +install_required_dependencies || TOOL_STATUS=$? devloop_install_skills "$ROOT" || SKILL_STATUS=$? echo "optional for PR-backed loops: install GitHub CLI and run gh auth login" diff --git a/site/public/install b/site/public/install index 1cfcc0a..4fcec97 100644 --- a/site/public/install +++ b/site/public/install @@ -73,7 +73,7 @@ main() { version="$(site_version)" fi - args=(--version "$version") + args=(--yes --version "$version") while [ "$#" -gt 0 ]; do case "$1" in --version)