Files
Michael Sitarzewski f541d07bb3 feat: Installer v2 — selective install, interactive TUI, consolidate the install.sh cluster (#567)
* feat: installer v2 — selective install, interactive TUI, consolidate cluster

One coherent, dependency-free installer (bash 3.2+, zero deps) that
consolidates 7 conflicting install.sh PRs and fixes #532.

Selective install (compose freely; empty = everything):
- --division / --agent / --agents-file filter across both source tools and
  the flat converted outputs via a slug-based allow-set (#157, #487)
- --list [tools|teams|agents] and --dry-run

Install mechanics:
- --link symlink vs copy (#233); --path + env-var fallbacks (#216);
  auto-run convert.sh when integration files are missing (#426);
  resolve_tool_path dynamic detection (#327); set -e-safe increments (#505)

Interactive wizard (pure bash):
- Tools -> Teams -> Review, arrow-key nav, space toggle, a/n all/none,
  live / search, live agent counts, inline OpenCode capacity warning,
  alt-screen takeover with trap-based Ctrl-C restore, non-TTY fallback

#532: installing a subset keeps you under OpenCode's ~119 scanner cap
(upstream anomalyco/opencode#27988); installer warns when exceeded; README
documents it.

New scripts/lib.sh holds shared frontmatter/slug helpers (used by
convert.sh too) + ANSI/TUI primitives.

Closes #157, #216, #233, #327, #426, #487, #505.

Co-Authored-By: kienbui1995 <kienbui1995@users.noreply.github.com>
Co-Authored-By: Shiven0504 <Shiven0504@users.noreply.github.com>
Co-Authored-By: rounakkumarsingh <rounakkumarsingh@users.noreply.github.com>
Co-Authored-By: toukanno <toukanno@users.noreply.github.com>
Co-Authored-By: ilyaivasyk <ilyaivasyk@users.noreply.github.com>
Co-Authored-By: Jason2031 <Jason2031@users.noreply.github.com>
Co-Authored-By: ShaoJiaZhen <ShaoJiaZhen@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(installer): robust arrow-key reading (bash 3.2 integer timeouts + SS3)

read_key used a fractional -t 0.01 timeout, which bash 3.2 (/bin/bash on
macOS) doesn't support — so arrow-key escape bytes ([A/[B) leaked through
and were parsed as letter commands (toggling instead of moving). Rewrite
to read the sequence byte-by-byte with integer timeouts and handle both
CSI ([) and SS3 (O) cursor modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(installer): clear-to-end-of-line per row so frames don't bleed

draw_frame only cleared below the frame (\033[0J), so when a new screen's
lines were shorter than the previous screen's, the old tails (tool paths,
warnings) bled through on the right. Now erase-to-eol (\033[K) on every
line before the screen-clear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(installer): 2-column grid for Tools/Teams on the Review screen

Replaces the wrapping space-joined 'Tools:'/'Teams:' lines with a compact
column-major 2-column grid (each item on its own line, like the selectors),
so long rosters stay readable and on-screen instead of wrapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(installer): Review layout — space after Teams, warning below Install

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(installer): consistent screen layout across all 3 screens

Standard vertical rhythm everywhere: pager -> description -> content ->
selection summary -> navigation -> warnings. Splits the selector footer
into separate summary/nav/warning lines (SEL_SUMMARY_FN/SEL_NAV/
SEL_WARN_FN) and reorders the Review screen to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: kienbui1995 <kienbui1995@users.noreply.github.com>
Co-authored-by: Shiven0504 <Shiven0504@users.noreply.github.com>
Co-authored-by: rounakkumarsingh <rounakkumarsingh@users.noreply.github.com>
Co-authored-by: toukanno <toukanno@users.noreply.github.com>
Co-authored-by: ilyaivasyk <ilyaivasyk@users.noreply.github.com>
Co-authored-by: Jason2031 <Jason2031@users.noreply.github.com>
Co-authored-by: ShaoJiaZhen <ShaoJiaZhen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:07:10 -05:00

164 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# lib.sh — shared pure-bash helpers for scripts/convert.sh and scripts/install.sh.
#
# No external dependencies. Bash 3.2+ compatible (macOS ships 3.2).
# Sourced, not executed. Groups:
# 1. Frontmatter / slug helpers (agent data model)
# 2. set -e-safe primitives
# 3. Terminal capability + ANSI (color, unicode, sizing)
# 4. TUI primitives (raw input, alt-screen, flicker-free draw)
#
# Everything here is namespaced loosely and guarded so it is safe to source
# from a script already running under `set -euo pipefail`.
# ---------------------------------------------------------------------------
# 1. Frontmatter / slug helpers
# ---------------------------------------------------------------------------
# get_field <field> <file> — value of a YAML frontmatter field (first match).
get_field() {
local field="$1" file="$2"
awk -v f="$field" '
/^---$/ { fm++; next }
fm == 1 && $0 ~ "^" f ": " { sub("^" f ": ", ""); print; exit }
' "$file"
}
# get_body <file> — file contents with the leading frontmatter block stripped.
get_body() {
awk 'BEGIN{fm=0} /^---$/{fm++; next} fm>=2{print}' "$1"
}
# slugify <string> — "Frontend Developer" -> "frontend-developer"
slugify() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//'
}
# agent_slug <file> — slug derived from the file's `name:` frontmatter.
# Single source of truth so convert + install always agree.
agent_slug() {
local name; name="$(get_field name "$1")"
[[ -n "$name" ]] && slugify "$name"
}
# is_agent_file <file> — true if the file starts with a YAML frontmatter fence.
is_agent_file() {
[[ -f "$1" ]] && [[ "$(head -1 "$1")" == "---" ]]
}
# ---------------------------------------------------------------------------
# 2. set -e-safe primitives (absorbs #505 — no more `(( x++ )) || true`)
# ---------------------------------------------------------------------------
# incr <varname> — increment a numeric variable in place, safely under set -e.
incr() { printf -v "$1" '%d' "$(( ${!1:-0} + 1 ))"; }
# ---------------------------------------------------------------------------
# 3. Terminal capability + ANSI
# ---------------------------------------------------------------------------
supports_color() { [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; }
supports_unicode() { [[ "${LANG:-}${LC_ALL:-}${LC_CTYPE:-}" == *[Uu][Tt][Ff]* ]]; }
term_cols() { local c; c="$(tput cols 2>/dev/null)"; [[ "$c" =~ ^[0-9]+$ ]] && echo "$c" || echo 80; }
term_rows() { local r; r="$(tput lines 2>/dev/null)"; [[ "$r" =~ ^[0-9]+$ ]] && echo "$r" || echo 24; }
# init_ansi — populate C_* color vars + box-drawing chars (UTF-8 or ASCII).
init_ansi() {
if supports_color; then
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_REV=$'\033[7m'
C_RED=$'\033[0;31m'; C_GREEN=$'\033[0;32m'; C_YELLOW=$'\033[1;33m'
C_BLUE=$'\033[0;34m'; C_CYAN=$'\033[0;36m'; C_MAGENTA=$'\033[0;35m'
else
C_RESET=''; C_BOLD=''; C_DIM=''; C_REV=''
C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''; C_CYAN=''; C_MAGENTA=''
fi
if supports_unicode; then
BX_TL='╭'; BX_TR='╮'; BX_BL='╰'; BX_BR='╯'; BX_H='─'; BX_V='│'
GLYPH_ON='✓'; GLYPH_DET='●'; GLYPH_OFF='○'; GLYPH_CUR='▸'
else
BX_TL='+'; BX_TR='+'; BX_BL='+'; BX_BR='+'; BX_H='-'; BX_V='|'
GLYPH_ON='x'; GLYPH_DET='*'; GLYPH_OFF=' '; GLYPH_CUR='>'
fi
}
# repeat <char> <n> — print <char> n times.
repeat() { local i; for (( i=0; i<$2; i++ )); do printf '%s' "$1"; done; }
# strip_ansi <string> — remove ANSI escape sequences (for width math).
strip_ansi() { printf '%s' "$1" | sed $'s/\033\\[[0-9;]*m//g'; }
# vis_len <string> — visible length (ANSI-stripped). Note: assumes 1 col/char.
vis_len() { local s; s="$(strip_ansi "$1")"; printf '%s' "${#s}"; }
# ---------------------------------------------------------------------------
# 4. TUI primitives (used by install.sh's interactive wizard)
# ---------------------------------------------------------------------------
_TUI_ACTIVE=0
_TUI_STTY_SAVE=""
# tui_begin — enter alt screen, hide cursor, raw mode; install restore trap.
tui_begin() {
# Test hook: drive the wizard from piped keystrokes (skips the TTY gate and
# the alt-screen/stty takeover). Used by the install-script test harness.
[[ -n "${AGENCY_TUI_FORCE:-}" ]] && { _TUI_ACTIVE=1; return 0; }
[[ -t 0 && -t 1 ]] || return 1
_TUI_STTY_SAVE="$(stty -g 2>/dev/null)" || return 1
stty -echo -icanon time 0 min 1 2>/dev/null || return 1
printf '\033[?1049h\033[?25l' # alt screen + hide cursor
_TUI_ACTIVE=1
trap 'tui_end' EXIT INT TERM
}
# tui_end — restore terminal (idempotent; safe from trap).
tui_end() {
[[ "$_TUI_ACTIVE" == "1" ]] || return 0
printf '\033[?25h\033[?1049l' # show cursor + leave alt screen
[[ -n "$_TUI_STTY_SAVE" ]] && stty "$_TUI_STTY_SAVE" 2>/dev/null
_TUI_ACTIVE=0
trap - EXIT INT TERM
}
# read_key — read one keypress, echo a normalized token:
# UP DOWN LEFT RIGHT ENTER SPACE ESC BACKSPACE TAB or the literal character.
#
# Reads escape sequences byte-by-byte with INTEGER timeouts (bash 3.2 has no
# fractional -t). A real arrow sends ESC [ A (or ESC O A in application-cursor
# mode) as one buffered burst, so the follow-up reads return instantly; only a
# lone Esc waits out the 1s timeout. Handles both CSI ('[') and SS3 ('O').
read_key() {
local k k2 k3
IFS= read -rsn1 k 2>/dev/null || { printf 'EOF'; return; }
case "$k" in
$'\033')
if ! IFS= read -rsn1 -t 1 k2 2>/dev/null; then printf 'ESC'; return; fi
if [[ "$k2" == '[' || "$k2" == 'O' ]]; then
IFS= read -rsn1 -t 1 k3 2>/dev/null
case "$k3" in
A) printf 'UP' ;; B) printf 'DOWN' ;;
C) printf 'RIGHT' ;; D) printf 'LEFT' ;;
*) printf 'ESC' ;;
esac
else
printf 'ESC'
fi ;;
$'\n'|$'\r'|'') printf 'ENTER' ;; # Enter is CR in raw mode (sometimes empty)
' ') printf 'SPACE' ;;
$'\t') printf 'TAB' ;;
$'\177'|$'\010') printf 'BACKSPACE' ;;
*) printf '%s' "$k" ;;
esac
}
# draw_frame <buffer> — home cursor and paint a pre-composed frame.
# Flicker-free: erase-to-end-of-line (\033[K) on every line so a shorter new
# line never leaves the previous frame's tail behind, then erase-to-end-of-
# screen (\033[0J) to drop any leftover lines below the frame.
draw_frame() {
local buf="${1//$'\n'/$'\033[K'$'\n'}"
printf '\033[H%s\033[K\033[0J' "$buf"
}