mirror of
https://github.com/msitarzewski/agency-agents.git
synced 2026-10-01 13:55:54 +03:00
strategy/runbooks.json names agents by file stem, the corpus id the desktop
app maps to catalog entries ("engineering-frontend-developer"). The installer
only accepted the slug built from `name:` ("frontend-developer"), and for
206 of the 279 agents those differ. Of the 36 agents the four runbooks list,
35 could not be selected by the ids the runbooks give them:
$ install.sh --tool claude-code --agent engineering-frontend-developer --dry-run
[ERR] Unknown agent 'engineering-frontend-developer'. Use --list agents ...
So there was no way to install a runbook's team from the CLI without
translating every id by hand.
resolve_agent now takes a slug, a display name (as before, via slugify), or a
file stem, and returns the install slug. Slugs are tried first. No stem
equals a different agent's slug today, and stems are unique, so nothing that
resolved before resolves differently now.
Resolution also stops rescanning the roster per request. agent_slug_exists
ran get_field over all 279 files for each requested agent; roster_index
builds the slug/stem table once in the parent shell. A 36-line agents file
went from ~10,000 get_field calls to 279.
test-agent-selection.sh gains a stem case and feeds each runbook's roster to
--agents-file, checking that it resolves to exactly its own agent count. It
fails on main at the first runbook. The usage text and the README selection
section say which ids are accepted, with an example of installing a
runbook's team.
1750 lines
71 KiB
Bash
Executable File
1750 lines
71 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# --- USAGE-START --- (sentinel for usage(); do not remove)
|
|
# install.sh -- Install The Agency agents into your local agentic tool(s).
|
|
#
|
|
# Reads converted files from integrations/ and copies them to the appropriate
|
|
# config directory for each tool. Run scripts/convert.sh first if integrations/
|
|
# is missing or stale.
|
|
#
|
|
# Usage:
|
|
# ./scripts/install.sh [selection] [mode] [behavior]
|
|
# Bare invocation installs all teams to detected tools (interactive when a TTY).
|
|
#
|
|
# Tools:
|
|
# claude-code -- Copy agents to ~/.claude/agents/
|
|
# copilot -- Copy agents to ~/.github/agents/ and ~/.copilot/agents/
|
|
# antigravity -- Copy skills to ~/.gemini/config/skills/
|
|
# gemini-cli -- Install agents to ~/.gemini/agents/
|
|
# opencode -- Copy agents to .opencode/agents/ in current directory
|
|
# cursor -- Copy rules to .cursor/rules/ in current directory
|
|
# aider -- Copy the CONVENTIONS.md roster index to current directory
|
|
# windsurf -- Copy .windsurfrules to current directory
|
|
# openclaw -- Copy workspaces to ~/.openclaw/agency-agents/
|
|
# qwen -- Copy SubAgents to ~/.qwen/agents/ (user-wide) or .qwen/agents/ (project)
|
|
# zcode -- Copy agents to ~/.zcode/agents/ (global) or .zcode/agents/ (project)
|
|
# codex -- Copy custom agent TOML files to ~/.codex/agents/
|
|
# osaurus -- Copy skills to ~/.osaurus/skills/
|
|
# hermes -- Copy lazy-router plugin to ~/.hermes/plugins/ and enable it
|
|
# vibe -- Copy agents and prompts to ~/.vibe/agents/ and ~/.vibe/prompts/
|
|
# dsh -- Copy skills to ~/.dsh/skills/ (user-wide) or .dsh/skills/ (project)
|
|
# all -- Install for all detected tools (default)
|
|
#
|
|
# Selection (compose freely; empty = everything):
|
|
# --tool <a,b> Only these tools
|
|
# --division <a,b> Only these teams/divisions (comma-separated)
|
|
# --agent <id,id> Only these specific agents (install slug, display name,
|
|
# or file stem such as engineering-frontend-developer)
|
|
# --agents-file <path> Agents listed in a file (one id per line, # comments ok)
|
|
#
|
|
# Mode:
|
|
# --link Symlink instead of copy (updates propagate)
|
|
# --path <dir> Override the install directory (single destination)
|
|
#
|
|
# Behavior:
|
|
# --interactive Show the interactive wizard (default when run in a terminal)
|
|
# --no-interactive Skip the wizard, install all detected tools
|
|
# --no-convert Don't auto-run convert.sh when integration files are missing
|
|
# --dry-run Print the plan and exit without writing anything
|
|
# --list [tools|teams|agents] List and exit
|
|
# --parallel Install tools in parallel (output buffered per tool)
|
|
# --jobs N Max parallel jobs (default: nproc or 4)
|
|
# --help Show this help
|
|
#
|
|
# Env: CLAUDE_CONFIG_DIR, COPILOT_AGENT_DIR, CURSOR_RULES_DIR, GEMINI_AGENTS_DIR,
|
|
# OPENCODE_AGENTS_DIR, OPENCLAW_DIR, QWEN_AGENTS_DIR, ZCODE_AGENTS_DIR,
|
|
# CODEX_AGENTS_DIR, OSAURUS_SKILLS_DIR, HERMES_HOME, HERMES_PLUGIN_DIR,
|
|
# VIBE_HOME, DSH_HOME, DSH_SKILLS_DIR
|
|
# override default install paths (checked before hardcoded defaults).
|
|
#
|
|
# --- USAGE-END --- (sentinel for usage(); do not remove)
|
|
# Platform support:
|
|
# Linux, macOS (requires bash 3.2+), Windows Git Bash / WSL
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Colours -- only when stdout supports color
|
|
# ---------------------------------------------------------------------------
|
|
if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then
|
|
C_GREEN=$'\033[0;32m'
|
|
C_YELLOW=$'\033[1;33m'
|
|
C_RED=$'\033[0;31m'
|
|
C_CYAN=$'\033[0;36m'
|
|
C_BOLD=$'\033[1m'
|
|
C_DIM=$'\033[2m'
|
|
C_RESET=$'\033[0m'
|
|
else
|
|
C_GREEN=''; C_YELLOW=''; C_RED=''; C_CYAN=''; C_BOLD=''; C_DIM=''; C_RESET=''
|
|
fi
|
|
|
|
ok() { printf "${C_GREEN}[OK]${C_RESET} %s\n" "$*"; }
|
|
warn() { printf "${C_YELLOW}[!!]${C_RESET} %s\n" "$*"; }
|
|
err() { printf "${C_RED}[ERR]${C_RESET} %s\n" "$*" >&2; }
|
|
header() { printf "\n${C_BOLD}%s${C_RESET}\n" "$*"; }
|
|
dim() { printf "${C_DIM}%s${C_RESET}\n" "$*"; }
|
|
|
|
# Progress bar: [=======> ] 3/8 (tqdm-style)
|
|
progress_bar() {
|
|
local current="$1" total="$2" width="${3:-20}" i filled empty
|
|
(( total > 0 )) || return
|
|
filled=$(( width * current / total ))
|
|
empty=$(( width - filled ))
|
|
printf "\r ["
|
|
for (( i=0; i<filled; i++ )); do printf "="; done
|
|
if (( filled < width )); then printf ">"; (( empty-- )); fi
|
|
for (( i=0; i<empty; i++ )); do printf " "; done
|
|
printf "] %s/%s" "$current" "$total"
|
|
[[ -t 1 ]] || printf "\n"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Box drawing -- pure ASCII, fixed 52-char wide
|
|
# box_top / box_mid / box_bot -- structural lines
|
|
# box_row <text> -- content row, right-padded to fit
|
|
# ---------------------------------------------------------------------------
|
|
BOX_INNER=48 # chars between the two | walls
|
|
|
|
box_top() { printf " +"; printf '%0.s-' $(seq 1 $BOX_INNER); printf "+\n"; }
|
|
box_bot() { box_top; }
|
|
box_sep() { printf " |"; printf '%0.s-' $(seq 1 $BOX_INNER); printf "|\n"; }
|
|
strip_ansi() {
|
|
awk '{ gsub(/\033\[[0-9;]*m/, ""); print }' <<< "$1"
|
|
}
|
|
box_row() {
|
|
# Strip ANSI escapes when measuring visible length
|
|
local raw="$1"
|
|
local visible
|
|
visible="$(strip_ansi "$raw")"
|
|
local pad=$(( BOX_INNER - 2 - ${#visible} ))
|
|
if (( pad < 0 )); then pad=0; fi
|
|
printf " | %s%*s |\n" "$raw" "$pad" ''
|
|
}
|
|
box_blank() { printf " |%*s|\n" $BOX_INNER ''; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Paths
|
|
# ---------------------------------------------------------------------------
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
INTEGRATIONS="$REPO_ROOT/integrations"
|
|
|
|
# Shared helpers (get_field, agent_slug, slugify, incr, ANSI + TUI primitives)
|
|
# shellcheck source=lib.sh
|
|
. "$SCRIPT_DIR/lib.sh"
|
|
|
|
ALL_TOOLS=(claude-code copilot antigravity gemini-cli opencode openclaw cursor aider windsurf qwen zcode kimi codex osaurus hermes vibe dsh)
|
|
|
|
# The division set is derived from divisions.json (the single source of truth)
|
|
# so the installer can never drift from the catalog — a hardcoded copy silently
|
|
# dropped healthcare (#655/#668) and can't be seen by check-divisions.sh. Same
|
|
# no-jq awk/grep/sed parse as scripts/check-divisions.sh (macOS + Linux).
|
|
divisions_from_json() {
|
|
local json="$REPO_ROOT/divisions.json"
|
|
[[ -f "$json" ]] || { err "divisions.json not found at $json"; exit 1; }
|
|
awk '/"divisions"[[:space:]]*:[[:space:]]*\{/{f=1; next} f' "$json" \
|
|
| grep -oE '"[a-z0-9-]+"[[:space:]]*:[[:space:]]*\{' \
|
|
| sed -E 's/"([a-z0-9-]+)".*/\1/'
|
|
}
|
|
|
|
# Selectable divisions = exactly the divisions.json entries.
|
|
ALL_DIVISIONS=()
|
|
while IFS= read -r _div; do [[ -n "$_div" ]] && ALL_DIVISIONS+=("$_div"); done < <(divisions_from_json)
|
|
[[ ${#ALL_DIVISIONS[@]} -gt 0 ]] || { err "no divisions parsed from divisions.json"; exit 1; }
|
|
|
|
# Directories scanned for installable agents = the divisions plus strategy/.
|
|
# strategy/ holds frontmatter-less NEXUS docs (filtered out by is_agent_file at
|
|
# scan time), so it is scanned but selectable only via ALL_DIVISIONS above.
|
|
AGENT_DIRS=("${ALL_DIVISIONS[@]}" strategy)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Selection engine (team / agent / agents-file filtering)
|
|
# ---------------------------------------------------------------------------
|
|
FILTER_DIVISIONS=() # --division
|
|
FILTER_AGENTS=() # --agent
|
|
AGENTS_FILE="" # --agents-file
|
|
DRY_RUN=false # --dry-run
|
|
SELECTION_ACTIVE=false # true once any agent-level filter is applied
|
|
_ALLOWED_SLUGS="" # newline-delimited cache of allowed slugs
|
|
_ROSTER_INDEX="" # "<install slug>\t<file stem>" per agent; see roster_index
|
|
|
|
# division_files <division> — agent file paths (frontmatter only) in a division.
|
|
division_files() {
|
|
local d="$REPO_ROOT/$1" f
|
|
[[ -d "$d" ]] || return 0
|
|
while IFS= read -r -d '' f; do
|
|
is_agent_file "$f" && printf '%s\n' "$f"
|
|
done < <(find "$d" -name "*.md" -type f -print0 2>/dev/null)
|
|
}
|
|
|
|
# division_count <division> — number of agents in a division.
|
|
division_count() { division_files "$1" | grep -c . ; }
|
|
|
|
# roster_index — fill _ROSTER_INDEX with one "<install slug>\t<file stem>" line
|
|
# per agent, once. Call it in the parent shell before resolve_agent: a $(...)
|
|
# caller would build its own copy and throw it away.
|
|
#
|
|
# Resolving each requested agent used to rescan the roster, running get_field
|
|
# on all 279 files per request, so a 36-agent runbook roster cost ~10,000
|
|
# get_field calls before anything installed.
|
|
roster_index() {
|
|
[[ -n "$_ROSTER_INDEX" ]] && return 0
|
|
local div f
|
|
for div in "${ALL_DIVISIONS[@]}"; do
|
|
while IFS= read -r f; do
|
|
_ROSTER_INDEX+="$(agent_slug "$f")"$'\t'"$(basename "$f" .md)"$'\n'
|
|
done < <(division_files "$div")
|
|
done
|
|
}
|
|
|
|
# resolve_agent <requested> — print the install slug for a requested agent,
|
|
# 1 if nothing matches. Selection filters should fail before installation when
|
|
# they name nothing that can be installed; otherwise dry-run counts and
|
|
# completion messages lie.
|
|
#
|
|
# Two spellings name an agent. The install slug comes from `name:` and is what
|
|
# --list agents prints. The file stem is the corpus id strategy/runbooks.json
|
|
# uses ("engineering-frontend-developer"), and for 206 of 279 agents it is not
|
|
# the slug, so 35 of the 36 agents the runbooks list could not be selected by
|
|
# the ids the runbooks give. Slugs are tried first; no stem equals another
|
|
# agent's slug today, and slug-first keeps it unambiguous if one ever does.
|
|
resolve_agent() {
|
|
local target="$1" slug stem
|
|
[[ -n "$target" ]] || return 1
|
|
while IFS=$'\t' read -r slug stem; do
|
|
[[ -n "$slug" && "$slug" == "$target" ]] && { printf '%s' "$slug"; return 0; }
|
|
done <<< "$_ROSTER_INDEX"
|
|
while IFS=$'\t' read -r slug stem; do
|
|
[[ -n "$slug" && "$stem" == "$target" ]] && { printf '%s' "$slug"; return 0; }
|
|
done <<< "$_ROSTER_INDEX"
|
|
return 1
|
|
}
|
|
|
|
# build_selection — compute the allowed slug set from --division/--agent/--agents-file.
|
|
# With no filter flags, SELECTION_ACTIVE stays false (install everything).
|
|
build_selection() {
|
|
if [[ ${#FILTER_DIVISIONS[@]} -eq 0 && ${#FILTER_AGENTS[@]} -eq 0 && -z "$AGENTS_FILE" ]]; then
|
|
SELECTION_ACTIVE=false
|
|
return
|
|
fi
|
|
SELECTION_ACTIVE=true
|
|
local slugs="" div f s line requested resolved
|
|
roster_index
|
|
for div in ${FILTER_DIVISIONS[@]+"${FILTER_DIVISIONS[@]}"}; do
|
|
while IFS= read -r f; do
|
|
s="$(agent_slug "$f")"; [[ -n "$s" ]] && slugs+="$s"$'\n'
|
|
done < <(division_files "$div")
|
|
done
|
|
for s in ${FILTER_AGENTS[@]+"${FILTER_AGENTS[@]}"}; do
|
|
requested="$(slugify "$s")"
|
|
if ! resolved="$(resolve_agent "$requested")"; then
|
|
err "Unknown agent '$s'. Use --list agents to see the available roster."
|
|
exit 1
|
|
fi
|
|
slugs+="$resolved"$'\n'
|
|
done
|
|
if [[ -n "$AGENTS_FILE" ]]; then
|
|
[[ -f "$AGENTS_FILE" ]] || { err "agents-file not found: $AGENTS_FILE"; exit 1; }
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
line="${line%%#*}" # strip trailing comment
|
|
line="$(printf '%s' "$line" | xargs 2>/dev/null)" # trim
|
|
[[ -z "$line" ]] && continue
|
|
requested="$(slugify "$line")"
|
|
if ! resolved="$(resolve_agent "$requested")"; then
|
|
err "Unknown agent '$line' in agents-file '$AGENTS_FILE'."
|
|
exit 1
|
|
fi
|
|
slugs+="$resolved"$'\n'
|
|
done < "$AGENTS_FILE"
|
|
fi
|
|
_ALLOWED_SLUGS="$(printf '%s' "$slugs" | sort -u | sed '/^$/d')"
|
|
}
|
|
|
|
# slug_allowed <slug> — true if installable under the active selection
|
|
# (always true when no filter). Tolerates the antigravity "agency-" prefix.
|
|
slug_allowed() {
|
|
$SELECTION_ACTIVE || return 0
|
|
local s="${1#agency-}"
|
|
# grep -q closes a pipe as soon as it finds an early match. With pipefail,
|
|
# printf may then get SIGPIPE and make a valid slug look unselected.
|
|
grep -qxF "$s" <<< "$_ALLOWED_SLUGS"
|
|
}
|
|
|
|
# selected_agent_count — how many agents the current selection installs.
|
|
selected_agent_count() {
|
|
if ! $SELECTION_ACTIVE; then
|
|
local d n=0; for d in "${ALL_DIVISIONS[@]}"; do incr_by n "$(division_count "$d")"; done; echo "$n"
|
|
else
|
|
printf '%s\n' "$_ALLOWED_SLUGS" | grep -c .
|
|
fi
|
|
}
|
|
incr_by() { printf -v "$1" '%d' "$(( ${!1:-0} + ${2:-0} ))"; }
|
|
|
|
# selected_agent_count_all — total agents across all divisions (ignores filter).
|
|
selected_agent_count_all() {
|
|
local d n=0; for d in "${ALL_DIVISIONS[@]}"; do incr_by n "$(division_count "$d")"; done; echo "$n"
|
|
}
|
|
|
|
# worker_flags — re-emit the active selection/mode flags for parallel workers.
|
|
worker_flags() {
|
|
local out="" d a
|
|
$USE_LINK && out="$out --link"
|
|
$AUTO_CONVERT || out="$out --no-convert"
|
|
[[ -n "$OVERRIDE_PATH" ]] && out="$out --path $OVERRIDE_PATH"
|
|
for d in ${FILTER_DIVISIONS[@]+"${FILTER_DIVISIONS[@]}"}; do out="$out --division $d"; done
|
|
for a in ${FILTER_AGENTS[@]+"${FILTER_AGENTS[@]}"}; do out="$out --agent $a"; done
|
|
[[ -n "$AGENTS_FILE" ]] && out="$out --agents-file $AGENTS_FILE"
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# validate_division <name> — exit on unknown division.
|
|
validate_division() {
|
|
local _ad
|
|
for _ad in "${ALL_DIVISIONS[@]}"; do [[ "$_ad" == "$1" ]] && return 0; done
|
|
err "Unknown division '$1'. Valid: ${ALL_DIVISIONS[*]}"
|
|
exit 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Install mechanics (copy vs symlink, path override, capacity guard)
|
|
# ---------------------------------------------------------------------------
|
|
USE_LINK=false # --link
|
|
OVERRIDE_PATH="" # --path (single-destination override)
|
|
|
|
# install_file <src> <dest> — copy, or symlink when --link is set.
|
|
install_file() {
|
|
if $USE_LINK; then
|
|
ln -sf "$1" "$2"
|
|
else
|
|
local target="$2"
|
|
[[ -d "$target" ]] && target="${target%/}/$(basename "$1")"
|
|
if [[ -L "$target" ]]; then
|
|
# cp would follow the link and overwrite whatever it points at.
|
|
local link_to; link_to="$(readlink "$target")"
|
|
if [[ "$link_to" == "$REPO_ROOT/"* ]]; then
|
|
# Our own --link install: switching to a copy is the intended change.
|
|
rm -f -- "$target"
|
|
else
|
|
# Someone else's link: leave it and its target alone, keep installing
|
|
# the rest, and say so in the summary (one stray link must not abort
|
|
# the install halfway through the roster).
|
|
warn "Skipped $target — it is a symlink to $link_to; not overwriting it."
|
|
[[ -n "${SKIPPED_LOG:-}" ]] && printf '%s -> %s\n' "$target" "$link_to" >> "$SKIPPED_LOG"
|
|
return 0
|
|
fi
|
|
fi
|
|
cp "$1" "$2"
|
|
fi
|
|
}
|
|
|
|
# resolve_dest <tool> <default> — --path > $ENV_VAR > default.
|
|
# path_collision_group <tool> — tools in the same group write identical
|
|
# filenames into a shared --path and would overwrite each other; empty means
|
|
# the tool's output is distinct and may share a path with anything. Derived by
|
|
# installing one agent with every tool into a sandbox and comparing what
|
|
# landed; re-measure if a converter's output naming changes.
|
|
path_collision_group() {
|
|
case "$1" in
|
|
claude-code|copilot) printf 'raw-source-md' ;; # <division>-<slug>.md
|
|
gemini-cli|opencode|qwen|zcode) printf 'slug-md' ;; # <slug>.md
|
|
antigravity|osaurus|dsh) printf 'agency-skill' ;; # agency-<slug>/SKILL.md
|
|
*) printf '' ;;
|
|
esac
|
|
}
|
|
|
|
# Validate after tool selection so --tool all and the interactive picker get
|
|
# the same protection as an explicit comma-separated list.
|
|
validate_path_collisions() {
|
|
[[ -n "$OVERRIDE_PATH" && $# -gt 1 ]] || return 0
|
|
local _ta _tb _ga _gb
|
|
for _ta in "$@"; do
|
|
_ga="$(path_collision_group "$_ta")"; [[ -z "$_ga" ]] && continue
|
|
for _tb in "$@"; do
|
|
[[ "$_tb" == "$_ta" ]] && continue
|
|
_gb="$(path_collision_group "$_tb")"
|
|
if [[ "$_ga" == "$_gb" ]]; then
|
|
err "--path is one shared directory, and $_ta and $_tb write the same filenames into it — they would overwrite each other. Use one of them per --path (tools with distinct outputs may share one)."
|
|
return 1
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
resolve_dest() {
|
|
local tool="$1" def="$2" var=""
|
|
[[ -n "$OVERRIDE_PATH" ]] && { printf '%s' "$OVERRIDE_PATH"; return; }
|
|
case "$tool" in
|
|
claude-code) var="CLAUDE_CONFIG_DIR" ;;
|
|
copilot) var="COPILOT_AGENT_DIR" ;;
|
|
cursor) var="CURSOR_RULES_DIR" ;;
|
|
gemini-cli) var="GEMINI_AGENTS_DIR" ;;
|
|
opencode) var="OPENCODE_AGENTS_DIR" ;;
|
|
openclaw) var="OPENCLAW_DIR" ;;
|
|
qwen) var="QWEN_AGENTS_DIR" ;;
|
|
zcode) var="ZCODE_AGENTS_DIR" ;;
|
|
codex) var="CODEX_AGENTS_DIR" ;;
|
|
osaurus) var="OSAURUS_SKILLS_DIR" ;;
|
|
hermes) var="HERMES_PLUGIN_DIR" ;;
|
|
vibe) var="VIBE_HOME" ;;
|
|
dsh) var="DSH_SKILLS_DIR" ;;
|
|
esac
|
|
if [[ -n "$var" && -n "${!var:-}" ]]; then
|
|
if [[ "$tool" == "claude-code" ]]; then
|
|
# CLAUDE_CONFIG_DIR is the config root (it replaces ~/.claude);
|
|
# agents live in its agents/ subdirectory (fixes #578). Strip one
|
|
# trailing slash; a value already ending in /agents is used verbatim
|
|
# so users who worked around the old bug are not double-nested.
|
|
local cfg="${!var}"; cfg="${cfg%/}"
|
|
if [[ "$cfg" == */agents ]]; then printf '%s' "$cfg"; else printf '%s' "$cfg/agents"; fi
|
|
else
|
|
printf '%s' "${!var}"
|
|
fi
|
|
else
|
|
printf '%s' "$def"
|
|
fi
|
|
}
|
|
|
|
# resolve_tool_path <tool> — best-effort binary path for the detection UI.
|
|
resolve_tool_path() {
|
|
local bin=""
|
|
case "$1" in
|
|
claude-code) bin="claude" ;; copilot) bin="code" ;; gemini-cli) bin="gemini" ;;
|
|
opencode) bin="opencode" ;; openclaw) bin="openclaw" ;; cursor) bin="cursor" ;;
|
|
aider) bin="aider" ;; windsurf) bin="windsurf" ;; qwen) bin="qwen" ;;
|
|
zcode) bin="zcode" ;;
|
|
kimi) bin="kimi" ;; codex) bin="codex" ;; antigravity) bin="" ;;
|
|
osaurus) bin="osaurus" ;; hermes) bin="hermes" ;; vibe) bin="vibe" ;;
|
|
dsh) bin="dsh" ;;
|
|
esac
|
|
[[ -n "$bin" ]] && command -v "$bin" 2>/dev/null
|
|
}
|
|
|
|
# ensure_converted <tool> — auto-run convert.sh if a converted tool's output
|
|
# is missing (absorbs #426). No-op for source tools and when --no-convert.
|
|
ensure_converted() {
|
|
local tool="$1"
|
|
$AUTO_CONVERT || return 0
|
|
case "$tool" in claude-code|copilot) return 0 ;; esac
|
|
local d="$INTEGRATIONS/$tool"
|
|
# Every integrations/<tool>/ ships a committed README.md, so "any file
|
|
# present" mistook the README for generated output and never converted in a
|
|
# fresh checkout (the installer then hard-failed "<tool> missing"). Only files
|
|
# other than the README count as output.
|
|
if [[ ! -d "$d" ]] || [[ -z "$(find "$d" -type f ! -name 'README.md' 2>/dev/null | head -1)" ]]; then
|
|
warn "$tool: integration files missing — running convert.sh --tool $tool"
|
|
if "$SCRIPT_DIR/convert.sh" --tool "$tool" >/dev/null 2>&1; then
|
|
ok "$tool: generated integration files"
|
|
else
|
|
# A failed conversion may have written only part of the roster. Remove
|
|
# that partial output so the next install retries conversion instead of
|
|
# treating it as a complete generated integration.
|
|
if [[ -d "$d" ]]; then
|
|
find "$d" -mindepth 1 -maxdepth 1 ! -name 'README.md' -exec rm -rf {} +
|
|
fi
|
|
err "$tool: convert.sh failed; run it manually"
|
|
return 1
|
|
fi
|
|
fi
|
|
}
|
|
AUTO_CONVERT=true # --no-convert disables
|
|
|
|
# Per-tool soft capacity (opencode silently drops past ~119 — upstream #27988).
|
|
tool_cap() { case "$1" in opencode) echo 119 ;; *) echo 0 ;; esac; }
|
|
|
|
# capacity_warn <tool> <count> — warn if a tool can't register this many.
|
|
capacity_warn() {
|
|
local cap; cap="$(tool_cap "$1")"
|
|
if [[ "$cap" -gt 0 && "$2" -gt "$cap" ]]; then
|
|
warn "$1: registers only ~$cap agents (upstream bug anomalyco/opencode#27988)."
|
|
warn " You selected $2 — ~$(( $2 - cap )) won't load. Narrow with --division to fix."
|
|
fi
|
|
}
|
|
|
|
# do_list <what> — print tools/teams/agents and exit.
|
|
do_list() {
|
|
case "$1" in
|
|
tools)
|
|
printf '%s\n' "${ALL_TOOLS[@]}" ;;
|
|
teams|divisions)
|
|
local d; for d in "${ALL_DIVISIONS[@]}"; do printf '%-22s %3s agents\n' "$d" "$(division_count "$d")"; done ;;
|
|
agents)
|
|
local d f; for d in "${ALL_DIVISIONS[@]}"; do
|
|
while IFS= read -r f; do printf '%-20s %s\n' "$d" "$(agent_slug "$f")"; done < <(division_files "$d")
|
|
done ;;
|
|
*)
|
|
echo "Tools (${#ALL_TOOLS[@]}):"; printf ' %s\n' "${ALL_TOOLS[@]}"; echo
|
|
echo "Teams (${#ALL_DIVISIONS[@]}):"
|
|
local d; for d in "${ALL_DIVISIONS[@]}"; do printf ' %-22s %3s agents\n' "$d" "$(division_count "$d")"; done ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Usage
|
|
# ---------------------------------------------------------------------------
|
|
usage() {
|
|
# Extract everything between the USAGE-START / USAGE-END sentinels
|
|
# (excluding the sentinel lines themselves) and strip the leading "# ".
|
|
# Using sentinels instead of hard-coded line numbers means adding lines
|
|
# to the header comment block won't silently break --help output.
|
|
sed -n '/^# --- USAGE-START ---/,/^# --- USAGE-END ---/p' "$0" \
|
|
| sed -e '1d;$d' -e 's/^# \{0,1\}//'
|
|
exit 0
|
|
}
|
|
|
|
# Default parallel job count (nproc on Linux; sysctl on macOS when nproc missing)
|
|
parallel_jobs_default() {
|
|
local n
|
|
n=$(nproc 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
|
|
n=$(sysctl -n hw.ncpu 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
|
|
echo 4
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Preflight
|
|
# ---------------------------------------------------------------------------
|
|
check_integrations() {
|
|
if [[ ! -d "$INTEGRATIONS" ]]; then
|
|
err "integrations/ not found. Run ./scripts/convert.sh first."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool detection
|
|
# ---------------------------------------------------------------------------
|
|
detect_claude_code() { [[ -d "${CLAUDE_CONFIG_DIR:-${HOME}/.claude}" ]]; }
|
|
detect_copilot() { command -v code >/dev/null 2>&1 || [[ -d "${HOME}/.github" || -d "${HOME}/.copilot" ]]; }
|
|
detect_antigravity() { [[ -d "${HOME}/.gemini/config/skills" ]]; }
|
|
detect_gemini_cli() { command -v gemini >/dev/null 2>&1 || [[ -d "${HOME}/.gemini" ]]; }
|
|
detect_cursor() { command -v cursor >/dev/null 2>&1 || [[ -d "${HOME}/.cursor" ]]; }
|
|
detect_opencode() { command -v opencode >/dev/null 2>&1 || [[ -d "${HOME}/.config/opencode" ]]; }
|
|
detect_aider() { command -v aider >/dev/null 2>&1; }
|
|
detect_openclaw() { command -v openclaw >/dev/null 2>&1 || [[ -d "${HOME}/.openclaw" ]]; }
|
|
detect_windsurf() { command -v windsurf >/dev/null 2>&1 || [[ -d "${HOME}/.codeium" ]]; }
|
|
detect_qwen() { command -v qwen >/dev/null 2>&1 || [[ -d "${HOME}/.qwen" ]]; }
|
|
detect_zcode() { command -v zcode >/dev/null 2>&1 || [[ -d "${HOME}/.zcode" ]]; }
|
|
detect_kimi() { command -v kimi >/dev/null 2>&1; }
|
|
detect_codex() { command -v codex >/dev/null 2>&1 || [[ -d "${HOME}/.codex" ]]; }
|
|
detect_osaurus() { command -v osaurus >/dev/null 2>&1 || [[ -d "${HOME}/.osaurus" ]]; }
|
|
detect_hermes() { command -v hermes >/dev/null 2>&1 || [[ -d "${HERMES_HOME:-${HOME}/.hermes}" ]]; }
|
|
detect_vibe() { command -v vibe >/dev/null 2>&1 || [[ -d "${VIBE_HOME:-${HOME}/.vibe}" ]]; }
|
|
detect_dsh() { command -v dsh >/dev/null 2>&1 || [[ -d "${DSH_HOME:-${HOME}/.dsh}" ]]; }
|
|
|
|
is_detected() {
|
|
case "$1" in
|
|
claude-code) detect_claude_code ;;
|
|
copilot) detect_copilot ;;
|
|
antigravity) detect_antigravity ;;
|
|
gemini-cli) detect_gemini_cli ;;
|
|
opencode) detect_opencode ;;
|
|
openclaw) detect_openclaw ;;
|
|
cursor) detect_cursor ;;
|
|
aider) detect_aider ;;
|
|
windsurf) detect_windsurf ;;
|
|
qwen) detect_qwen ;;
|
|
zcode) detect_zcode ;;
|
|
kimi) detect_kimi ;;
|
|
codex) detect_codex ;;
|
|
osaurus) detect_osaurus ;;
|
|
hermes) detect_hermes ;;
|
|
vibe) detect_vibe ;;
|
|
dsh) detect_dsh ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Fixed-width labels: name (14) + detail (24) = 38 visible chars
|
|
tool_label() {
|
|
case "$1" in
|
|
claude-code) printf "%-14s %s" "Claude Code" "(claude.ai/code)" ;;
|
|
copilot) printf "%-14s %s" "Copilot" "(~/.github + ~/.copilot)" ;;
|
|
antigravity) printf "%-14s %s" "Antigravity" "(~/.gemini/config/skills)" ;;
|
|
gemini-cli) printf "%-14s %s" "Gemini CLI" "(~/.gemini/agents)" ;;
|
|
opencode) printf "%-14s %s" "OpenCode" "(opencode.ai)" ;;
|
|
openclaw) printf "%-14s %s" "OpenClaw" "(~/.openclaw/agency-agents)" ;;
|
|
cursor) printf "%-14s %s" "Cursor" "(.cursor/rules)" ;;
|
|
aider) printf "%-14s %s" "Aider" "(CONVENTIONS.md)" ;;
|
|
windsurf) printf "%-14s %s" "Windsurf" "(.windsurfrules)" ;;
|
|
qwen) printf "%-14s %s" "Qwen Code" "(~/.qwen/agents)" ;;
|
|
zcode) printf "%-14s %s" "ZCode" "(~/.zcode/agents)" ;;
|
|
kimi) printf "%-14s %s" "Kimi Code" "(~/.config/kimi/agents)" ;;
|
|
codex) printf "%-14s %s" "Codex" "(~/.codex/agents)" ;;
|
|
osaurus) printf "%-14s %s" "Osaurus" "(~/.osaurus/skills)" ;;
|
|
hermes) printf "%-14s %s" "Hermes" "(~/.hermes/plugins)" ;;
|
|
vibe) printf "%-14s %s" "Mistral Vibe" "(~/.vibe/agents)" ;;
|
|
dsh) printf "%-14s %s" "DeepSeek Harness" "(~/.dsh/skills)" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Interactive selector
|
|
# ---------------------------------------------------------------------------
|
|
# ---------------------------------------------------------------------------
|
|
# Interactive wizard (pure-bash TUI): Tools -> Teams -> Review -> install
|
|
# Uses lib.sh primitives (tui_begin/read_key/draw_frame). Falls back to the
|
|
# legacy auto-detect path when there is no TTY.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Persistent selection state across screens.
|
|
TOOL_SEL=() # 1/0 per ALL_TOOLS
|
|
TEAM_SEL=() # 1/0 per ALL_DIVISIONS
|
|
|
|
# division_emoji <div> — a glyph for the team list (unicode only).
|
|
division_emoji() {
|
|
if ! supports_unicode; then printf '*'; return; fi
|
|
case "$1" in
|
|
academic) printf '📚';; design) printf '🎨';; engineering) printf '💻';;
|
|
finance) printf '💵';; game-development) printf '🎮';; gis) printf '🌍';; marketing) printf '📢';;
|
|
paid-media) printf '💰';; product) printf '📊';; project-management) printf '🎬';;
|
|
research) printf '🔍';; sales) printf '💼';; security) printf '🔒';; spatial-computing) printf '🥽';;
|
|
specialized) printf '🎯';; support) printf '🛟';; testing) printf '🧪';; *) printf '•';;
|
|
esac
|
|
}
|
|
|
|
# Generic multi-select. Inputs (globals): OPT_LABEL[], OPT_SEL[];
|
|
# SEL_TITLE, SEL_HINT, SEL_SUMMARY_FN, SEL_NAV, SEL_WARN_FN.
|
|
# Mutates OPT_SEL[]; sets SEL_RESULT = next|back|quit.
|
|
selector() {
|
|
local n=${#OPT_LABEL[@]} cur=0 top=0 query="" searching=false key i idx vn rows W
|
|
rows=$(( $(term_rows) - 9 )); (( rows < 3 )) && rows=3
|
|
while true; do
|
|
local view=() qlc
|
|
qlc="$(printf '%s' "$query" | tr '[:upper:]' '[:lower:]')"
|
|
for (( i=0; i<n; i++ )); do
|
|
if [[ -z "$query" || "$(printf '%s' "${OPT_LABEL[$i]}" | tr '[:upper:]' '[:lower:]')" == *"$qlc"* ]]; then
|
|
view+=("$i")
|
|
fi
|
|
done
|
|
vn=${#view[@]}
|
|
(( cur>=vn )) && cur=$(( vn>0 ? vn-1 : 0 ))
|
|
(( cur<top )) && top=$cur
|
|
(( cur>=top+rows )) && top=$(( cur-rows+1 ))
|
|
W=$(( $(term_cols) - 4 )); (( W>74 )) && W=74; (( W<40 )) && W=40
|
|
local buf="" hlen=$(( W - ${#SEL_TITLE} - 5 )); (( hlen<1 )) && hlen=1
|
|
buf+=" ${C_BOLD}${C_CYAN}${BX_TL}${BX_H}${BX_H} ${SEL_TITLE} $(repeat "$BX_H" "$hlen")${BX_TR}${C_RESET}"$'\n'
|
|
buf+=" ${C_DIM}${SEL_HINT}${C_RESET}"$'\n\n'
|
|
(( vn==0 )) && buf+=" ${C_DIM}(no matches)${C_RESET}"$'\n'
|
|
for (( i=top; i<top+rows && i<vn; i++ )); do
|
|
idx=${view[$i]}
|
|
local mark cg label="${OPT_LABEL[$idx]}"
|
|
[[ "${OPT_SEL[$idx]}" == 1 ]] && mark="${C_GREEN}${GLYPH_ON}${C_RESET}" || mark="${C_DIM}·${C_RESET}"
|
|
if (( i==cur )); then cg="${C_CYAN}${GLYPH_CUR}${C_RESET}"; label="${C_BOLD}${label}${C_RESET}"; else cg=" "; fi
|
|
buf+=" $cg [$mark] $label"$'\n'
|
|
done
|
|
local shown=$(( vn<rows ? vn : rows )); for (( i=shown; i<rows; i++ )); do buf+=$'\n'; done
|
|
# Consistent footer: summary -> nav -> warnings (-> search line)
|
|
buf+=$'\n'" ${C_BOLD}$("$SEL_SUMMARY_FN")${C_RESET}"$'\n'
|
|
buf+=" ${C_DIM}${SEL_NAV}${C_RESET}"$'\n'
|
|
local _w; _w="$("$SEL_WARN_FN")"; [[ -n "$_w" ]] && buf+=" ${C_YELLOW}${_w}${C_RESET}"$'\n'
|
|
if $searching; then buf+=" ${C_CYAN}search:${C_RESET} ${query}_"$'\n'
|
|
elif [[ -n "$query" ]]; then buf+=" ${C_CYAN}/${query}${C_RESET} ${C_DIM}(esc clears)${C_RESET}"$'\n'; fi
|
|
draw_frame "$buf"
|
|
|
|
key="$(read_key)"
|
|
if $searching; then
|
|
case "$key" in
|
|
ENTER) searching=false ;;
|
|
ESC) query=""; searching=false ;;
|
|
BACKSPACE) query="${query%?}" ;;
|
|
*) [[ ${#key} -eq 1 ]] && query="$query$key" ;;
|
|
esac
|
|
continue
|
|
fi
|
|
case "$key" in
|
|
UP|k) (( cur>0 )) && cur=$(( cur-1 )) ;;
|
|
DOWN|j) (( cur<vn-1 )) && cur=$(( cur+1 )) ;;
|
|
SPACE) (( vn>0 )) && { idx=${view[$cur]}; OPT_SEL[$idx]=$(( 1 - ${OPT_SEL[$idx]} )); } ;;
|
|
a|A) for (( i=0; i<n; i++ )); do OPT_SEL[$i]=1; done ;;
|
|
n|N) for (( i=0; i<n; i++ )); do OPT_SEL[$i]=0; done ;;
|
|
/) searching=true ;;
|
|
ENTER|RIGHT) SEL_RESULT=next; return ;;
|
|
LEFT) SEL_RESULT=back; return ;;
|
|
ESC) [[ -n "$query" ]] && query="" || { SEL_RESULT=back; return; } ;;
|
|
q|Q) SEL_RESULT=quit; return ;;
|
|
EOF) SEL_RESULT=quit; return ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# --- Screen: Tools ---
|
|
_no_warn() { :; }
|
|
_tools_summary() {
|
|
local i c=0; for (( i=0; i<${#OPT_SEL[@]}; i++ )); do [[ "${OPT_SEL[$i]}" == 1 ]] && c=$(( c+1 )); done
|
|
printf '%s of %s tools selected' "$c" "${#OPT_SEL[@]}"
|
|
}
|
|
screen_tools() {
|
|
OPT_LABEL=(); OPT_SEL=()
|
|
local i det path label
|
|
for (( i=0; i<${#ALL_TOOLS[@]}; i++ )); do
|
|
local t="${ALL_TOOLS[$i]}"
|
|
path="$(resolve_tool_path "$t" 2>/dev/null || true)"
|
|
if is_detected "$t" 2>/dev/null; then det="${C_GREEN}${GLYPH_DET}${C_RESET}"; else det="${C_DIM}${GLYPH_OFF}${C_RESET}"; fi
|
|
label="$(printf '%s %-13s %s' "$det" "$(tool_simple_name "$t")" "${C_DIM}${path:-not found}${C_RESET}")"
|
|
OPT_LABEL+=("$label"); OPT_SEL+=("${TOOL_SEL[$i]:-0}")
|
|
done
|
|
SEL_TITLE="The Agency · Installer — 1/3 · Tools"
|
|
SEL_HINT="Pick where to install. ${GLYPH_DET} = detected on this machine."
|
|
SEL_SUMMARY_FN=_tools_summary
|
|
SEL_NAV="space toggle · a all · n none · / search · enter next · q quit"
|
|
SEL_WARN_FN=_no_warn
|
|
selector
|
|
for (( i=0; i<${#OPT_SEL[@]}; i++ )); do TOOL_SEL[$i]="${OPT_SEL[$i]}"; done
|
|
}
|
|
|
|
tool_simple_name() {
|
|
case "$1" in
|
|
claude-code) echo "Claude Code";; copilot) echo "Copilot";; antigravity) echo "Antigravity";;
|
|
gemini-cli) echo "Gemini CLI";; opencode) echo "OpenCode";; openclaw) echo "OpenClaw";;
|
|
cursor) echo "Cursor";; aider) echo "Aider";; windsurf) echo "Windsurf";;
|
|
qwen) echo "Qwen Code";; zcode) echo "ZCode";; kimi) echo "Kimi Code";; codex) echo "Codex";; osaurus) echo "Osaurus";; dsh) echo "DeepSeek Harness";; *) echo "$1";;
|
|
esac
|
|
}
|
|
|
|
# --- Screen: Teams ---
|
|
_teams_agents() {
|
|
local i c=0 d
|
|
for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do
|
|
[[ "${OPT_SEL[$i]}" == 1 ]] && { d="${ALL_DIVISIONS[$i]}"; c=$(( c + ${TEAM_COUNTS[$i]} )); }
|
|
done
|
|
echo "$c"
|
|
}
|
|
_teams_summary() {
|
|
local sel=0 i a; a="$(_teams_agents)"
|
|
for (( i=0; i<${#OPT_SEL[@]}; i++ )); do [[ "${OPT_SEL[$i]}" == 1 ]] && sel=$(( sel+1 )); done
|
|
printf '%s agents · %s of %s teams' "$a" "$sel" "${#OPT_SEL[@]}"
|
|
}
|
|
_teams_warn() {
|
|
local a cap; a="$(_teams_agents)"; cap="$(tool_cap opencode)"
|
|
if _opencode_selected && [[ "$a" -gt "$cap" ]]; then
|
|
printf "⚠ OpenCode registers ~%s; ~%s of %s won't load (#27988)" "$cap" "$(( a - cap ))" "$a"
|
|
fi
|
|
}
|
|
_opencode_selected() {
|
|
local i; for (( i=0; i<${#TOOL_SEL[@]}; i++ )); do
|
|
[[ "${ALL_TOOLS[$i]}" == "opencode" && "${TOOL_SEL[$i]}" == 1 ]] && return 0
|
|
done; return 1
|
|
}
|
|
screen_teams() {
|
|
OPT_LABEL=(); OPT_SEL=()
|
|
local i
|
|
for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do
|
|
local d="${ALL_DIVISIONS[$i]}"
|
|
OPT_LABEL+=("$(printf '%s %-20s %s' "$(division_emoji "$d")" "$d" "${C_DIM}${TEAM_COUNTS[$i]} agents${C_RESET}")")
|
|
OPT_SEL+=("${TEAM_SEL[$i]:-1}")
|
|
done
|
|
SEL_TITLE="The Agency · Installer — 2/3 · Teams"
|
|
SEL_HINT="Pick which teams to install. Fewer teams keeps OpenCode under its limit."
|
|
SEL_SUMMARY_FN=_teams_summary
|
|
SEL_NAV="space toggle · a all · n none · / search · enter next · ← back · q quit"
|
|
SEL_WARN_FN=_teams_warn
|
|
selector
|
|
for (( i=0; i<${#OPT_SEL[@]}; i++ )); do TEAM_SEL[$i]="${OPT_SEL[$i]}"; done
|
|
}
|
|
|
|
# --- Screen: Review ---
|
|
REVIEW_RESULT=""
|
|
# grid_2col <cellwidth> <items...> — lay items out in two column-major columns
|
|
# (left column filled top-to-bottom first). Plain text cells (no ANSI) so the
|
|
# width padding stays correct.
|
|
grid_2col() {
|
|
local w="$1"; shift
|
|
local n=$# r rows left right out=""
|
|
(( n==0 )) && { printf ' %snone%s\n' "$C_DIM" "$C_RESET"; return; }
|
|
local items=("$@")
|
|
rows=$(( (n + 1) / 2 ))
|
|
for (( r=0; r<rows; r++ )); do
|
|
left="${items[$r]}"
|
|
right="${items[$(( r + rows ))]:-}"
|
|
if [[ -n "$right" ]]; then out+="$(printf ' %-*s %s' "$w" "$left" "$right")"$'\n'
|
|
else out+=" $left"$'\n'; fi
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
screen_review() {
|
|
local tools=() teams=() i agents
|
|
for (( i=0; i<${#TOOL_SEL[@]}; i++ )); do [[ "${TOOL_SEL[$i]}" == 1 ]] && tools+=("$(tool_simple_name "${ALL_TOOLS[$i]}")"); done
|
|
for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] && teams+=("${ALL_DIVISIONS[$i]}"); done
|
|
agents=0; for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] && agents=$(( agents + ${TEAM_COUNTS[$i]} )); done
|
|
local cur=0 # 0=Install 1=mode toggle
|
|
while true; do
|
|
local buf="" m
|
|
# pager
|
|
buf+=" ${C_BOLD}${C_CYAN}${BX_TL}${BX_H}${BX_H} The Agency · Installer — 3/3 · Review $(repeat "$BX_H" 28)${BX_TR}${C_RESET}"$'\n'
|
|
# description
|
|
buf+=" ${C_DIM}Confirm your selection, then install.${C_RESET}"$'\n\n'
|
|
# content: the selections + the mode toggle
|
|
buf+=" ${C_BOLD}Tools${C_RESET} ${C_DIM}(${#tools[@]})${C_RESET}"$'\n'
|
|
buf+="$(grid_2col 16 ${tools[@]+"${tools[@]}"})"$'\n'
|
|
buf+=" ${C_BOLD}Teams${C_RESET} ${C_DIM}(${#teams[@]})${C_RESET}"$'\n'
|
|
buf+="$(grid_2col 20 ${teams[@]+"${teams[@]}"})"$'\n\n'
|
|
$USE_LINK && m="symlink" || m="copy"
|
|
if (( cur==1 )); then buf+=" ${C_CYAN}${GLYPH_CUR}${C_RESET} Mode: ${C_BOLD}${m}${C_RESET} ${C_DIM}(space toggles copy/symlink)${C_RESET}"$'\n'
|
|
else buf+=" Mode: ${m} ${C_DIM}(space toggles copy/symlink)${C_RESET}"$'\n'; fi
|
|
buf+=$'\n'
|
|
# summary
|
|
buf+=" ${C_BOLD}Installing ${agents} agents · ${#teams[@]} teams · ${#tools[@]} tools${C_RESET}"$'\n'
|
|
# navigation (Install is the action cursor target)
|
|
if (( cur==0 )); then buf+=" ${C_CYAN}${GLYPH_CUR}${C_RESET} ${C_BOLD}${C_GREEN}Install${C_RESET} ${C_DIM}↑/↓ move · enter install · ← back · q quit${C_RESET}"$'\n'
|
|
else buf+=" ${C_GREEN}Install${C_RESET} ${C_DIM}↑/↓ move · space toggle mode · ← back · q quit${C_RESET}"$'\n'; fi
|
|
# warnings
|
|
local cap; cap="$(tool_cap opencode)"
|
|
if printf '%s\n' "${tools[@]}" | grep -qx "OpenCode" && [[ "$agents" -gt "$cap" ]]; then
|
|
buf+=" ${C_YELLOW}⚠ OpenCode registers ~${cap}; ~$(( agents - cap )) of ${agents} won't load (#27988)${C_RESET}"$'\n'
|
|
fi
|
|
draw_frame "$buf"
|
|
local key; key="$(read_key)"
|
|
case "$key" in
|
|
UP|DOWN|k|j|TAB) cur=$(( 1 - cur )) ;;
|
|
SPACE) (( cur==1 )) && { $USE_LINK && USE_LINK=false || USE_LINK=true; } ;;
|
|
ENTER) if (( cur==0 )); then REVIEW_RESULT=install; return; fi ;;
|
|
LEFT) REVIEW_RESULT=back; return ;;
|
|
q|Q|EOF) REVIEW_RESULT=quit; return ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# interactive_wizard — drive the three screens; commit to SELECTED_TOOLS /
|
|
# FILTER_DIVISIONS / USE_LINK. Returns 1 if no TTY (caller falls back).
|
|
interactive_wizard() {
|
|
init_ansi
|
|
TEAM_COUNTS=(); local i
|
|
for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do TEAM_COUNTS+=("$(division_count "${ALL_DIVISIONS[$i]}")"); done
|
|
# seed defaults: tools = detected, teams = all
|
|
TOOL_SEL=(); for (( i=0; i<${#ALL_TOOLS[@]}; i++ )); do is_detected "${ALL_TOOLS[$i]}" 2>/dev/null && TOOL_SEL+=(1) || TOOL_SEL+=(0); done
|
|
TEAM_SEL=(); for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do TEAM_SEL+=(1); done
|
|
|
|
tui_begin || return 1
|
|
local screen=tools
|
|
while true; do
|
|
case "$screen" in
|
|
tools) screen_tools; case "$SEL_RESULT" in next) screen=teams;; quit) tui_end; exit 0;; esac ;;
|
|
teams) screen_teams; case "$SEL_RESULT" in next) screen=review;; back) screen=tools;; quit) tui_end; exit 0;; esac ;;
|
|
review) screen_review; case "$REVIEW_RESULT" in install) break;; back) screen=teams;; quit) tui_end; exit 0;; esac ;;
|
|
esac
|
|
done
|
|
tui_end
|
|
|
|
# commit
|
|
SELECTED_TOOLS=()
|
|
for (( i=0; i<${#TOOL_SEL[@]}; i++ )); do [[ "${TOOL_SEL[$i]}" == 1 ]] && SELECTED_TOOLS+=("${ALL_TOOLS[$i]}"); done
|
|
FILTER_DIVISIONS=()
|
|
local all=1
|
|
for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] || all=0; done
|
|
if [[ "$all" == 0 ]]; then
|
|
for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] && FILTER_DIVISIONS+=("${ALL_DIVISIONS[$i]}"); done
|
|
fi
|
|
build_selection
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Installers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
install_claude_code() {
|
|
local dest; dest="$(resolve_dest claude-code "${HOME}/.claude/agents")"
|
|
local count=0 dir f slug
|
|
mkdir -p "$dest"
|
|
for dir in "${AGENT_DIRS[@]}"; do
|
|
[[ -d "$REPO_ROOT/$dir" ]] || continue
|
|
while IFS= read -r -d '' f; do
|
|
is_agent_file "$f" || continue
|
|
slug="$(agent_slug "$f")"; slug_allowed "$slug" || continue
|
|
install_file "$f" "$dest/"; incr count
|
|
done < <(find "$REPO_ROOT/$dir" -name "*.md" -type f -print0)
|
|
done
|
|
ok "Claude Code: $count agents -> $dest"
|
|
}
|
|
|
|
install_copilot() {
|
|
local dest_github; dest_github="$(resolve_dest copilot "${HOME}/.github/agents")"
|
|
local dest_copilot=""
|
|
# The two default locations are intentional, but an explicit destination
|
|
# must not also write into the user's default Copilot directory.
|
|
if [[ -z "$OVERRIDE_PATH" && -z "${COPILOT_AGENT_DIR:-}" ]]; then
|
|
dest_copilot="${HOME}/.copilot/agents"
|
|
fi
|
|
local count=0 dir f slug
|
|
mkdir -p "$dest_github"
|
|
[[ -n "$dest_copilot" ]] && mkdir -p "$dest_copilot"
|
|
for dir in "${AGENT_DIRS[@]}"; do
|
|
[[ -d "$REPO_ROOT/$dir" ]] || continue
|
|
while IFS= read -r -d '' f; do
|
|
is_agent_file "$f" || continue
|
|
slug="$(agent_slug "$f")"; slug_allowed "$slug" || continue
|
|
install_file "$f" "$dest_github/"
|
|
[[ -n "$dest_copilot" ]] && install_file "$f" "$dest_copilot/"
|
|
incr count
|
|
done < <(find "$REPO_ROOT/$dir" -name "*.md" -type f -print0)
|
|
done
|
|
ok "Copilot: $count agents -> $dest_github"
|
|
[[ -n "$dest_copilot" ]] && ok "Copilot: $count agents -> $dest_copilot"
|
|
warn "Copilot: Verify VS Code setting 'chat.agentFilesLocations' includes your install path."
|
|
dim " Open Settings (Ctrl/Cmd+,) -> search 'chat.agentFilesLocations'"
|
|
}
|
|
|
|
install_antigravity() {
|
|
local src="$INTEGRATIONS/antigravity"
|
|
local dest; dest="$(resolve_dest antigravity "${HOME}/.gemini/config/skills")"
|
|
local count=0
|
|
[[ -d "$src" ]] || { err "integrations/antigravity missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest"
|
|
local d
|
|
while IFS= read -r -d '' d; do
|
|
local name; name="$(basename "$d")"
|
|
slug_allowed "$name" || continue
|
|
mkdir -p "$dest/$name"
|
|
install_file "$d/SKILL.md" "$dest/$name/SKILL.md"
|
|
incr count
|
|
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
|
|
ok "Antigravity: $count skills -> $dest"
|
|
}
|
|
|
|
install_osaurus() {
|
|
local src="$INTEGRATIONS/osaurus"
|
|
local dest; dest="$(resolve_dest osaurus "${HOME}/.osaurus/skills")"
|
|
local count=0
|
|
[[ -d "$src" ]] || { err "integrations/osaurus missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest"
|
|
local d
|
|
while IFS= read -r -d '' d; do
|
|
local name; name="$(basename "$d")"
|
|
slug_allowed "$name" || continue
|
|
mkdir -p "$dest/$name"
|
|
install_file "$d/SKILL.md" "$dest/$name/SKILL.md"
|
|
incr count
|
|
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
|
|
ok "Osaurus: $count skills -> $dest"
|
|
}
|
|
|
|
install_dsh() {
|
|
local src="$INTEGRATIONS/dsh"
|
|
local dest; dest="$(resolve_dest dsh "${DSH_HOME:-${HOME}/.dsh}/skills")"
|
|
local count=0
|
|
[[ -d "$src" ]] || { err "integrations/dsh missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest"
|
|
local d
|
|
while IFS= read -r -d '' d; do
|
|
local name; name="$(basename "$d")"
|
|
slug_allowed "$name" || continue
|
|
mkdir -p "$dest/$name"
|
|
install_file "$d/SKILL.md" "$dest/$name/SKILL.md"
|
|
incr count
|
|
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
|
|
ok "DeepSeek Harness: $count skills -> $dest"
|
|
warn "DeepSeek Harness: set DSH_SKILLS_DIR=.dsh/skills (in a project) to install there instead."
|
|
if command -v dsh >/dev/null 2>&1; then
|
|
warn "DeepSeek Harness: activate an agent with /agency-<slug> or by name in conversation."
|
|
fi
|
|
}
|
|
|
|
install_gemini_cli() {
|
|
local src="$INTEGRATIONS/gemini-cli/agents"
|
|
local dest; dest="$(resolve_dest gemini-cli "${HOME}/.gemini/agents")"
|
|
local count=0
|
|
[[ -d "$src" ]] || { err "integrations/gemini-cli/agents missing. Run ./scripts/convert.sh --tool gemini-cli first."; return 1; }
|
|
mkdir -p "$dest"
|
|
local f
|
|
while IFS= read -r -d '' f; do
|
|
slug_allowed "$(basename "$f" .md)" || continue
|
|
install_file "$f" "$dest/"
|
|
incr count
|
|
done < <(find "$src" -maxdepth 1 -name "*.md" -print0)
|
|
ok "Gemini CLI: $count agents -> $dest"
|
|
}
|
|
|
|
install_opencode() {
|
|
local src="$INTEGRATIONS/opencode"
|
|
local dest; dest="$(resolve_dest opencode "${PWD}/.opencode/agents")"
|
|
local count=0
|
|
[[ -d "$src" ]] || { err "integrations/opencode missing. Run convert.sh first."; return 1; }
|
|
# Support both flat layout (integrations/opencode/*.md) and nested (integrations/opencode/agents/*.md)
|
|
local search_dir="$src"
|
|
[[ -d "$src/agents" ]] && search_dir="$src/agents"
|
|
mkdir -p "$dest"
|
|
local f base
|
|
while IFS= read -r -d '' f; do
|
|
base="$(basename "$f")"
|
|
[[ "$base" == "README.md" ]] && continue
|
|
slug_allowed "${base%.md}" || continue
|
|
install_file "$f" "$dest/"; incr count
|
|
done < <(find "$search_dir" -maxdepth 1 -name "*.md" -print0)
|
|
if (( count == 0 )); then
|
|
warn "OpenCode: no agent files found in $search_dir. Run convert.sh --tool opencode first."
|
|
else
|
|
ok "OpenCode: $count agents -> $dest"
|
|
fi
|
|
capacity_warn opencode "$count"
|
|
warn "OpenCode: project-scoped. Run from your project root to install there."
|
|
}
|
|
|
|
install_openclaw() {
|
|
local src="$INTEGRATIONS/openclaw"
|
|
local dest; dest="$(resolve_dest openclaw "${HOME}/.openclaw/agency-agents")"
|
|
local count=0
|
|
local existing_agents=""
|
|
local failed_names="" # a string, not an array: bash 3.2 + set -u rejects "${empty[@]}"
|
|
[[ -d "$src" ]] || { err "integrations/openclaw missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest"
|
|
if command -v openclaw >/dev/null 2>&1; then
|
|
local agents_json
|
|
if ! agents_json="$(openclaw agents list --json 2>/dev/null)"; then
|
|
err "OpenClaw: could not list registered agents; refusing to guess which workspaces need registration."
|
|
return 1
|
|
fi
|
|
# IDs may appear in compact or pretty JSON, and several may share a line.
|
|
# Agent IDs are slugs, so quoted id tokens need no JSON parser dependency.
|
|
existing_agents=$'\n'"$(printf '%s' "$agents_json" | grep -oE '"id"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
| sed -E 's/^"id"[[:space:]]*:[[:space:]]*"([^"]*)"$/\1/' || true)"$'\n'
|
|
fi
|
|
local d
|
|
while IFS= read -r -d '' d; do
|
|
local name; name="$(basename "$d")"
|
|
slug_allowed "$name" || continue
|
|
[[ -f "$d/SOUL.md" && -f "$d/AGENTS.md" && -f "$d/IDENTITY.md" ]] || continue
|
|
mkdir -p "$dest/$name"
|
|
install_file "$d/SOUL.md" "$dest/$name/SOUL.md"
|
|
install_file "$d/AGENTS.md" "$dest/$name/AGENTS.md"
|
|
install_file "$d/IDENTITY.md" "$dest/$name/IDENTITY.md"
|
|
if command -v openclaw >/dev/null 2>&1; then
|
|
if [[ "$existing_agents" != *$'\n'"$name"$'\n'* ]]; then
|
|
if ! openclaw agents add "$name" --workspace "$dest/$name" --non-interactive; then
|
|
err "OpenClaw: failed to register '$name'; the copied workspace is not active."
|
|
# Keep registering the rest: one bad registration must not cost the others.
|
|
failed_names="${failed_names:+$failed_names }$name"
|
|
continue
|
|
fi
|
|
fi
|
|
fi
|
|
(( count++ )) || true
|
|
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
|
|
if (( count == 0 )); then
|
|
err "integrations/openclaw contains no generated workspaces. Run ./scripts/convert.sh --tool openclaw first."
|
|
return 1
|
|
fi
|
|
ok "OpenClaw: $count workspaces -> $dest"
|
|
if command -v openclaw >/dev/null 2>&1; then
|
|
warn "OpenClaw: run 'openclaw gateway restart' to activate new agents"
|
|
fi
|
|
if [[ -n "$failed_names" ]]; then
|
|
err "OpenClaw: not registered: $failed_names. Their workspaces are copied; re-run to retry registration."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
install_cursor() {
|
|
local src="$INTEGRATIONS/cursor/rules"
|
|
local dest; dest="$(resolve_dest cursor "${PWD}/.cursor/rules")"
|
|
local count=0
|
|
[[ -d "$src" ]] || { err "integrations/cursor missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest"
|
|
local f
|
|
while IFS= read -r -d '' f; do
|
|
slug_allowed "$(basename "$f" .mdc)" || continue
|
|
install_file "$f" "$dest/"; incr count
|
|
done < <(find "$src" -maxdepth 1 -name "*.mdc" -print0)
|
|
ok "Cursor: $count rules -> $dest"
|
|
warn "Cursor: project-scoped. Run from your project root to install there."
|
|
}
|
|
|
|
install_aider() {
|
|
local src="$INTEGRATIONS/aider/CONVENTIONS.md"
|
|
local dest_dir; dest_dir="$(resolve_dest aider "$PWD")"
|
|
local dest="$dest_dir/CONVENTIONS.md"
|
|
[[ -f "$src" ]] || { err "integrations/aider/CONVENTIONS.md missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest_dir"
|
|
if [[ -f "$dest" ]]; then
|
|
# Never overwrite: CONVENTIONS.md is aider's own user-authored file, and the
|
|
# one sitting here may well be the reader's rather than ours. But the guard
|
|
# used to strand the very users this integration was fixed for — anyone
|
|
# holding the pre-index roster (3.8M characters, far past what aider can keep
|
|
# in context for a session) re-ran the installer, read "already exists", and
|
|
# kept the broken file. Our generated file has always opened with the same
|
|
# marker, so tell our stale copy apart from someone else's conventions.
|
|
if head -n 1 "$dest" | grep -q 'The Agency'; then
|
|
local bytes; bytes="$(wc -c < "$dest" | tr -d ' ')"
|
|
warn "Aider: $dest is an Agency roster index from an earlier install ($bytes bytes)."
|
|
dim " The roster is an index now, not the agents themselves. Delete it and"
|
|
dim " re-run this installer to pick up the smaller file."
|
|
else
|
|
warn "Aider: CONVENTIONS.md already exists at $dest — leaving your file alone."
|
|
dim " Remove it and re-run to install the Agency roster index instead."
|
|
fi
|
|
return 0
|
|
fi
|
|
install_file "$src" "$dest"
|
|
ok "Aider: installed -> $dest"
|
|
dim " CONVENTIONS.md is the roster index. Load one agent's full instructions with"
|
|
dim " /read-only $REPO_ROOT/<path shown in the index>"
|
|
$SELECTION_ACTIVE && warn "Aider: single-file format — team/agent filtering N/A (installs the full roster)."
|
|
warn "Aider: project-scoped. Run from your project root to install there."
|
|
}
|
|
|
|
install_windsurf() {
|
|
local src="$INTEGRATIONS/windsurf/.windsurfrules"
|
|
local dest_dir; dest_dir="$(resolve_dest windsurf "$PWD")"
|
|
local dest="$dest_dir/.windsurfrules"
|
|
[[ -f "$src" ]] || { err "integrations/windsurf/.windsurfrules missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest_dir"
|
|
if [[ -f "$dest" ]]; then
|
|
warn "Windsurf: .windsurfrules already exists at $dest (remove to reinstall)."
|
|
return 0
|
|
fi
|
|
install_file "$src" "$dest"
|
|
ok "Windsurf: installed -> $dest"
|
|
$SELECTION_ACTIVE && warn "Windsurf: single-file format — team/agent filtering N/A (installs the full roster)."
|
|
warn "Windsurf: project-scoped. Run from your project root to install there."
|
|
}
|
|
|
|
install_qwen() {
|
|
local src="$INTEGRATIONS/qwen/agents"
|
|
local dest; dest="$(resolve_dest qwen "${PWD}/.qwen/agents")"
|
|
local count=0
|
|
|
|
[[ -d "$src" ]] || { err "integrations/qwen missing. Run convert.sh first."; return 1; }
|
|
|
|
mkdir -p "$dest"
|
|
|
|
local f
|
|
while IFS= read -r -d '' f; do
|
|
slug_allowed "$(basename "$f" .md)" || continue
|
|
install_file "$f" "$dest/"
|
|
incr count
|
|
done < <(find "$src" -maxdepth 1 -name "*.md" -print0)
|
|
|
|
ok "Qwen Code: installed $count agents to $dest"
|
|
warn "Qwen Code: project-scoped. Run from your project root to install there."
|
|
warn "Tip: Run '/agents manage' in Qwen Code to refresh, or restart session"
|
|
}
|
|
|
|
install_zcode() {
|
|
local src="$INTEGRATIONS/zcode/agents"
|
|
local dest; dest="$(resolve_dest zcode "${HOME}/.zcode/agents")"
|
|
local count=0
|
|
|
|
[[ -d "$src" ]] || { err "integrations/zcode missing. Run convert.sh first."; return 1; }
|
|
|
|
mkdir -p "$dest"
|
|
|
|
local f
|
|
while IFS= read -r -d '' f; do
|
|
slug_allowed "$(basename "$f" .md)" || continue
|
|
install_file "$f" "$dest/"
|
|
incr count
|
|
done < <(find "$src" -maxdepth 1 -name "*.md" -print0)
|
|
|
|
ok "ZCode: installed $count agents to $dest"
|
|
warn "ZCode: set ZCODE_AGENTS_DIR=.zcode/agents (in a project) to install there instead."
|
|
}
|
|
|
|
install_kimi() {
|
|
local src="$INTEGRATIONS/kimi"
|
|
local dest; dest="$(resolve_dest kimi "${HOME}/.config/kimi/agents")"
|
|
local count=0
|
|
|
|
[[ -d "$src" ]] || { err "integrations/kimi missing. Run convert.sh first."; return 1; }
|
|
|
|
mkdir -p "$dest"
|
|
|
|
local d
|
|
while IFS= read -r -d '' d; do
|
|
local name; name="$(basename "$d")"
|
|
slug_allowed "$name" || continue
|
|
mkdir -p "$dest/$name"
|
|
install_file "$d/agent.yaml" "$dest/$name/agent.yaml"
|
|
install_file "$d/system.md" "$dest/$name/system.md"
|
|
incr count
|
|
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
|
|
|
|
ok "Kimi Code: installed $count agents to $dest"
|
|
ok "Usage: kimi --agent-file ~/.config/kimi/agents/<agent-name>/agent.yaml"
|
|
}
|
|
|
|
install_codex() {
|
|
local src="$INTEGRATIONS/codex/agents"
|
|
local dest; dest="$(resolve_dest codex "${HOME}/.codex/agents")"
|
|
local count=0
|
|
[[ -d "$src" ]] || { err "integrations/codex missing. Run convert.sh first."; return 1; }
|
|
mkdir -p "$dest"
|
|
local f
|
|
while IFS= read -r -d '' f; do
|
|
slug_allowed "$(basename "$f" .toml)" || continue
|
|
install_file "$f" "$dest/"
|
|
incr count
|
|
done < <(find "$src" -maxdepth 1 -name "*.toml" -print0)
|
|
ok "Codex: $count agents -> $dest"
|
|
}
|
|
|
|
install_vibe() {
|
|
local src_agents="$INTEGRATIONS/vibe/agents"
|
|
local src_prompts="$INTEGRATIONS/vibe/prompts"
|
|
local dest; dest="$(resolve_dest vibe "${HOME}/.vibe")"
|
|
local count=0
|
|
|
|
[[ -d "$src_agents" && -d "$src_prompts" ]] || { err "integrations/vibe missing. Run convert.sh first."; return 1; }
|
|
|
|
mkdir -p "$dest/agents" "$dest/prompts"
|
|
|
|
local agent_file prompt_file slug
|
|
|
|
while IFS= read -r -d '' agent_file; do
|
|
slug="$(basename "$agent_file" .toml)"
|
|
slug_allowed "$slug" || continue
|
|
|
|
# Find the corresponding prompt file
|
|
prompt_file="$src_prompts/$slug.md"
|
|
|
|
[[ -f "$prompt_file" ]] || continue
|
|
|
|
install_file "$agent_file" "$dest/agents/"
|
|
install_file "$prompt_file" "$dest/prompts/"
|
|
incr count
|
|
done < <(find "$src_agents" -maxdepth 1 -name "*.toml" -print0)
|
|
|
|
ok "Mistral Vibe: $count agents -> $dest/agents/ and $dest/prompts/"
|
|
}
|
|
|
|
vibe_home_dir() {
|
|
printf '%s\n' "${VIBE_HOME:-${HOME}/.vibe}"
|
|
}
|
|
|
|
hermes_home_dir() {
|
|
printf '%s\n' "${HERMES_HOME:-${HOME}/.hermes}"
|
|
}
|
|
|
|
ensure_hermes_plugin_enabled() {
|
|
local hermes_home config plugin backup
|
|
hermes_home="$(hermes_home_dir)"
|
|
config="${hermes_home}/config.yaml"
|
|
plugin="agency-agents-router"
|
|
mkdir -p "$hermes_home"
|
|
backup="${config}.bak.agency-agents-plugin.$$"
|
|
[[ -f "$config" ]] && cp "$config" "$backup"
|
|
python3 - "$config" "$plugin" <<'PY' || return 1
|
|
from pathlib import Path
|
|
import sys
|
|
import re
|
|
|
|
path = Path(sys.argv[1])
|
|
plugin = sys.argv[2]
|
|
text = path.read_text() if path.exists() else ""
|
|
lines = text.splitlines()
|
|
plugin_strip = plugin.strip()
|
|
|
|
# plugins block + enabled: key/indent/rest. Avoids old 2-space hardcode and cross-list scan into disabled:/entries: (#879).
|
|
plugin_start = None
|
|
end_line = None
|
|
enabled_idx = None
|
|
enabled_indent = ""
|
|
enabled_rest = ""
|
|
item_indent = ""
|
|
has_enabled = False
|
|
for i, line in enumerate(lines):
|
|
if line.startswith("plugins:"):
|
|
plugin_start = i
|
|
j = i + 1
|
|
broke = False
|
|
while j < len(lines):
|
|
jl = lines[j]
|
|
# Only a top-level KEY ends the block. Hermes writes enabled:/disabled: below a
|
|
# column-0 "# ====" section banner; treating that comment as the end hid them.
|
|
if jl and not jl.startswith((" ", "\t")) and not jl.startswith("#"):
|
|
broke = True
|
|
break
|
|
stripped = jl.strip()
|
|
if stripped.startswith("enabled:") and not has_enabled:
|
|
has_enabled = True
|
|
enabled_idx = j
|
|
enabled_indent = jl[: len(jl) - len(stripped)]
|
|
enabled_rest = stripped[len("enabled:") :].strip()
|
|
j += 1
|
|
# ran off EOF: end_line = one past last scanned line so inserts land right.
|
|
end_line = j if broke else len(lines)
|
|
break
|
|
|
|
# plugins: must be bare key; inline {} or scalar can't be edited line-wise — bail.
|
|
if plugin_start is not None:
|
|
if re.sub(r"\s*#.*$", "", lines[plugin_start]).strip() != "plugins:":
|
|
sys.exit(1)
|
|
|
|
# Classify enabled: rest: empty (block), [] (empty), [a,b] (flow); else bail.
|
|
enabled_empty = False
|
|
inline_flow = False
|
|
inline_items = []
|
|
inline_comment = ""
|
|
if has_enabled:
|
|
rest_nc = re.sub(r"\s*#.*$", "", enabled_rest).strip()
|
|
if rest_nc == "":
|
|
pass # block-style list (or an empty key) — handled below
|
|
elif re.fullmatch(r"\[[^\[\]]*\]", rest_nc):
|
|
inner = rest_nc[1:-1]
|
|
inline_items = [
|
|
p.strip().strip("\"'") for p in inner.split(",") if p.strip()
|
|
]
|
|
if inline_items:
|
|
inline_flow = True
|
|
inline_comment = enabled_rest[enabled_rest.find("]") + 1 :]
|
|
else:
|
|
enabled_empty = True
|
|
else:
|
|
sys.exit(1)
|
|
|
|
# enabled: sub-block ends at first sibling key (indent <= enabled_indent); blanks/comments/items don't end it (#879).
|
|
enabled_end = end_line
|
|
if has_enabled:
|
|
for j in range(enabled_idx + 1, end_line):
|
|
line = lines[j]
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
this_indent = line[: len(line) - len(stripped)]
|
|
if not stripped.startswith("-") and len(this_indent) <= len(enabled_indent):
|
|
enabled_end = j
|
|
break
|
|
|
|
# item_indent from within (enabled_idx, enabled_end) so sibling lists can't leak in (#879).
|
|
if has_enabled and not enabled_empty and not inline_flow:
|
|
for idx in range(enabled_idx + 1, enabled_end):
|
|
stripped = lines[idx].strip()
|
|
if stripped.startswith("-"):
|
|
item_indent = lines[idx][: len(lines[idx]) - len(stripped)]
|
|
break
|
|
|
|
# Detect "already enabled" + corrupted-scalar form (glued "- " from old 2-space bug); repair splits to one per line.
|
|
corrupted_lines = []
|
|
has_plugin_already = False
|
|
if inline_flow:
|
|
has_plugin_already = plugin_strip in inline_items
|
|
elif has_enabled and not enabled_empty:
|
|
for idx in range(enabled_idx + 1, enabled_end):
|
|
l = lines[idx]
|
|
stripped = l.strip()
|
|
if not stripped.startswith("-"):
|
|
continue
|
|
# >1 "- " in stripped line = corrupted glue (strict match won't work: names contain dashes).
|
|
if stripped.count("- ") > 1:
|
|
corrupted_lines.append(idx)
|
|
else:
|
|
value = stripped[1:].strip().strip("\"'")
|
|
if value == plugin_strip:
|
|
has_plugin_already = True
|
|
|
|
# Repair in reverse to keep indices. Splice grows the block — sync enabled_end with end_line or the stale sweep eats the new plugin (#879).
|
|
for idx in sorted(corrupted_lines, reverse=True):
|
|
l = lines[idx]
|
|
stripped = l.strip()
|
|
if not item_indent:
|
|
item_indent = l[: len(l) - len(stripped)] or (enabled_indent + " ")
|
|
content = stripped[1:].strip()
|
|
parts = re.split(r"\s+-\s+", content)
|
|
new_lines = [f"{item_indent}- {parts[0]}"]
|
|
for p in parts[1:]:
|
|
new_lines.append(f"{item_indent}- {p}")
|
|
lines[idx : idx + 1] = new_lines
|
|
end_line += len(new_lines) - 1
|
|
enabled_end += len(new_lines) - 1
|
|
# Re-check presence after rewrite.
|
|
has_plugin_already = False
|
|
for nl in lines[enabled_idx + 1 : enabled_end]:
|
|
if nl.strip().startswith("-") and nl[len(item_indent) :].strip() == f"- {plugin_strip}":
|
|
has_plugin_already = True
|
|
break
|
|
|
|
# Remove stale plugin entries elsewhere in the block (disabled:, entries:); sweep whole block if no enabled: yet (#879).
|
|
if plugin_start is not None:
|
|
stale = []
|
|
if has_enabled:
|
|
scan_ranges = [
|
|
range(plugin_start + 1, enabled_idx),
|
|
range(enabled_end, end_line),
|
|
]
|
|
else:
|
|
scan_ranges = [range(plugin_start + 1, end_line)]
|
|
for rng in scan_ranges:
|
|
for idx in rng:
|
|
stripped = lines[idx].strip()
|
|
if stripped.startswith("-"):
|
|
value = stripped[1:].strip().strip("\"'")
|
|
if value == plugin_strip:
|
|
stale.append(idx)
|
|
for idx in sorted(stale, reverse=True):
|
|
del lines[idx]
|
|
end_line -= 1
|
|
if has_enabled and idx < enabled_idx:
|
|
enabled_idx -= 1
|
|
enabled_end -= 1
|
|
elif has_enabled and idx < enabled_end:
|
|
enabled_end -= 1
|
|
|
|
# Idempotent fast path.
|
|
if has_plugin_already:
|
|
path.write_text("\n".join(lines) + "\n")
|
|
sys.exit(0)
|
|
|
|
new_item_line = f"{item_indent or (enabled_indent + ' ')}- {plugin}"
|
|
|
|
# Case 1: no plugins: block at all.
|
|
if plugin_start is None:
|
|
if lines and lines[-1].strip():
|
|
lines.append("")
|
|
lines.append("plugins:")
|
|
lines.append(f"{enabled_indent or ' '}enabled:")
|
|
lines.append(new_item_line)
|
|
path.write_text("\n".join(lines) + "\n")
|
|
sys.exit(0)
|
|
|
|
# Case 2: no enabled: key — create as first child at existing child indent (else sibling items dedent to col 0 → invalid YAML).
|
|
if not has_enabled:
|
|
child_indent = ""
|
|
for idx in range(plugin_start + 1, end_line):
|
|
stripped = lines[idx].strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
child_indent = lines[idx][: len(lines[idx]) - len(stripped)]
|
|
break
|
|
child_indent = child_indent or " "
|
|
lines[plugin_start + 1 : plugin_start + 1] = [
|
|
f"{child_indent}enabled:",
|
|
f"{child_indent} - {plugin}",
|
|
]
|
|
path.write_text("\n".join(lines) + "\n")
|
|
sys.exit(0)
|
|
|
|
# Case 3: enabled: [] — replace key line with block list (keyed off enabled_idx; disabled: may precede).
|
|
if enabled_empty:
|
|
new_block = [
|
|
f"{enabled_indent}enabled:",
|
|
new_item_line,
|
|
]
|
|
lines[enabled_idx : enabled_idx + 1] = new_block
|
|
path.write_text("\n".join(lines) + "\n")
|
|
sys.exit(0)
|
|
|
|
# Case 4: enabled: [a,b] — append inside brackets, keep trailing comment.
|
|
if inline_flow:
|
|
rendered = "[" + ", ".join(inline_items + [plugin_strip]) + "]"
|
|
lines[enabled_idx] = f"{enabled_indent}enabled: {rendered}{inline_comment}"
|
|
path.write_text("\n".join(lines) + "\n")
|
|
sys.exit(0)
|
|
|
|
# Case 5: block-style enabled: key with no items yet.
|
|
if not item_indent:
|
|
lines.insert(enabled_idx + 1, new_item_line)
|
|
path.write_text("\n".join(lines) + "\n")
|
|
sys.exit(0)
|
|
|
|
# Case 6: append at end of enabled block at item indent, never past enabled_end; normalize mismatched indents (#879).
|
|
insert_at = None
|
|
for idx in range(enabled_end - 1, enabled_idx, -1):
|
|
l = lines[idx]
|
|
stripped = l.strip()
|
|
if stripped.startswith("-"):
|
|
if l != item_indent + stripped:
|
|
lines[idx] = item_indent + stripped
|
|
insert_at = idx + 1
|
|
break
|
|
# Fallback: insert under the enabled: key (shouldn't happen after Case 5).
|
|
if insert_at is None:
|
|
insert_at = enabled_idx + 1
|
|
lines.insert(insert_at, new_item_line)
|
|
path.write_text("\n".join(lines) + "\n")
|
|
PY
|
|
if [[ -f "$backup" ]]; then
|
|
ok "Hermes: enabled plugin $plugin in $config (backup: $backup)"
|
|
else
|
|
ok "Hermes: created config.yaml with plugins.enabled: $plugin"
|
|
fi
|
|
}
|
|
|
|
install_hermes() {
|
|
local src="$INTEGRATIONS/hermes/agency-agents-router"
|
|
local hermes_home; hermes_home="$(hermes_home_dir)"
|
|
local dest; dest="$(resolve_dest hermes "${hermes_home}/plugins/agency-agents-router")"
|
|
# Strip trailing slashes first: basename ignores them, but `rm -rf link/`
|
|
# follows a symlink and empties its target instead of removing the link.
|
|
while [[ "$dest" == */ && "$dest" != "/" ]]; do dest="${dest%/}"; done
|
|
# HERMES_PLUGIN_DIR is ambiguous: its name invites setting it to the plugins
|
|
# parent (~/.hermes/plugins) rather than the full plugin path. Always target
|
|
# the agency-agents-router subdir so we never rm -rf a shared plugins dir that
|
|
# holds other plugins.
|
|
if [[ "$(basename "$dest")" != "agency-agents-router" ]]; then
|
|
dest="${dest%/}/agency-agents-router"
|
|
fi
|
|
[[ -f "$src/plugin.yaml" && -f "$src/__init__.py" && -f "$src/data/agents.json" ]] || {
|
|
err "integrations/hermes/agency-agents-router missing. Run ./scripts/convert.sh --tool hermes first."
|
|
return 1
|
|
}
|
|
mkdir -p "$(dirname "$dest")"
|
|
# Safety net: only ever remove our own plugin directory, never a parent.
|
|
if [[ "$(basename "$dest")" != "agency-agents-router" ]]; then
|
|
err "Hermes: refusing to remove '$dest' — expected an agency-agents-router directory."
|
|
return 1
|
|
fi
|
|
# The basename alone does not establish ownership: --path or an existing
|
|
# Hermes setup may point here with unrelated user files. Replace only a
|
|
# previous copy of this plugin, identified by its generated manifest.
|
|
if [[ -e "$dest" || -L "$dest" ]]; then
|
|
if [[ ! -f "$dest/plugin.yaml" ]] || \
|
|
! grep -Eq '^[[:space:]]*name:[[:space:]]*agency-agents-router[[:space:]]*$' "$dest/plugin.yaml"; then
|
|
err "Hermes: refusing to replace '$dest' because it is not an existing agency-agents-router plugin."
|
|
return 1
|
|
fi
|
|
fi
|
|
# A symlink (e.g. from an earlier --link install) is replaced, never followed.
|
|
if [[ -L "$dest" ]]; then
|
|
rm -f -- "$dest"
|
|
else
|
|
rm -rf -- "$dest"
|
|
fi
|
|
if $USE_LINK; then
|
|
ln -s "$src" "$dest"
|
|
else
|
|
cp -R "$src" "$dest"
|
|
fi
|
|
ensure_hermes_plugin_enabled || warn "Hermes: plugin installed but config.yaml was not updated."
|
|
local count
|
|
count="$(python3 - "$src/data/agents.json" <<'PY'
|
|
from pathlib import Path
|
|
import json, sys
|
|
print(len(json.loads(Path(sys.argv[1]).read_text())))
|
|
PY
|
|
)"
|
|
ok "Hermes: lazy-router plugin ($count agents on disk) -> $dest"
|
|
warn "Hermes: restart sessions/gateway so the new plugin toolset is discovered."
|
|
if $SELECTION_ACTIVE; then
|
|
warn "Hermes: selection flags ignored; router keeps the full roster on disk and loads agents lazily."
|
|
fi
|
|
}
|
|
|
|
install_tool() {
|
|
ensure_converted "$1"
|
|
case "$1" in
|
|
claude-code) install_claude_code ;;
|
|
copilot) install_copilot ;;
|
|
antigravity) install_antigravity ;;
|
|
gemini-cli) install_gemini_cli ;;
|
|
opencode) install_opencode ;;
|
|
openclaw) install_openclaw ;;
|
|
cursor) install_cursor ;;
|
|
aider) install_aider ;;
|
|
windsurf) install_windsurf ;;
|
|
qwen) install_qwen ;;
|
|
zcode) install_zcode ;;
|
|
kimi) install_kimi ;;
|
|
codex) install_codex ;;
|
|
osaurus) install_osaurus ;;
|
|
hermes) install_hermes ;;
|
|
vibe) install_vibe ;;
|
|
dsh) install_dsh ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
main() {
|
|
SKIPPED_LOG="$(mktemp "${TMPDIR:-/tmp}/agency-install-skipped.XXXXXX")"
|
|
export SKIPPED_LOG
|
|
trap 'rm -f "$SKIPPED_LOG"' EXIT
|
|
local tool="all"
|
|
local interactive_mode="auto"
|
|
local use_parallel=false
|
|
local parallel_jobs
|
|
parallel_jobs="$(parallel_jobs_default)"
|
|
|
|
local list_what=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--tool) tool="${2:?'--tool requires a value'}"; shift 2; interactive_mode="no" ;;
|
|
--division)
|
|
local _d
|
|
IFS=',' read -ra _divs <<< "${2:?'--division requires a value'}"
|
|
for _d in "${_divs[@]}"; do
|
|
_d="$(printf '%s' "$_d" | xargs)"; [[ -z "$_d" ]] && continue
|
|
validate_division "$_d"; FILTER_DIVISIONS+=("$_d")
|
|
done
|
|
interactive_mode="no"; shift 2 ;;
|
|
--agent)
|
|
local _a
|
|
IFS=',' read -ra _ags <<< "${2:?'--agent requires a value'}"
|
|
for _a in "${_ags[@]}"; do
|
|
_a="$(printf '%s' "$_a" | xargs)"; [[ -n "$_a" ]] && FILTER_AGENTS+=("$_a")
|
|
done
|
|
interactive_mode="no"; shift 2 ;;
|
|
--agents-file) AGENTS_FILE="${2:?'--agents-file requires a value'}"; interactive_mode="no"; shift 2 ;;
|
|
--link) USE_LINK=true; shift ;;
|
|
--path) OVERRIDE_PATH="${2:?'--path requires a value'}"; shift 2 ;;
|
|
--no-convert) AUTO_CONVERT=false; shift ;;
|
|
--dry-run) DRY_RUN=true; interactive_mode="no"; shift ;;
|
|
--list) if [[ -z "${2:-}" || "${2:-}" == --* ]]; then list_what="all"; shift; else list_what="$2"; shift 2; fi ;;
|
|
--interactive) interactive_mode="yes"; shift ;;
|
|
--no-interactive) interactive_mode="no"; shift ;;
|
|
--parallel) use_parallel=true; shift ;;
|
|
--jobs) parallel_jobs="${2:?'--jobs requires a value'}"; shift 2 ;;
|
|
--help|-h) usage ;;
|
|
*) err "Unknown option: $1"; usage ;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$list_what" ]] && { do_list "$list_what"; exit 0; }
|
|
build_selection
|
|
|
|
check_integrations
|
|
|
|
# Validate explicit tool(s). --tool accepts a comma-separated list (like
|
|
# --division / --agent), e.g. --tool claude-code,cursor.
|
|
local _tool_list=()
|
|
if [[ "$tool" != "all" ]]; then
|
|
local _t
|
|
IFS=',' read -ra _tool_list <<< "$tool"
|
|
local _cleaned=()
|
|
for _t in "${_tool_list[@]}"; do
|
|
_t="$(printf '%s' "$_t" | xargs)"; [[ -z "$_t" ]] && continue
|
|
local valid=false _vt
|
|
for _vt in "${ALL_TOOLS[@]}"; do [[ "$_vt" == "$_t" ]] && valid=true && break; done
|
|
$valid || { err "Unknown tool '$_t'. Valid: ${ALL_TOOLS[*]}"; exit 1; }
|
|
# A repeated --tool value would otherwise launch duplicate workers in
|
|
# --parallel mode and make the reported install count misleading.
|
|
local duplicate=false _selected
|
|
if [[ ${#_cleaned[@]} -gt 0 ]]; then
|
|
for _selected in "${_cleaned[@]}"; do
|
|
[[ "$_selected" == "$_t" ]] && { duplicate=true; break; }
|
|
done
|
|
fi
|
|
$duplicate || _cleaned+=("$_t")
|
|
done
|
|
_tool_list=("${_cleaned[@]}")
|
|
fi
|
|
|
|
# Decide whether to show interactive UI
|
|
local use_interactive=false
|
|
if [[ "$interactive_mode" == "yes" ]]; then
|
|
use_interactive=true
|
|
elif [[ "$interactive_mode" == "auto" && -t 0 && -t 1 && "$tool" == "all" ]]; then
|
|
use_interactive=true
|
|
fi
|
|
|
|
SELECTED_TOOLS=()
|
|
|
|
if $use_interactive && interactive_wizard; then
|
|
: # wizard committed SELECTED_TOOLS + FILTER_DIVISIONS
|
|
|
|
elif [[ "$tool" != "all" ]]; then
|
|
SELECTED_TOOLS=("${_tool_list[@]}")
|
|
|
|
else
|
|
# Non-interactive (or no TTY): auto-detect
|
|
header "The Agency -- Scanning for installed tools..."
|
|
printf "\n"
|
|
local t
|
|
for t in "${ALL_TOOLS[@]}"; do
|
|
if is_detected "$t" 2>/dev/null; then
|
|
SELECTED_TOOLS+=("$t")
|
|
printf " ${C_GREEN}[*]${C_RESET} %s ${C_DIM}detected${C_RESET}\n" "$(tool_label "$t")"
|
|
else
|
|
printf " ${C_DIM}[ ] %s not found${C_RESET}\n" "$(tool_label "$t")"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ ${#SELECTED_TOOLS[@]} -eq 0 ]]; then
|
|
warn "No tools selected or detected. Nothing to install."
|
|
printf "\n"
|
|
dim " Tip: use --tool <name> to force-install a specific tool."
|
|
dim " Available: ${ALL_TOOLS[*]}"
|
|
exit 0
|
|
fi
|
|
|
|
# --tool all and the interactive wizard only know their selected tools now.
|
|
validate_path_collisions "${SELECTED_TOOLS[@]}"
|
|
|
|
# --dry-run: print the plan and exit without writing anything.
|
|
if $DRY_RUN; then
|
|
local agents; agents="$(selected_agent_count)"
|
|
printf "\n"; header "The Agency -- Dry run (nothing written)"
|
|
printf " Tools: %s\n" "${SELECTED_TOOLS[*]}"
|
|
if $SELECTION_ACTIVE; then
|
|
[[ ${#FILTER_DIVISIONS[@]} -gt 0 ]] && printf " Teams: %s\n" "${FILTER_DIVISIONS[*]}"
|
|
[[ ${#FILTER_AGENTS[@]} -gt 0 ]] && printf " Agents: %s\n" "${FILTER_AGENTS[*]}"
|
|
[[ -n "$AGENTS_FILE" ]] && printf " File: %s\n" "$AGENTS_FILE"
|
|
else
|
|
printf " Teams: all (%s)\n" "${#ALL_DIVISIONS[@]}"
|
|
fi
|
|
printf " Agents: %s Mode: %s\n" "$agents" "$($USE_LINK && echo symlink || echo copy)"
|
|
local _t _cap
|
|
for _t in "${SELECTED_TOOLS[@]}"; do
|
|
_cap="$(tool_cap "$_t")"
|
|
[[ "$_cap" -gt 0 && "$agents" -gt "$_cap" ]] && \
|
|
warn "$_t caps ~$_cap — ~$(( agents - _cap )) of $agents won't register (anomalyco/opencode#27988)"
|
|
done
|
|
printf "\n"; exit 0
|
|
fi
|
|
|
|
# When parent runs install.sh --parallel, it spawns workers with AGENCY_INSTALL_WORKER=1
|
|
# so each worker only runs install_tool(s) and skips header/done box (avoids duplicate output).
|
|
if [[ -n "${AGENCY_INSTALL_WORKER:-}" ]]; then
|
|
local t
|
|
for t in "${SELECTED_TOOLS[@]}"; do
|
|
install_tool "$t"
|
|
done
|
|
return 0
|
|
fi
|
|
|
|
printf "\n"
|
|
header "The Agency -- Installing agents"
|
|
printf " Repo: %s\n" "$REPO_ROOT"
|
|
local n_selected=${#SELECTED_TOOLS[@]}
|
|
printf " Installing: %s\n" "${SELECTED_TOOLS[*]}"
|
|
if $SELECTION_ACTIVE; then
|
|
[[ ${#FILTER_DIVISIONS[@]} -gt 0 ]] && printf " Teams: %s\n" "${FILTER_DIVISIONS[*]}"
|
|
printf " Agents: %s of %s\n" "$(selected_agent_count)" "$(selected_agent_count_all)"
|
|
fi
|
|
$USE_LINK && printf " Mode: ${C_CYAN}symlink${C_RESET} (--link)\n"
|
|
if $use_parallel; then
|
|
ok "Installing $n_selected tools in parallel (output buffered per tool)."
|
|
fi
|
|
printf "\n"
|
|
|
|
local installed=0 t i=0
|
|
if $use_parallel; then
|
|
local install_out_dir
|
|
install_out_dir="$(mktemp -d)"
|
|
export AGENCY_INSTALL_OUT_DIR="$install_out_dir"
|
|
export AGENCY_INSTALL_SCRIPT="$SCRIPT_DIR/install.sh"
|
|
export AGENCY_INSTALL_EXTRA="$(worker_flags)"
|
|
printf '%s\n' "${SELECTED_TOOLS[@]}" | xargs -P "$parallel_jobs" -I {} sh -c 'AGENCY_INSTALL_WORKER=1 "$AGENCY_INSTALL_SCRIPT" --tool "{}" --no-interactive $AGENCY_INSTALL_EXTRA > "$AGENCY_INSTALL_OUT_DIR/{}" 2>&1'
|
|
for t in "${SELECTED_TOOLS[@]}"; do
|
|
[[ -f "$install_out_dir/$t" ]] && cat "$install_out_dir/$t"
|
|
done
|
|
rm -rf "$install_out_dir"
|
|
installed=$n_selected
|
|
else
|
|
for t in "${SELECTED_TOOLS[@]}"; do
|
|
(( i++ )) || true
|
|
progress_bar "$i" "$n_selected"
|
|
printf "\n"
|
|
printf " ${C_DIM}[%s/%s]${C_RESET} %s\n" "$i" "$n_selected" "$t"
|
|
install_tool "$t"
|
|
(( installed++ )) || true
|
|
done
|
|
fi
|
|
|
|
# Done box
|
|
local msg=" Done! Installed $installed tool(s)."
|
|
printf "\n"
|
|
box_top
|
|
box_row "${C_GREEN}${C_BOLD}${msg}${C_RESET}"
|
|
box_bot
|
|
printf "\n"
|
|
if [[ -s "$SKIPPED_LOG" ]]; then
|
|
warn "Not installed: $(wc -l < "$SKIPPED_LOG" | tr -d ' ') file(s) whose destination is a symlink to somewhere else:"
|
|
sed 's/^/ /' "$SKIPPED_LOG" >&2
|
|
warn "Remove or replace those links, then re-run to install them."
|
|
fi
|
|
dim " Run ./scripts/convert.sh to regenerate after adding or editing agents."
|
|
printf "\n"
|
|
}
|
|
|
|
main "$@"
|