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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ cd devloop
./scripts/install.sh
```

> Requires Bash, git, `codex`, `claude`, `gum`, and `fzf`. Run `devloop doctor` to check.
> Requires Bash, git, `codex`, `claude`, `glow`, `gum`, and `fzf`. Run `devloop doctor` to check.

Uninstall with `./scripts/uninstall.sh` (`--dry-run` to preview).

Expand Down
25 changes: 24 additions & 1 deletion devloop
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,10 @@ ui_has_fzf() {
[ "$USE_TUI" = true ] && command -v fzf >/dev/null 2>&1
}

ui_has_glow() {
[ "$USE_TUI" = true ] && command -v glow >/dev/null 2>&1
}

ui_color_code() {
local color="$1"
case "$color" in
Expand Down Expand Up @@ -1109,15 +1113,18 @@ ui_numbered_pick() {
ui_pick_from_file() {
local file="$1"
local header="$2"
local preview_cmd
if [ ! -s "$file" ]; then return 1; fi
if ui_has_fzf; then
# shellcheck disable=SC2016
preview_cmd='case {} in *.md) if command -v glow >/dev/null 2>&1; then glow -w "$FZF_PREVIEW_COLUMNS" {}; else sed -n "1,80p" {} 2>/dev/null; fi ;; *) sed -n "1,80p" {} 2>/dev/null ;; esac'
fzf \
--height 70% \
--layout reverse \
--border \
--color "fg:-1,bg:-1,fg+:${UI_OK_COLOR},bg+:-1,hl:${UI_REC_COLOR},hl+:${UI_REC_COLOR},prompt:${UI_DIM_COLOR},pointer:${UI_REC_COLOR},marker:${UI_OK_COLOR},spinner:${UI_REC_COLOR},info:${UI_DIM_COLOR},border:${UI_BORDER_COLOR},header:${UI_ACCENT_COLOR},gutter:-1" \
--prompt "$header > " \
--preview 'sed -n "1,80p" {} 2>/dev/null' < "$file"
--preview "$preview_cmd" < "$file"
return $?
fi
if ui_has_gum; then
Expand Down Expand Up @@ -1783,6 +1790,22 @@ list_artifact_files() {

view_file() {
local file="$1"
case "$file" in
*.md)
if ui_has_glow; then
if ui_has_gum; then
glow "$file" | gum pager
return $?
fi
if [ "$USE_TUI" = true ] && [ -t 0 ]; then
glow "$file" | "${PAGER:-less}"
return $?
fi
glow "$file"
return $?
fi
;;
esac
if ui_has_gum; then
gum pager < "$file"
return $?
Expand Down
175 changes: 169 additions & 6 deletions scripts/devloop_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,149 @@ equals "$(ui_pick_from_file "$picker_file" "Pick")" "alpha" "non-tui picker fall
equals "$(USE_TUI=true; ui_numbered_pick "$picker_file" "Pick" 2>/dev/null <<<"2")" "beta" "numbered picker"
view_file "$picker_file" >/dev/null
USE_TUI="$old_use_tui"

preview_bin="$work/preview-bin"
preview_log="$work/preview.log"
mkdir -p "$preview_bin"
cat > "$preview_bin/glow" <<'GLOW'
#!/usr/bin/env bash
{
printf 'glow'
for arg in "$@"; do printf ' <%s>' "$arg"; done
printf '\n'
} >> "$DEVLOOP_PREVIEW_LOG"
printf '%s\n' "rendered"
GLOW
cat > "$preview_bin/sed" <<'SED'
#!/usr/bin/env bash
{
printf 'sed'
for arg in "$@"; do printf ' <%s>' "$arg"; done
printf '\n'
} >> "$DEVLOOP_PREVIEW_LOG"
printf '%s\n' "raw"
SED
chmod +x "$preview_bin/glow" "$preview_bin/sed"
preview_md="$work/preview.md"
preview_log_file="$work/preview-file.log"
preview_list="$work/preview-list.txt"
printf '%s\n' "# Preview" > "$preview_md"
printf '%s\n' "plain log" > "$preview_log_file"
printf '%s\n' "$preview_md" > "$preview_list"
: > "$preview_log"
DEVLOOP_PREVIEW_LOG="$preview_log"
export DEVLOOP_PREVIEW_LOG
old_use_tui="$USE_TUI"
USE_TUI=true
(
ui_has_fzf() { return 0; }
fzf() {
local preview="" arg selection quoted_selection expanded
while [ "$#" -gt 0 ]; do
arg="$1"
shift
if [ "$arg" = "--preview" ]; then
preview="$1"
shift
fi
done
IFS= read -r selection || return 1
quoted_selection="'$selection'"
expanded="${preview//\{\}/$quoted_selection}"
FZF_PREVIEW_COLUMNS=77 PATH="$preview_bin:/usr/bin:/bin:/usr/sbin:/sbin" sh -c "$expanded" >/dev/null
printf '%s\n' "$selection"
}
ui_pick_from_file "$preview_list" "Pick" >/dev/null
)
contains "$(cat "$preview_log")" "glow <-w> <77> <$preview_md>" "fzf markdown preview"
not_contains "$(cat "$preview_log")" "sed" "fzf markdown preview"
printf '%s\n' "$preview_log_file" > "$preview_list"
: > "$preview_log"
(
ui_has_fzf() { return 0; }
fzf() {
local preview="" arg selection quoted_selection expanded
while [ "$#" -gt 0 ]; do
arg="$1"
shift
if [ "$arg" = "--preview" ]; then
preview="$1"
shift
fi
done
IFS= read -r selection || return 1
quoted_selection="'$selection'"
expanded="${preview//\{\}/$quoted_selection}"
FZF_PREVIEW_COLUMNS=77 PATH="$preview_bin:/usr/bin:/bin:/usr/sbin:/sbin" sh -c "$expanded" >/dev/null
printf '%s\n' "$selection"
}
ui_pick_from_file "$preview_list" "Pick" >/dev/null
)
not_contains "$(cat "$preview_log")" "glow" "fzf non-markdown preview"
contains "$(cat "$preview_log")" "sed" "fzf non-markdown preview"
unset DEVLOOP_PREVIEW_LOG

view_bin="$work/view-bin"
view_log="$work/view.log"
mkdir -p "$view_bin"
cat > "$view_bin/glow" <<'GLOW'
#!/usr/bin/env bash
{
printf 'glow'
for arg in "$@"; do printf ' <%s>' "$arg"; done
printf '\n'
} >> "$DEVLOOP_VIEW_LOG"
printf '%s\n' "rendered markdown"
GLOW
cat > "$view_bin/gum" <<'GUM'
#!/usr/bin/env bash
{
printf 'gum'
for arg in "$@"; do printf ' <%s>' "$arg"; done
printf '\n'
} >> "$DEVLOOP_VIEW_LOG"
cat >/dev/null
GUM
chmod +x "$view_bin/glow" "$view_bin/gum"
view_md="$work/view.md"
view_log_file="$work/view.logfile"
printf '%s\n' "# View" > "$view_md"
printf '%s\n' "raw log" > "$view_log_file"
old_path="$PATH"
old_use_tui="$USE_TUI"
DEVLOOP_VIEW_LOG="$view_log"
export DEVLOOP_VIEW_LOG
PATH="$view_bin:/usr/bin:/bin:/usr/sbin:/sbin"
USE_TUI=true
ui_has_gum() { [ "$USE_TUI" = true ] && command -v gum >/dev/null 2>&1; }
: > "$view_log"
view_file "$view_md" >/dev/null
contains "$(cat "$view_log")" "glow <$view_md>" "markdown view uses glow"
contains "$(cat "$view_log")" "gum <pager>" "markdown view uses pager"
rm -f "$view_bin/glow"
: > "$view_log"
view_file "$view_md" >/dev/null
not_contains "$(cat "$view_log")" "glow" "markdown view absent glow"
contains "$(cat "$view_log")" "gum <pager>" "markdown view absent glow fallback"
cat > "$view_bin/glow" <<'GLOW'
#!/usr/bin/env bash
{
printf 'glow'
for arg in "$@"; do printf ' <%s>' "$arg"; done
printf '\n'
} >> "$DEVLOOP_VIEW_LOG"
printf '%s\n' "rendered markdown"
GLOW
chmod +x "$view_bin/glow"
USE_TUI=false
: > "$view_log"
equals "$(view_file "$view_log_file")" "raw log" "non-markdown raw view"
not_contains "$(cat "$view_log")" "glow" "non-markdown view skips glow"
PATH="$old_path"
USE_TUI="$old_use_tui"
ui_has_gum() { return 1; }
unset DEVLOOP_VIEW_LOG

equals "$(title_from_slug "chat-retry")" "Chat Retry" "title from slug"
RUN_TIMEOUT_MINUTES=7
contains "$(timeout_message)" "7 minutes" "timeout message"
Expand Down Expand Up @@ -875,8 +1018,8 @@ 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: gum fzf" "remote missing UI guidance"
contains "$remote_dry_output" "install with: brew install gum fzf" "remote missing UI guidance"
contains "$remote_dry_output" "missing UI tools: 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"
[[ ! -e "$remote_custom_root" ]] || fail "remote dry run created install root"
Expand Down Expand Up @@ -927,7 +1070,7 @@ ok "remote installer rejects checksum mismatch"

remote_tool_bin="$work/remote-tool-bin"
mkdir -p "$remote_tool_bin"
for tool in gum fzf codex claude; do
for tool in glow gum fzf codex claude; do
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$remote_tool_bin/$tool"
chmod +x "$remote_tool_bin/$tool"
done
Expand Down Expand Up @@ -1091,6 +1234,7 @@ equals "$("$remote_default_bin/devloop" --version)" "devloop $remote_version" "r
contains "$remote_install_output" "verified checksum" "remote install checksum"
contains "$remote_install_output" "$remote_default_bin is not on PATH" "remote install PATH guidance"
contains "$remote_install_output" "export PATH=\"$remote_default_bin:\$PATH\"" "remote install PATH guidance"
contains "$remote_install_output" "[ok] glow:" "remote install UI check"
contains "$remote_install_output" "[ok] gum:" "remote install UI check"
contains "$remote_install_output" "[ok] codex:" "remote install agent check"
contains "$remote_install_output" "devloop $remote_version installed" "remote install banner version"
Expand Down Expand Up @@ -1181,7 +1325,7 @@ shift
tool_dir="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"
for formula in "$@"; do
case "$formula" in
gum|fzf)
glow|gum|fzf)
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/$formula"
chmod +x "$tool_dir/$formula"
;;
Expand All @@ -1195,6 +1339,7 @@ DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$SCRIPTS_D
[[ -x "$REPO_ROOT/devloop" ]] || fail "devloop is not executable"
[[ -L "$bin_dir/devloop" ]] || fail "installer did not create symlink"
contains "$(cat /tmp/devloop-install-test.out)" "gh auth login" "installer optional gh auth"
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"
[[ -f "$install_home/.agents/skills/devloop-spec/SKILL.md" ]] || fail "installer did not install Codex spec skill"
Expand Down Expand Up @@ -1225,6 +1370,7 @@ fake_bin="$work/fake-bin"
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"
printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/glow"
cat > "$fake_bin/gh" <<'GH'
#!/usr/bin/env bash
set -euo pipefail
Expand Down Expand Up @@ -1340,7 +1486,7 @@ case "${1:-}" in
esac
GH
chmod +x "$fake_bin/gh"
chmod +x "$fake_bin/codex" "$fake_bin/claude"
chmod +x "$fake_bin/codex" "$fake_bin/claude" "$fake_bin/glow"

backlink_repo="$work/spec-backlink-repo"
git init -q "$backlink_repo"
Expand Down Expand Up @@ -1386,6 +1532,7 @@ 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] glow:" "doctor"
contains "$doctor_output" "[ok] skill devloop-spec" "doctor"
contains "$doctor_output" "[ok] gum:" "doctor"
contains "$doctor_output" "[ok] fzf:" "doctor"
Expand All @@ -1403,7 +1550,8 @@ no_gh_bin="$work/no-gh-bin"
mkdir -p "$no_gh_bin"
printf '#!/usr/bin/env bash\nexit 0\n' > "$no_gh_bin/codex"
printf '#!/usr/bin/env bash\nexit 0\n' > "$no_gh_bin/claude"
chmod +x "$no_gh_bin/codex" "$no_gh_bin/claude"
printf '#!/usr/bin/env bash\nexit 0\n' > "$no_gh_bin/glow"
chmod +x "$no_gh_bin/codex" "$no_gh_bin/claude" "$no_gh_bin/glow"
# Mirror the system bin dirs without gh so `command -v gh` fails regardless of
# where gh is installed on the host (CI runners ship gh in /usr/bin).
sys_clean="$work/sys-clean"
Expand All @@ -1423,6 +1571,21 @@ contains "$doctor_no_gh_output" "[FAIL] gh installed" "doctor no gh"
contains "$doctor_no_gh_output" "PR-backed loop readiness unavailable" "doctor no gh"
ok "doctor optional GitHub readiness"

no_glow_bin="$work/no-glow-bin"
mkdir -p "$no_glow_bin"
printf '#!/usr/bin/env bash\nexit 0\n' > "$no_glow_bin/codex"
printf '#!/usr/bin/env bash\nexit 0\n' > "$no_glow_bin/claude"
printf '#!/usr/bin/env bash\nexit 0\n' > "$no_glow_bin/gum"
printf '#!/usr/bin/env bash\nexit 0\n' > "$no_glow_bin/fzf"
chmod +x "$no_glow_bin/codex" "$no_glow_bin/claude" "$no_glow_bin/gum" "$no_glow_bin/fzf"
if doctor_no_glow_output="$(HOME="$install_home" PATH="$bin_dir:$no_glow_bin:$sys_clean" "$bin_dir/devloop" doctor 2>&1)"; then
printf '%s\n' "$doctor_no_glow_output" >&2
fail "doctor passed when glow was unavailable"
fi
contains "$doctor_no_glow_output" "[fail] missing command: glow" "doctor no glow"
contains "$doctor_no_glow_output" "devloop doctor: not ready" "doctor no glow"
ok "doctor requires glow"

agent="$work/spec-agent"
cat > "$agent" <<'AGENT'
#!/usr/bin/env bash
Expand Down
6 changes: 4 additions & 2 deletions scripts/install.remote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ check_ui_tools() {
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)"
return 0
Expand Down Expand Up @@ -234,10 +235,11 @@ check_ui_tools() {
return 0
fi

missing_text="$(missing_commands gum fzf)"
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)"
fi
Expand Down Expand Up @@ -352,7 +354,7 @@ main() {
VERSION="$(resolve_latest_version)"
fi
version="$(normalize_version "$VERSION")"
ui_missing="$(missing_commands gum fzf)"
ui_missing="$(missing_commands glow gum fzf)"
agent_missing="$(missing_commands codex claude)"

if [ "$DRY_RUN" = true ]; then
Expand Down
2 changes: 1 addition & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ install_required_ui_tools() {
local missing=()
local tool

for tool in gum fzf; do
for tool in glow gum fzf; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing+=("$tool")
fi
Expand Down
1 change: 1 addition & 0 deletions scripts/skill_helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ devloop_doctor() {
devloop_doctor_command git || status=1
devloop_doctor_command codex || status=1
devloop_doctor_command claude || status=1
devloop_doctor_command glow || status=1
devloop_doctor_command gum || status=1
devloop_doctor_command fzf || status=1
printf '\nSkills\n'
Expand Down
Loading