install.sh 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324
  1. #!/usr/bin/env bash
  2. #
  3. # --- USAGE-START --- (sentinel for usage(); do not remove)
  4. # install.sh -- Install The Agency agents into your local agentic tool(s).
  5. #
  6. # Reads converted files from integrations/ and copies them to the appropriate
  7. # config directory for each tool. Run scripts/convert.sh first if integrations/
  8. # is missing or stale.
  9. #
  10. # Usage:
  11. # ./scripts/install.sh [selection] [mode] [behavior]
  12. # Bare invocation installs all teams to detected tools (interactive when a TTY).
  13. #
  14. # Tools:
  15. # claude-code -- Copy agents to ~/.claude/agents/
  16. # copilot -- Copy agents to ~/.github/agents/ and ~/.copilot/agents/
  17. # antigravity -- Copy skills to ~/.gemini/config/skills/
  18. # gemini-cli -- Install agents to ~/.gemini/agents/
  19. # opencode -- Copy agents to .opencode/agents/ in current directory
  20. # cursor -- Copy rules to .cursor/rules/ in current directory
  21. # aider -- Copy CONVENTIONS.md to current directory
  22. # windsurf -- Copy .windsurfrules to current directory
  23. # openclaw -- Copy workspaces to ~/.openclaw/agency-agents/
  24. # qwen -- Copy SubAgents to ~/.qwen/agents/ (user-wide) or .qwen/agents/ (project)
  25. # codex -- Copy custom agent TOML files to ~/.codex/agents/
  26. # osaurus -- Copy skills to ~/.osaurus/skills/
  27. # hermes -- Copy lazy-router plugin to ~/.hermes/plugins/ and enable it
  28. # vibe -- Copy agents and prompts to ~/.vibe/agents/ and ~/.vibe/prompts/
  29. # all -- Install for all detected tools (default)
  30. #
  31. # Selection (compose freely; empty = everything):
  32. # --tool <a,b> Only these tools
  33. # --division <a,b> Only these teams/divisions (comma-separated)
  34. # --agent <slug,slug> Only these specific agents
  35. # --agents-file <path> Agents listed in a file (one slug/name per line, # comments ok)
  36. #
  37. # Mode:
  38. # --link Symlink instead of copy (updates propagate)
  39. # --path <dir> Override the install directory (single destination)
  40. #
  41. # Behavior:
  42. # --interactive Show the interactive wizard (default when run in a terminal)
  43. # --no-interactive Skip the wizard, install all detected tools
  44. # --no-convert Don't auto-run convert.sh when integration files are missing
  45. # --dry-run Print the plan and exit without writing anything
  46. # --list [tools|teams|agents] List and exit
  47. # --parallel Install tools in parallel (output buffered per tool)
  48. # --jobs N Max parallel jobs (default: nproc or 4)
  49. # --help Show this help
  50. #
  51. # Env: CLAUDE_CONFIG_DIR, COPILOT_AGENT_DIR, CURSOR_RULES_DIR, GEMINI_AGENTS_DIR,
  52. # OPENCODE_AGENTS_DIR, OPENCLAW_DIR, QWEN_AGENTS_DIR, CODEX_AGENTS_DIR,
  53. # OSAURUS_SKILLS_DIR, HERMES_HOME, HERMES_PLUGIN_DIR, VIBE_HOME
  54. # override default install paths (checked before hardcoded defaults).
  55. #
  56. # --- USAGE-END --- (sentinel for usage(); do not remove)
  57. # Platform support:
  58. # Linux, macOS (requires bash 3.2+), Windows Git Bash / WSL
  59. set -euo pipefail
  60. # ---------------------------------------------------------------------------
  61. # Colours -- only when stdout supports color
  62. # ---------------------------------------------------------------------------
  63. if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then
  64. C_GREEN=$'\033[0;32m'
  65. C_YELLOW=$'\033[1;33m'
  66. C_RED=$'\033[0;31m'
  67. C_CYAN=$'\033[0;36m'
  68. C_BOLD=$'\033[1m'
  69. C_DIM=$'\033[2m'
  70. C_RESET=$'\033[0m'
  71. else
  72. C_GREEN=''; C_YELLOW=''; C_RED=''; C_CYAN=''; C_BOLD=''; C_DIM=''; C_RESET=''
  73. fi
  74. ok() { printf "${C_GREEN}[OK]${C_RESET} %s\n" "$*"; }
  75. warn() { printf "${C_YELLOW}[!!]${C_RESET} %s\n" "$*"; }
  76. err() { printf "${C_RED}[ERR]${C_RESET} %s\n" "$*" >&2; }
  77. header() { printf "\n${C_BOLD}%s${C_RESET}\n" "$*"; }
  78. dim() { printf "${C_DIM}%s${C_RESET}\n" "$*"; }
  79. # Progress bar: [=======> ] 3/8 (tqdm-style)
  80. progress_bar() {
  81. local current="$1" total="$2" width="${3:-20}" i filled empty
  82. (( total > 0 )) || return
  83. filled=$(( width * current / total ))
  84. empty=$(( width - filled ))
  85. printf "\r ["
  86. for (( i=0; i<filled; i++ )); do printf "="; done
  87. if (( filled < width )); then printf ">"; (( empty-- )); fi
  88. for (( i=0; i<empty; i++ )); do printf " "; done
  89. printf "] %s/%s" "$current" "$total"
  90. [[ -t 1 ]] || printf "\n"
  91. }
  92. # ---------------------------------------------------------------------------
  93. # Box drawing -- pure ASCII, fixed 52-char wide
  94. # box_top / box_mid / box_bot -- structural lines
  95. # box_row <text> -- content row, right-padded to fit
  96. # ---------------------------------------------------------------------------
  97. BOX_INNER=48 # chars between the two | walls
  98. box_top() { printf " +"; printf '%0.s-' $(seq 1 $BOX_INNER); printf "+\n"; }
  99. box_bot() { box_top; }
  100. box_sep() { printf " |"; printf '%0.s-' $(seq 1 $BOX_INNER); printf "|\n"; }
  101. strip_ansi() {
  102. awk '{ gsub(/\033\[[0-9;]*m/, ""); print }' <<< "$1"
  103. }
  104. box_row() {
  105. # Strip ANSI escapes when measuring visible length
  106. local raw="$1"
  107. local visible
  108. visible="$(strip_ansi "$raw")"
  109. local pad=$(( BOX_INNER - 2 - ${#visible} ))
  110. if (( pad < 0 )); then pad=0; fi
  111. printf " | %s%*s |\n" "$raw" "$pad" ''
  112. }
  113. box_blank() { printf " |%*s|\n" $BOX_INNER ''; }
  114. # ---------------------------------------------------------------------------
  115. # Paths
  116. # ---------------------------------------------------------------------------
  117. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  118. REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
  119. INTEGRATIONS="$REPO_ROOT/integrations"
  120. # Shared helpers (get_field, agent_slug, slugify, incr, ANSI + TUI primitives)
  121. # shellcheck source=lib.sh
  122. . "$SCRIPT_DIR/lib.sh"
  123. ALL_TOOLS=(claude-code copilot antigravity gemini-cli opencode openclaw cursor aider windsurf qwen kimi codex osaurus hermes vibe)
  124. # The division set is derived from divisions.json (the single source of truth)
  125. # so the installer can never drift from the catalog — a hardcoded copy silently
  126. # dropped healthcare (#655/#668) and can't be seen by check-divisions.sh. Same
  127. # no-jq awk/grep/sed parse as scripts/check-divisions.sh (macOS + Linux).
  128. divisions_from_json() {
  129. local json="$REPO_ROOT/divisions.json"
  130. [[ -f "$json" ]] || { err "divisions.json not found at $json"; exit 1; }
  131. awk '/"divisions"[[:space:]]*:[[:space:]]*\{/{f=1; next} f' "$json" \
  132. | grep -oE '"[a-z0-9-]+"[[:space:]]*:[[:space:]]*\{' \
  133. | sed -E 's/"([a-z0-9-]+)".*/\1/'
  134. }
  135. # Selectable divisions = exactly the divisions.json entries.
  136. ALL_DIVISIONS=()
  137. while IFS= read -r _div; do [[ -n "$_div" ]] && ALL_DIVISIONS+=("$_div"); done < <(divisions_from_json)
  138. [[ ${#ALL_DIVISIONS[@]} -gt 0 ]] || { err "no divisions parsed from divisions.json"; exit 1; }
  139. # Directories scanned for installable agents = the divisions plus strategy/.
  140. # strategy/ holds frontmatter-less NEXUS docs (filtered out by is_agent_file at
  141. # scan time), so it is scanned but selectable only via ALL_DIVISIONS above.
  142. AGENT_DIRS=("${ALL_DIVISIONS[@]}" strategy)
  143. # ---------------------------------------------------------------------------
  144. # Selection engine (team / agent / agents-file filtering)
  145. # ---------------------------------------------------------------------------
  146. FILTER_DIVISIONS=() # --division
  147. FILTER_AGENTS=() # --agent
  148. AGENTS_FILE="" # --agents-file
  149. DRY_RUN=false # --dry-run
  150. SELECTION_ACTIVE=false # true once any agent-level filter is applied
  151. _ALLOWED_SLUGS="" # newline-delimited cache of allowed slugs
  152. # division_files <division> — agent file paths (frontmatter only) in a division.
  153. division_files() {
  154. local d="$REPO_ROOT/$1" f
  155. [[ -d "$d" ]] || return 0
  156. while IFS= read -r -d '' f; do
  157. is_agent_file "$f" && printf '%s\n' "$f"
  158. done < <(find "$d" -name "*.md" -type f -print0 2>/dev/null)
  159. }
  160. # division_count <division> — number of agents in a division.
  161. division_count() { division_files "$1" | grep -c . ; }
  162. # build_selection — compute the allowed slug set from --division/--agent/--agents-file.
  163. # With no filter flags, SELECTION_ACTIVE stays false (install everything).
  164. build_selection() {
  165. if [[ ${#FILTER_DIVISIONS[@]} -eq 0 && ${#FILTER_AGENTS[@]} -eq 0 && -z "$AGENTS_FILE" ]]; then
  166. SELECTION_ACTIVE=false
  167. return
  168. fi
  169. SELECTION_ACTIVE=true
  170. local slugs="" div f s line
  171. for div in ${FILTER_DIVISIONS[@]+"${FILTER_DIVISIONS[@]}"}; do
  172. while IFS= read -r f; do
  173. s="$(agent_slug "$f")"; [[ -n "$s" ]] && slugs+="$s"$'\n'
  174. done < <(division_files "$div")
  175. done
  176. for s in ${FILTER_AGENTS[@]+"${FILTER_AGENTS[@]}"}; do slugs+="$(slugify "$s")"$'\n'; done
  177. if [[ -n "$AGENTS_FILE" ]]; then
  178. [[ -f "$AGENTS_FILE" ]] || { err "agents-file not found: $AGENTS_FILE"; exit 1; }
  179. while IFS= read -r line || [[ -n "$line" ]]; do
  180. line="${line%%#*}" # strip trailing comment
  181. line="$(printf '%s' "$line" | xargs 2>/dev/null)" # trim
  182. [[ -z "$line" ]] && continue
  183. slugs+="$(slugify "$line")"$'\n'
  184. done < "$AGENTS_FILE"
  185. fi
  186. _ALLOWED_SLUGS="$(printf '%s' "$slugs" | sort -u | sed '/^$/d')"
  187. }
  188. # slug_allowed <slug> — true if installable under the active selection
  189. # (always true when no filter). Tolerates the antigravity "agency-" prefix.
  190. slug_allowed() {
  191. $SELECTION_ACTIVE || return 0
  192. local s="${1#agency-}"
  193. printf '%s\n' "$_ALLOWED_SLUGS" | grep -qxF "$s"
  194. }
  195. # selected_agent_count — how many agents the current selection installs.
  196. selected_agent_count() {
  197. if ! $SELECTION_ACTIVE; then
  198. local d n=0; for d in "${ALL_DIVISIONS[@]}"; do incr_by n "$(division_count "$d")"; done; echo "$n"
  199. else
  200. printf '%s\n' "$_ALLOWED_SLUGS" | grep -c .
  201. fi
  202. }
  203. incr_by() { printf -v "$1" '%d' "$(( ${!1:-0} + ${2:-0} ))"; }
  204. # selected_agent_count_all — total agents across all divisions (ignores filter).
  205. selected_agent_count_all() {
  206. local d n=0; for d in "${ALL_DIVISIONS[@]}"; do incr_by n "$(division_count "$d")"; done; echo "$n"
  207. }
  208. # worker_flags — re-emit the active selection/mode flags for parallel workers.
  209. worker_flags() {
  210. local out="" d a
  211. $USE_LINK && out="$out --link"
  212. $AUTO_CONVERT || out="$out --no-convert"
  213. [[ -n "$OVERRIDE_PATH" ]] && out="$out --path $OVERRIDE_PATH"
  214. for d in ${FILTER_DIVISIONS[@]+"${FILTER_DIVISIONS[@]}"}; do out="$out --division $d"; done
  215. for a in ${FILTER_AGENTS[@]+"${FILTER_AGENTS[@]}"}; do out="$out --agent $a"; done
  216. [[ -n "$AGENTS_FILE" ]] && out="$out --agents-file $AGENTS_FILE"
  217. printf '%s' "$out"
  218. }
  219. # validate_division <name> — exit on unknown division.
  220. validate_division() {
  221. local _ad
  222. for _ad in "${ALL_DIVISIONS[@]}"; do [[ "$_ad" == "$1" ]] && return 0; done
  223. err "Unknown division '$1'. Valid: ${ALL_DIVISIONS[*]}"
  224. exit 1
  225. }
  226. # ---------------------------------------------------------------------------
  227. # Install mechanics (copy vs symlink, path override, capacity guard)
  228. # ---------------------------------------------------------------------------
  229. USE_LINK=false # --link
  230. OVERRIDE_PATH="" # --path (single-destination override)
  231. # install_file <src> <dest> — copy, or symlink when --link is set.
  232. install_file() {
  233. if $USE_LINK; then ln -sf "$1" "$2"; else cp "$1" "$2"; fi
  234. }
  235. # resolve_dest <tool> <default> — --path > $ENV_VAR > default.
  236. resolve_dest() {
  237. local tool="$1" def="$2" var=""
  238. [[ -n "$OVERRIDE_PATH" ]] && { printf '%s' "$OVERRIDE_PATH"; return; }
  239. case "$tool" in
  240. claude-code) var="CLAUDE_CONFIG_DIR" ;;
  241. copilot) var="COPILOT_AGENT_DIR" ;;
  242. cursor) var="CURSOR_RULES_DIR" ;;
  243. gemini-cli) var="GEMINI_AGENTS_DIR" ;;
  244. opencode) var="OPENCODE_AGENTS_DIR" ;;
  245. openclaw) var="OPENCLAW_DIR" ;;
  246. qwen) var="QWEN_AGENTS_DIR" ;;
  247. codex) var="CODEX_AGENTS_DIR" ;;
  248. osaurus) var="OSAURUS_SKILLS_DIR" ;;
  249. hermes) var="HERMES_PLUGIN_DIR" ;;
  250. vibe) var="VIBE_HOME" ;;
  251. esac
  252. if [[ -n "$var" && -n "${!var:-}" ]]; then printf '%s' "${!var}"; else printf '%s' "$def"; fi
  253. }
  254. # resolve_tool_path <tool> — best-effort binary path for the detection UI.
  255. resolve_tool_path() {
  256. local bin=""
  257. case "$1" in
  258. claude-code) bin="claude" ;; copilot) bin="code" ;; gemini-cli) bin="gemini" ;;
  259. opencode) bin="opencode" ;; openclaw) bin="openclaw" ;; cursor) bin="cursor" ;;
  260. aider) bin="aider" ;; windsurf) bin="windsurf" ;; qwen) bin="qwen" ;;
  261. kimi) bin="kimi" ;; codex) bin="codex" ;; antigravity) bin="" ;;
  262. osaurus) bin="osaurus" ;; hermes) bin="hermes" ;; vibe) bin="vibe" ;;
  263. esac
  264. [[ -n "$bin" ]] && command -v "$bin" 2>/dev/null
  265. }
  266. # ensure_converted <tool> — auto-run convert.sh if a converted tool's output
  267. # is missing (absorbs #426). No-op for source tools and when --no-convert.
  268. ensure_converted() {
  269. local tool="$1"
  270. $AUTO_CONVERT || return 0
  271. case "$tool" in claude-code|copilot) return 0 ;; esac
  272. local d="$INTEGRATIONS/$tool"
  273. if [[ ! -d "$d" ]] || [[ -z "$(find "$d" -type f 2>/dev/null | head -1)" ]]; then
  274. warn "$tool: integration files missing — running convert.sh --tool $tool"
  275. "$SCRIPT_DIR/convert.sh" --tool "$tool" >/dev/null 2>&1 \
  276. && ok "$tool: generated integration files" \
  277. || err "$tool: convert.sh failed; run it manually"
  278. fi
  279. }
  280. AUTO_CONVERT=true # --no-convert disables
  281. # Per-tool soft capacity (opencode silently drops past ~119 — upstream #27988).
  282. tool_cap() { case "$1" in opencode) echo 119 ;; *) echo 0 ;; esac; }
  283. # capacity_warn <tool> <count> — warn if a tool can't register this many.
  284. capacity_warn() {
  285. local cap; cap="$(tool_cap "$1")"
  286. if [[ "$cap" -gt 0 && "$2" -gt "$cap" ]]; then
  287. warn "$1: registers only ~$cap agents (upstream bug anomalyco/opencode#27988)."
  288. warn " You selected $2 — ~$(( $2 - cap )) won't load. Narrow with --division to fix."
  289. fi
  290. }
  291. # do_list <what> — print tools/teams/agents and exit.
  292. do_list() {
  293. case "$1" in
  294. tools)
  295. printf '%s\n' "${ALL_TOOLS[@]}" ;;
  296. teams|divisions)
  297. local d; for d in "${ALL_DIVISIONS[@]}"; do printf '%-22s %3s agents\n' "$d" "$(division_count "$d")"; done ;;
  298. agents)
  299. local d f; for d in "${ALL_DIVISIONS[@]}"; do
  300. while IFS= read -r f; do printf '%-20s %s\n' "$d" "$(agent_slug "$f")"; done < <(division_files "$d")
  301. done ;;
  302. *)
  303. echo "Tools (${#ALL_TOOLS[@]}):"; printf ' %s\n' "${ALL_TOOLS[@]}"; echo
  304. echo "Teams (${#ALL_DIVISIONS[@]}):"
  305. local d; for d in "${ALL_DIVISIONS[@]}"; do printf ' %-22s %3s agents\n' "$d" "$(division_count "$d")"; done ;;
  306. esac
  307. }
  308. # ---------------------------------------------------------------------------
  309. # Usage
  310. # ---------------------------------------------------------------------------
  311. usage() {
  312. # Extract everything between the USAGE-START / USAGE-END sentinels
  313. # (excluding the sentinel lines themselves) and strip the leading "# ".
  314. # Using sentinels instead of hard-coded line numbers means adding lines
  315. # to the header comment block won't silently break --help output.
  316. sed -n '/^# --- USAGE-START ---/,/^# --- USAGE-END ---/p' "$0" \
  317. | sed -e '1d;$d' -e 's/^# \{0,1\}//'
  318. exit 0
  319. }
  320. # Default parallel job count (nproc on Linux; sysctl on macOS when nproc missing)
  321. parallel_jobs_default() {
  322. local n
  323. n=$(nproc 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
  324. n=$(sysctl -n hw.ncpu 2>/dev/null) && [[ -n "$n" ]] && echo "$n" && return
  325. echo 4
  326. }
  327. # ---------------------------------------------------------------------------
  328. # Preflight
  329. # ---------------------------------------------------------------------------
  330. check_integrations() {
  331. if [[ ! -d "$INTEGRATIONS" ]]; then
  332. err "integrations/ not found. Run ./scripts/convert.sh first."
  333. exit 1
  334. fi
  335. }
  336. # ---------------------------------------------------------------------------
  337. # Tool detection
  338. # ---------------------------------------------------------------------------
  339. detect_claude_code() { [[ -d "${HOME}/.claude" ]]; }
  340. detect_copilot() { command -v code >/dev/null 2>&1 || [[ -d "${HOME}/.github" || -d "${HOME}/.copilot" ]]; }
  341. detect_antigravity() { [[ -d "${HOME}/.gemini/config/skills" ]]; }
  342. detect_gemini_cli() { command -v gemini >/dev/null 2>&1 || [[ -d "${HOME}/.gemini" ]]; }
  343. detect_cursor() { command -v cursor >/dev/null 2>&1 || [[ -d "${HOME}/.cursor" ]]; }
  344. detect_opencode() { command -v opencode >/dev/null 2>&1 || [[ -d "${HOME}/.config/opencode" ]]; }
  345. detect_aider() { command -v aider >/dev/null 2>&1; }
  346. detect_openclaw() { command -v openclaw >/dev/null 2>&1 || [[ -d "${HOME}/.openclaw" ]]; }
  347. detect_windsurf() { command -v windsurf >/dev/null 2>&1 || [[ -d "${HOME}/.codeium" ]]; }
  348. detect_qwen() { command -v qwen >/dev/null 2>&1 || [[ -d "${HOME}/.qwen" ]]; }
  349. detect_kimi() { command -v kimi >/dev/null 2>&1; }
  350. detect_codex() { command -v codex >/dev/null 2>&1 || [[ -d "${HOME}/.codex" ]]; }
  351. detect_osaurus() { command -v osaurus >/dev/null 2>&1 || [[ -d "${HOME}/.osaurus" ]]; }
  352. detect_hermes() { command -v hermes >/dev/null 2>&1 || [[ -d "${HERMES_HOME:-${HOME}/.hermes}" ]]; }
  353. detect_vibe() { command -v vibe >/dev/null 2>&1 || [[ -d "${VIBE_HOME:-${HOME}/.vibe}" ]]; }
  354. is_detected() {
  355. case "$1" in
  356. claude-code) detect_claude_code ;;
  357. copilot) detect_copilot ;;
  358. antigravity) detect_antigravity ;;
  359. gemini-cli) detect_gemini_cli ;;
  360. opencode) detect_opencode ;;
  361. openclaw) detect_openclaw ;;
  362. cursor) detect_cursor ;;
  363. aider) detect_aider ;;
  364. windsurf) detect_windsurf ;;
  365. qwen) detect_qwen ;;
  366. kimi) detect_kimi ;;
  367. codex) detect_codex ;;
  368. osaurus) detect_osaurus ;;
  369. hermes) detect_hermes ;;
  370. vibe) detect_vibe ;;
  371. *) return 1 ;;
  372. esac
  373. }
  374. # Fixed-width labels: name (14) + detail (24) = 38 visible chars
  375. tool_label() {
  376. case "$1" in
  377. claude-code) printf "%-14s %s" "Claude Code" "(claude.ai/code)" ;;
  378. copilot) printf "%-14s %s" "Copilot" "(~/.github + ~/.copilot)" ;;
  379. antigravity) printf "%-14s %s" "Antigravity" "(~/.gemini/config/skills)" ;;
  380. gemini-cli) printf "%-14s %s" "Gemini CLI" "(~/.gemini/agents)" ;;
  381. opencode) printf "%-14s %s" "OpenCode" "(opencode.ai)" ;;
  382. openclaw) printf "%-14s %s" "OpenClaw" "(~/.openclaw/agency-agents)" ;;
  383. cursor) printf "%-14s %s" "Cursor" "(.cursor/rules)" ;;
  384. aider) printf "%-14s %s" "Aider" "(CONVENTIONS.md)" ;;
  385. windsurf) printf "%-14s %s" "Windsurf" "(.windsurfrules)" ;;
  386. qwen) printf "%-14s %s" "Qwen Code" "(~/.qwen/agents)" ;;
  387. kimi) printf "%-14s %s" "Kimi Code" "(~/.config/kimi/agents)" ;;
  388. codex) printf "%-14s %s" "Codex" "(~/.codex/agents)" ;;
  389. osaurus) printf "%-14s %s" "Osaurus" "(~/.osaurus/skills)" ;;
  390. hermes) printf "%-14s %s" "Hermes" "(~/.hermes/plugins)" ;;
  391. vibe) printf "%-14s %s" "Mistral Vibe" "(~/.vibe/agents)" ;;
  392. esac
  393. }
  394. # ---------------------------------------------------------------------------
  395. # Interactive selector
  396. # ---------------------------------------------------------------------------
  397. # ---------------------------------------------------------------------------
  398. # Interactive wizard (pure-bash TUI): Tools -> Teams -> Review -> install
  399. # Uses lib.sh primitives (tui_begin/read_key/draw_frame). Falls back to the
  400. # legacy auto-detect path when there is no TTY.
  401. # ---------------------------------------------------------------------------
  402. # Persistent selection state across screens.
  403. TOOL_SEL=() # 1/0 per ALL_TOOLS
  404. TEAM_SEL=() # 1/0 per ALL_DIVISIONS
  405. # division_emoji <div> — a glyph for the team list (unicode only).
  406. division_emoji() {
  407. if ! supports_unicode; then printf '*'; return; fi
  408. case "$1" in
  409. academic) printf '📚';; design) printf '🎨';; engineering) printf '💻';;
  410. finance) printf '💵';; game-development) printf '🎮';; gis) printf '🌍';; marketing) printf '📢';;
  411. paid-media) printf '💰';; product) printf '📊';; project-management) printf '🎬';;
  412. sales) printf '💼';; security) printf '🔒';; spatial-computing) printf '🥽';;
  413. specialized) printf '🎯';; support) printf '🛟';; testing) printf '🧪';; *) printf '•';;
  414. esac
  415. }
  416. # Generic multi-select. Inputs (globals): OPT_LABEL[], OPT_SEL[];
  417. # SEL_TITLE, SEL_HINT, SEL_SUMMARY_FN, SEL_NAV, SEL_WARN_FN.
  418. # Mutates OPT_SEL[]; sets SEL_RESULT = next|back|quit.
  419. selector() {
  420. local n=${#OPT_LABEL[@]} cur=0 top=0 query="" searching=false key i idx vn rows W
  421. rows=$(( $(term_rows) - 9 )); (( rows < 3 )) && rows=3
  422. while true; do
  423. local view=() qlc
  424. qlc="$(printf '%s' "$query" | tr '[:upper:]' '[:lower:]')"
  425. for (( i=0; i<n; i++ )); do
  426. if [[ -z "$query" || "$(printf '%s' "${OPT_LABEL[$i]}" | tr '[:upper:]' '[:lower:]')" == *"$qlc"* ]]; then
  427. view+=("$i")
  428. fi
  429. done
  430. vn=${#view[@]}
  431. (( cur>=vn )) && cur=$(( vn>0 ? vn-1 : 0 ))
  432. (( cur<top )) && top=$cur
  433. (( cur>=top+rows )) && top=$(( cur-rows+1 ))
  434. W=$(( $(term_cols) - 4 )); (( W>74 )) && W=74; (( W<40 )) && W=40
  435. local buf="" hlen=$(( W - ${#SEL_TITLE} - 5 )); (( hlen<1 )) && hlen=1
  436. buf+=" ${C_BOLD}${C_CYAN}${BX_TL}${BX_H}${BX_H} ${SEL_TITLE} $(repeat "$BX_H" "$hlen")${BX_TR}${C_RESET}"$'\n'
  437. buf+=" ${C_DIM}${SEL_HINT}${C_RESET}"$'\n\n'
  438. (( vn==0 )) && buf+=" ${C_DIM}(no matches)${C_RESET}"$'\n'
  439. for (( i=top; i<top+rows && i<vn; i++ )); do
  440. idx=${view[$i]}
  441. local mark cg label="${OPT_LABEL[$idx]}"
  442. [[ "${OPT_SEL[$idx]}" == 1 ]] && mark="${C_GREEN}${GLYPH_ON}${C_RESET}" || mark="${C_DIM}·${C_RESET}"
  443. if (( i==cur )); then cg="${C_CYAN}${GLYPH_CUR}${C_RESET}"; label="${C_BOLD}${label}${C_RESET}"; else cg=" "; fi
  444. buf+=" $cg [$mark] $label"$'\n'
  445. done
  446. local shown=$(( vn<rows ? vn : rows )); for (( i=shown; i<rows; i++ )); do buf+=$'\n'; done
  447. # Consistent footer: summary -> nav -> warnings (-> search line)
  448. buf+=$'\n'" ${C_BOLD}$("$SEL_SUMMARY_FN")${C_RESET}"$'\n'
  449. buf+=" ${C_DIM}${SEL_NAV}${C_RESET}"$'\n'
  450. local _w; _w="$("$SEL_WARN_FN")"; [[ -n "$_w" ]] && buf+=" ${C_YELLOW}${_w}${C_RESET}"$'\n'
  451. if $searching; then buf+=" ${C_CYAN}search:${C_RESET} ${query}_"$'\n'
  452. elif [[ -n "$query" ]]; then buf+=" ${C_CYAN}/${query}${C_RESET} ${C_DIM}(esc clears)${C_RESET}"$'\n'; fi
  453. draw_frame "$buf"
  454. key="$(read_key)"
  455. if $searching; then
  456. case "$key" in
  457. ENTER) searching=false ;;
  458. ESC) query=""; searching=false ;;
  459. BACKSPACE) query="${query%?}" ;;
  460. *) [[ ${#key} -eq 1 ]] && query="$query$key" ;;
  461. esac
  462. continue
  463. fi
  464. case "$key" in
  465. UP|k) (( cur>0 )) && cur=$(( cur-1 )) ;;
  466. DOWN|j) (( cur<vn-1 )) && cur=$(( cur+1 )) ;;
  467. SPACE) (( vn>0 )) && { idx=${view[$cur]}; OPT_SEL[$idx]=$(( 1 - ${OPT_SEL[$idx]} )); } ;;
  468. a|A) for (( i=0; i<n; i++ )); do OPT_SEL[$i]=1; done ;;
  469. n|N) for (( i=0; i<n; i++ )); do OPT_SEL[$i]=0; done ;;
  470. /) searching=true ;;
  471. ENTER|RIGHT) SEL_RESULT=next; return ;;
  472. LEFT) SEL_RESULT=back; return ;;
  473. ESC) [[ -n "$query" ]] && query="" || { SEL_RESULT=back; return; } ;;
  474. q|Q) SEL_RESULT=quit; return ;;
  475. EOF) SEL_RESULT=quit; return ;;
  476. esac
  477. done
  478. }
  479. # --- Screen: Tools ---
  480. _no_warn() { :; }
  481. _tools_summary() {
  482. local i c=0; for (( i=0; i<${#OPT_SEL[@]}; i++ )); do [[ "${OPT_SEL[$i]}" == 1 ]] && c=$(( c+1 )); done
  483. printf '%s of %s tools selected' "$c" "${#OPT_SEL[@]}"
  484. }
  485. screen_tools() {
  486. OPT_LABEL=(); OPT_SEL=()
  487. local i det path label
  488. for (( i=0; i<${#ALL_TOOLS[@]}; i++ )); do
  489. local t="${ALL_TOOLS[$i]}"
  490. path="$(resolve_tool_path "$t" 2>/dev/null || true)"
  491. if is_detected "$t" 2>/dev/null; then det="${C_GREEN}${GLYPH_DET}${C_RESET}"; else det="${C_DIM}${GLYPH_OFF}${C_RESET}"; fi
  492. label="$(printf '%s %-13s %s' "$det" "$(tool_simple_name "$t")" "${C_DIM}${path:-not found}${C_RESET}")"
  493. OPT_LABEL+=("$label"); OPT_SEL+=("${TOOL_SEL[$i]:-0}")
  494. done
  495. SEL_TITLE="The Agency · Installer — 1/3 · Tools"
  496. SEL_HINT="Pick where to install. ${GLYPH_DET} = detected on this machine."
  497. SEL_SUMMARY_FN=_tools_summary
  498. SEL_NAV="space toggle · a all · n none · / search · enter next · q quit"
  499. SEL_WARN_FN=_no_warn
  500. selector
  501. for (( i=0; i<${#OPT_SEL[@]}; i++ )); do TOOL_SEL[$i]="${OPT_SEL[$i]}"; done
  502. }
  503. tool_simple_name() {
  504. case "$1" in
  505. claude-code) echo "Claude Code";; copilot) echo "Copilot";; antigravity) echo "Antigravity";;
  506. gemini-cli) echo "Gemini CLI";; opencode) echo "OpenCode";; openclaw) echo "OpenClaw";;
  507. cursor) echo "Cursor";; aider) echo "Aider";; windsurf) echo "Windsurf";;
  508. qwen) echo "Qwen Code";; kimi) echo "Kimi Code";; codex) echo "Codex";; osaurus) echo "Osaurus";; *) echo "$1";;
  509. esac
  510. }
  511. # --- Screen: Teams ---
  512. _teams_agents() {
  513. local i c=0 d
  514. for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do
  515. [[ "${OPT_SEL[$i]}" == 1 ]] && { d="${ALL_DIVISIONS[$i]}"; c=$(( c + ${TEAM_COUNTS[$i]} )); }
  516. done
  517. echo "$c"
  518. }
  519. _teams_summary() {
  520. local sel=0 i a; a="$(_teams_agents)"
  521. for (( i=0; i<${#OPT_SEL[@]}; i++ )); do [[ "${OPT_SEL[$i]}" == 1 ]] && sel=$(( sel+1 )); done
  522. printf '%s agents · %s of %s teams' "$a" "$sel" "${#OPT_SEL[@]}"
  523. }
  524. _teams_warn() {
  525. local a cap; a="$(_teams_agents)"; cap="$(tool_cap opencode)"
  526. if _opencode_selected && [[ "$a" -gt "$cap" ]]; then
  527. printf "⚠ OpenCode registers ~%s; ~%s of %s won't load (#27988)" "$cap" "$(( a - cap ))" "$a"
  528. fi
  529. }
  530. _opencode_selected() {
  531. local i; for (( i=0; i<${#TOOL_SEL[@]}; i++ )); do
  532. [[ "${ALL_TOOLS[$i]}" == "opencode" && "${TOOL_SEL[$i]}" == 1 ]] && return 0
  533. done; return 1
  534. }
  535. screen_teams() {
  536. OPT_LABEL=(); OPT_SEL=()
  537. local i
  538. for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do
  539. local d="${ALL_DIVISIONS[$i]}"
  540. OPT_LABEL+=("$(printf '%s %-20s %s' "$(division_emoji "$d")" "$d" "${C_DIM}${TEAM_COUNTS[$i]} agents${C_RESET}")")
  541. OPT_SEL+=("${TEAM_SEL[$i]:-1}")
  542. done
  543. SEL_TITLE="The Agency · Installer — 2/3 · Teams"
  544. SEL_HINT="Pick which teams to install. Fewer teams keeps OpenCode under its limit."
  545. SEL_SUMMARY_FN=_teams_summary
  546. SEL_NAV="space toggle · a all · n none · / search · enter next · ← back · q quit"
  547. SEL_WARN_FN=_teams_warn
  548. selector
  549. for (( i=0; i<${#OPT_SEL[@]}; i++ )); do TEAM_SEL[$i]="${OPT_SEL[$i]}"; done
  550. }
  551. # --- Screen: Review ---
  552. REVIEW_RESULT=""
  553. # grid_2col <cellwidth> <items...> — lay items out in two column-major columns
  554. # (left column filled top-to-bottom first). Plain text cells (no ANSI) so the
  555. # width padding stays correct.
  556. grid_2col() {
  557. local w="$1"; shift
  558. local n=$# r rows left right out=""
  559. (( n==0 )) && { printf ' %snone%s\n' "$C_DIM" "$C_RESET"; return; }
  560. local items=("$@")
  561. rows=$(( (n + 1) / 2 ))
  562. for (( r=0; r<rows; r++ )); do
  563. left="${items[$r]}"
  564. right="${items[$(( r + rows ))]:-}"
  565. if [[ -n "$right" ]]; then out+="$(printf ' %-*s %s' "$w" "$left" "$right")"$'\n'
  566. else out+=" $left"$'\n'; fi
  567. done
  568. printf '%s' "$out"
  569. }
  570. screen_review() {
  571. local tools=() teams=() i agents
  572. for (( i=0; i<${#TOOL_SEL[@]}; i++ )); do [[ "${TOOL_SEL[$i]}" == 1 ]] && tools+=("$(tool_simple_name "${ALL_TOOLS[$i]}")"); done
  573. for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] && teams+=("${ALL_DIVISIONS[$i]}"); done
  574. agents=0; for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] && agents=$(( agents + ${TEAM_COUNTS[$i]} )); done
  575. local cur=0 # 0=Install 1=mode toggle
  576. while true; do
  577. local buf="" m
  578. # pager
  579. 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'
  580. # description
  581. buf+=" ${C_DIM}Confirm your selection, then install.${C_RESET}"$'\n\n'
  582. # content: the selections + the mode toggle
  583. buf+=" ${C_BOLD}Tools${C_RESET} ${C_DIM}(${#tools[@]})${C_RESET}"$'\n'
  584. buf+="$(grid_2col 16 ${tools[@]+"${tools[@]}"})"$'\n'
  585. buf+=" ${C_BOLD}Teams${C_RESET} ${C_DIM}(${#teams[@]})${C_RESET}"$'\n'
  586. buf+="$(grid_2col 20 ${teams[@]+"${teams[@]}"})"$'\n\n'
  587. $USE_LINK && m="symlink" || m="copy"
  588. 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'
  589. else buf+=" Mode: ${m} ${C_DIM}(space toggles copy/symlink)${C_RESET}"$'\n'; fi
  590. buf+=$'\n'
  591. # summary
  592. buf+=" ${C_BOLD}Installing ${agents} agents · ${#teams[@]} teams · ${#tools[@]} tools${C_RESET}"$'\n'
  593. # navigation (Install is the action cursor target)
  594. 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'
  595. else buf+=" ${C_GREEN}Install${C_RESET} ${C_DIM}↑/↓ move · space toggle mode · ← back · q quit${C_RESET}"$'\n'; fi
  596. # warnings
  597. local cap; cap="$(tool_cap opencode)"
  598. if printf '%s\n' "${tools[@]}" | grep -qx "OpenCode" && [[ "$agents" -gt "$cap" ]]; then
  599. buf+=" ${C_YELLOW}⚠ OpenCode registers ~${cap}; ~$(( agents - cap )) of ${agents} won't load (#27988)${C_RESET}"$'\n'
  600. fi
  601. draw_frame "$buf"
  602. local key; key="$(read_key)"
  603. case "$key" in
  604. UP|DOWN|k|j|TAB) cur=$(( 1 - cur )) ;;
  605. SPACE) (( cur==1 )) && { $USE_LINK && USE_LINK=false || USE_LINK=true; } ;;
  606. ENTER) if (( cur==0 )); then REVIEW_RESULT=install; return; fi ;;
  607. LEFT) REVIEW_RESULT=back; return ;;
  608. q|Q|EOF) REVIEW_RESULT=quit; return ;;
  609. esac
  610. done
  611. }
  612. # interactive_wizard — drive the three screens; commit to SELECTED_TOOLS /
  613. # FILTER_DIVISIONS / USE_LINK. Returns 1 if no TTY (caller falls back).
  614. interactive_wizard() {
  615. init_ansi
  616. TEAM_COUNTS=(); local i
  617. for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do TEAM_COUNTS+=("$(division_count "${ALL_DIVISIONS[$i]}")"); done
  618. # seed defaults: tools = detected, teams = all
  619. 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
  620. TEAM_SEL=(); for (( i=0; i<${#ALL_DIVISIONS[@]}; i++ )); do TEAM_SEL+=(1); done
  621. tui_begin || return 1
  622. local screen=tools
  623. while true; do
  624. case "$screen" in
  625. tools) screen_tools; case "$SEL_RESULT" in next) screen=teams;; quit) tui_end; exit 0;; esac ;;
  626. teams) screen_teams; case "$SEL_RESULT" in next) screen=review;; back) screen=tools;; quit) tui_end; exit 0;; esac ;;
  627. review) screen_review; case "$REVIEW_RESULT" in install) break;; back) screen=teams;; quit) tui_end; exit 0;; esac ;;
  628. esac
  629. done
  630. tui_end
  631. # commit
  632. SELECTED_TOOLS=()
  633. for (( i=0; i<${#TOOL_SEL[@]}; i++ )); do [[ "${TOOL_SEL[$i]}" == 1 ]] && SELECTED_TOOLS+=("${ALL_TOOLS[$i]}"); done
  634. FILTER_DIVISIONS=()
  635. local all=1
  636. for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] || all=0; done
  637. if [[ "$all" == 0 ]]; then
  638. for (( i=0; i<${#TEAM_SEL[@]}; i++ )); do [[ "${TEAM_SEL[$i]}" == 1 ]] && FILTER_DIVISIONS+=("${ALL_DIVISIONS[$i]}"); done
  639. fi
  640. build_selection
  641. return 0
  642. }
  643. # ---------------------------------------------------------------------------
  644. # Installers
  645. # ---------------------------------------------------------------------------
  646. install_claude_code() {
  647. local dest; dest="$(resolve_dest claude-code "${HOME}/.claude/agents")"
  648. local count=0 dir f slug
  649. mkdir -p "$dest"
  650. for dir in "${AGENT_DIRS[@]}"; do
  651. [[ -d "$REPO_ROOT/$dir" ]] || continue
  652. while IFS= read -r -d '' f; do
  653. is_agent_file "$f" || continue
  654. slug="$(agent_slug "$f")"; slug_allowed "$slug" || continue
  655. install_file "$f" "$dest/"; incr count
  656. done < <(find "$REPO_ROOT/$dir" -name "*.md" -type f -print0)
  657. done
  658. ok "Claude Code: $count agents -> $dest"
  659. }
  660. install_copilot() {
  661. local dest_github; dest_github="$(resolve_dest copilot "${HOME}/.github/agents")"
  662. local dest_copilot="${HOME}/.copilot/agents"
  663. local count=0 dir f slug
  664. mkdir -p "$dest_github" "$dest_copilot"
  665. for dir in "${AGENT_DIRS[@]}"; do
  666. [[ -d "$REPO_ROOT/$dir" ]] || continue
  667. while IFS= read -r -d '' f; do
  668. is_agent_file "$f" || continue
  669. slug="$(agent_slug "$f")"; slug_allowed "$slug" || continue
  670. install_file "$f" "$dest_github/"
  671. install_file "$f" "$dest_copilot/"
  672. incr count
  673. done < <(find "$REPO_ROOT/$dir" -name "*.md" -type f -print0)
  674. done
  675. ok "Copilot: $count agents -> $dest_github"
  676. ok "Copilot: $count agents -> $dest_copilot"
  677. warn "Copilot: Verify VS Code setting 'chat.agentFilesLocations' includes your install path."
  678. dim " Open Settings (Ctrl/Cmd+,) -> search 'chat.agentFilesLocations'"
  679. }
  680. install_antigravity() {
  681. local src="$INTEGRATIONS/antigravity"
  682. local dest; dest="$(resolve_dest antigravity "${HOME}/.gemini/config/skills")"
  683. local count=0
  684. [[ -d "$src" ]] || { err "integrations/antigravity missing. Run convert.sh first."; return 1; }
  685. mkdir -p "$dest"
  686. local d
  687. while IFS= read -r -d '' d; do
  688. local name; name="$(basename "$d")"
  689. slug_allowed "$name" || continue
  690. mkdir -p "$dest/$name"
  691. install_file "$d/SKILL.md" "$dest/$name/SKILL.md"
  692. incr count
  693. done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
  694. ok "Antigravity: $count skills -> $dest"
  695. }
  696. install_osaurus() {
  697. local src="$INTEGRATIONS/osaurus"
  698. local dest; dest="$(resolve_dest osaurus "${HOME}/.osaurus/skills")"
  699. local count=0
  700. [[ -d "$src" ]] || { err "integrations/osaurus missing. Run convert.sh first."; return 1; }
  701. mkdir -p "$dest"
  702. local d
  703. while IFS= read -r -d '' d; do
  704. local name; name="$(basename "$d")"
  705. slug_allowed "$name" || continue
  706. mkdir -p "$dest/$name"
  707. install_file "$d/SKILL.md" "$dest/$name/SKILL.md"
  708. incr count
  709. done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
  710. ok "Osaurus: $count skills -> $dest"
  711. }
  712. install_gemini_cli() {
  713. local src="$INTEGRATIONS/gemini-cli/agents"
  714. local dest; dest="$(resolve_dest gemini-cli "${HOME}/.gemini/agents")"
  715. local count=0
  716. [[ -d "$src" ]] || { err "integrations/gemini-cli/agents missing. Run ./scripts/convert.sh --tool gemini-cli first."; return 1; }
  717. mkdir -p "$dest"
  718. local f
  719. while IFS= read -r -d '' f; do
  720. slug_allowed "$(basename "$f" .md)" || continue
  721. install_file "$f" "$dest/"
  722. incr count
  723. done < <(find "$src" -maxdepth 1 -name "*.md" -print0)
  724. ok "Gemini CLI: $count agents -> $dest"
  725. }
  726. install_opencode() {
  727. local src="$INTEGRATIONS/opencode"
  728. local dest; dest="$(resolve_dest opencode "${PWD}/.opencode/agents")"
  729. local count=0
  730. [[ -d "$src" ]] || { err "integrations/opencode missing. Run convert.sh first."; return 1; }
  731. # Support both flat layout (integrations/opencode/*.md) and nested (integrations/opencode/agents/*.md)
  732. local search_dir="$src"
  733. [[ -d "$src/agents" ]] && search_dir="$src/agents"
  734. mkdir -p "$dest"
  735. local f base
  736. while IFS= read -r -d '' f; do
  737. base="$(basename "$f")"
  738. [[ "$base" == "README.md" ]] && continue
  739. slug_allowed "${base%.md}" || continue
  740. install_file "$f" "$dest/"; incr count
  741. done < <(find "$search_dir" -maxdepth 1 -name "*.md" -print0)
  742. if (( count == 0 )); then
  743. warn "OpenCode: no agent files found in $search_dir. Run convert.sh --tool opencode first."
  744. else
  745. ok "OpenCode: $count agents -> $dest"
  746. fi
  747. capacity_warn opencode "$count"
  748. warn "OpenCode: project-scoped. Run from your project root to install there."
  749. }
  750. install_openclaw() {
  751. local src="$INTEGRATIONS/openclaw"
  752. local dest; dest="$(resolve_dest openclaw "${HOME}/.openclaw/agency-agents")"
  753. local count=0
  754. local existing_agents=""
  755. [[ -d "$src" ]] || { err "integrations/openclaw missing. Run convert.sh first."; return 1; }
  756. mkdir -p "$dest"
  757. if command -v openclaw >/dev/null 2>&1; then
  758. existing_agents=$'\n'"$(openclaw agents list --json 2>/dev/null | sed -n 's/^[[:space:]]*\"id\": \"\\([^\"]*\\)\".*/\\1/p')"$'\n'
  759. fi
  760. local d
  761. while IFS= read -r -d '' d; do
  762. local name; name="$(basename "$d")"
  763. slug_allowed "$name" || continue
  764. [[ -f "$d/SOUL.md" && -f "$d/AGENTS.md" && -f "$d/IDENTITY.md" ]] || continue
  765. mkdir -p "$dest/$name"
  766. install_file "$d/SOUL.md" "$dest/$name/SOUL.md"
  767. install_file "$d/AGENTS.md" "$dest/$name/AGENTS.md"
  768. install_file "$d/IDENTITY.md" "$dest/$name/IDENTITY.md"
  769. if command -v openclaw >/dev/null 2>&1; then
  770. if [[ "$existing_agents" != *$'\n'"$name"$'\n'* ]]; then
  771. openclaw agents add "$name" --workspace "$dest/$name" --non-interactive || true
  772. fi
  773. fi
  774. (( count++ )) || true
  775. done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
  776. if (( count == 0 )); then
  777. err "integrations/openclaw contains no generated workspaces. Run ./scripts/convert.sh --tool openclaw first."
  778. return 1
  779. fi
  780. ok "OpenClaw: $count workspaces -> $dest"
  781. if command -v openclaw >/dev/null 2>&1; then
  782. warn "OpenClaw: run 'openclaw gateway restart' to activate new agents"
  783. fi
  784. }
  785. install_cursor() {
  786. local src="$INTEGRATIONS/cursor/rules"
  787. local dest; dest="$(resolve_dest cursor "${PWD}/.cursor/rules")"
  788. local count=0
  789. [[ -d "$src" ]] || { err "integrations/cursor missing. Run convert.sh first."; return 1; }
  790. mkdir -p "$dest"
  791. local f
  792. while IFS= read -r -d '' f; do
  793. slug_allowed "$(basename "$f" .mdc)" || continue
  794. install_file "$f" "$dest/"; incr count
  795. done < <(find "$src" -maxdepth 1 -name "*.mdc" -print0)
  796. ok "Cursor: $count rules -> $dest"
  797. warn "Cursor: project-scoped. Run from your project root to install there."
  798. }
  799. install_aider() {
  800. local src="$INTEGRATIONS/aider/CONVENTIONS.md"
  801. local dest="${PWD}/CONVENTIONS.md"
  802. [[ -f "$src" ]] || { err "integrations/aider/CONVENTIONS.md missing. Run convert.sh first."; return 1; }
  803. if [[ -f "$dest" ]]; then
  804. warn "Aider: CONVENTIONS.md already exists at $dest (remove to reinstall)."
  805. return 0
  806. fi
  807. install_file "$src" "$dest"
  808. ok "Aider: installed -> $dest"
  809. $SELECTION_ACTIVE && warn "Aider: single-file format — team/agent filtering N/A (installs the full roster)."
  810. warn "Aider: project-scoped. Run from your project root to install there."
  811. }
  812. install_windsurf() {
  813. local src="$INTEGRATIONS/windsurf/.windsurfrules"
  814. local dest="${PWD}/.windsurfrules"
  815. [[ -f "$src" ]] || { err "integrations/windsurf/.windsurfrules missing. Run convert.sh first."; return 1; }
  816. if [[ -f "$dest" ]]; then
  817. warn "Windsurf: .windsurfrules already exists at $dest (remove to reinstall)."
  818. return 0
  819. fi
  820. install_file "$src" "$dest"
  821. ok "Windsurf: installed -> $dest"
  822. $SELECTION_ACTIVE && warn "Windsurf: single-file format — team/agent filtering N/A (installs the full roster)."
  823. warn "Windsurf: project-scoped. Run from your project root to install there."
  824. }
  825. install_qwen() {
  826. local src="$INTEGRATIONS/qwen/agents"
  827. local dest; dest="$(resolve_dest qwen "${PWD}/.qwen/agents")"
  828. local count=0
  829. [[ -d "$src" ]] || { err "integrations/qwen missing. Run convert.sh first."; return 1; }
  830. mkdir -p "$dest"
  831. local f
  832. while IFS= read -r -d '' f; do
  833. slug_allowed "$(basename "$f" .md)" || continue
  834. install_file "$f" "$dest/"
  835. incr count
  836. done < <(find "$src" -maxdepth 1 -name "*.md" -print0)
  837. ok "Qwen Code: installed $count agents to $dest"
  838. warn "Qwen Code: project-scoped. Run from your project root to install there."
  839. warn "Tip: Run '/agents manage' in Qwen Code to refresh, or restart session"
  840. }
  841. install_kimi() {
  842. local src="$INTEGRATIONS/kimi"
  843. local dest; dest="$(resolve_dest kimi "${HOME}/.config/kimi/agents")"
  844. local count=0
  845. [[ -d "$src" ]] || { err "integrations/kimi missing. Run convert.sh first."; return 1; }
  846. mkdir -p "$dest"
  847. local d
  848. while IFS= read -r -d '' d; do
  849. local name; name="$(basename "$d")"
  850. slug_allowed "$name" || continue
  851. mkdir -p "$dest/$name"
  852. install_file "$d/agent.yaml" "$dest/$name/agent.yaml"
  853. install_file "$d/system.md" "$dest/$name/system.md"
  854. incr count
  855. done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
  856. ok "Kimi Code: installed $count agents to $dest"
  857. ok "Usage: kimi --agent-file ~/.config/kimi/agents/<agent-name>/agent.yaml"
  858. }
  859. install_codex() {
  860. local src="$INTEGRATIONS/codex/agents"
  861. local dest; dest="$(resolve_dest codex "${HOME}/.codex/agents")"
  862. local count=0
  863. [[ -d "$src" ]] || { err "integrations/codex missing. Run convert.sh first."; return 1; }
  864. mkdir -p "$dest"
  865. local f
  866. while IFS= read -r -d '' f; do
  867. slug_allowed "$(basename "$f" .toml)" || continue
  868. install_file "$f" "$dest/"
  869. incr count
  870. done < <(find "$src" -maxdepth 1 -name "*.toml" -print0)
  871. ok "Codex: $count agents -> $dest"
  872. }
  873. install_vibe() {
  874. local src_agents="$INTEGRATIONS/vibe/agents"
  875. local src_prompts="$INTEGRATIONS/vibe/prompts"
  876. local dest; dest="$(resolve_dest vibe "${HOME}/.vibe")"
  877. local count=0
  878. [[ -d "$src_agents" && -d "$src_prompts" ]] || { err "integrations/vibe missing. Run convert.sh first."; return 1; }
  879. mkdir -p "$dest/agents" "$dest/prompts"
  880. local agent_file prompt_file slug
  881. while IFS= read -r -d '' agent_file; do
  882. slug="$(basename "$agent_file" .toml)"
  883. slug_allowed "$slug" || continue
  884. # Find the corresponding prompt file
  885. prompt_file="$src_prompts/$slug.md"
  886. [[ -f "$prompt_file" ]] || continue
  887. install_file "$agent_file" "$dest/agents/"
  888. install_file "$prompt_file" "$dest/prompts/"
  889. incr count
  890. done < <(find "$src_agents" -maxdepth 1 -name "*.toml" -print0)
  891. ok "Mistral Vibe: $count agents -> $dest/agents/ and $dest/prompts/"
  892. }
  893. vibe_home_dir() {
  894. printf '%s\n' "${VIBE_HOME:-${HOME}/.vibe}"
  895. }
  896. hermes_home_dir() {
  897. printf '%s\n' "${HERMES_HOME:-${HOME}/.hermes}"
  898. }
  899. ensure_hermes_plugin_enabled() {
  900. local hermes_home config plugin backup
  901. hermes_home="$(hermes_home_dir)"
  902. config="${hermes_home}/config.yaml"
  903. plugin="agency-agents-router"
  904. mkdir -p "$hermes_home"
  905. backup="${config}.bak.agency-agents-plugin.$$"
  906. [[ -f "$config" ]] && cp "$config" "$backup"
  907. python3 - "$config" "$plugin" <<'PY'
  908. from pathlib import Path
  909. import sys
  910. path = Path(sys.argv[1])
  911. plugin = sys.argv[2]
  912. text = path.read_text() if path.exists() else ""
  913. lines = text.splitlines()
  914. # Already enabled?
  915. in_plugins = False
  916. in_enabled = False
  917. for line in lines:
  918. if line.startswith("plugins:"):
  919. in_plugins = True
  920. in_enabled = False
  921. continue
  922. if in_plugins and line and not line.startswith((" ", "\t")):
  923. in_plugins = False
  924. in_enabled = False
  925. stripped_line = line.strip()
  926. if in_plugins and stripped_line == "enabled:":
  927. in_enabled = True
  928. continue
  929. if in_plugins and stripped_line.startswith("enabled:") and "[]" in stripped_line:
  930. in_enabled = False
  931. continue
  932. if in_enabled:
  933. stripped = line.strip()
  934. if stripped.startswith("-"):
  935. value = stripped[1:].strip().strip('"\'')
  936. if value == plugin:
  937. sys.exit(0)
  938. elif line.startswith(" ") and stripped.endswith(":"):
  939. in_enabled = False
  940. if not lines:
  941. lines = ["plugins:", " enabled:", f" - {plugin}"]
  942. elif not any(line.startswith("plugins:") for line in lines):
  943. if lines and lines[-1].strip():
  944. lines.append("")
  945. lines.extend(["plugins:", " enabled:", f" - {plugin}"])
  946. else:
  947. out = []
  948. in_plugins = False
  949. inserted = False
  950. saw_enabled = False
  951. for idx, line in enumerate(lines):
  952. if line.startswith("plugins:"):
  953. in_plugins = True
  954. out.append(line)
  955. continue
  956. if in_plugins and line and not line.startswith((" ", "\t")):
  957. if not saw_enabled and not inserted:
  958. out.extend([" enabled:", f" - {plugin}"])
  959. inserted = True
  960. in_plugins = False
  961. out.append(line)
  962. continue
  963. if in_plugins and line.strip().startswith("enabled:") and "[]" in line:
  964. saw_enabled = True
  965. out.extend([" enabled:", f" - {plugin}"])
  966. inserted = True
  967. continue
  968. if in_plugins and line.strip() == "enabled:":
  969. saw_enabled = True
  970. out.append(line)
  971. # Insert before the next sibling key or top-level key; if the list is
  972. # empty this still creates a valid block.
  973. out.append(f" - {plugin}")
  974. inserted = True
  975. continue
  976. out.append(line)
  977. if in_plugins and not saw_enabled and not inserted:
  978. out.extend([" enabled:", f" - {plugin}"])
  979. lines = out
  980. path.write_text("\n".join(lines) + "\n")
  981. PY
  982. if [[ -f "$backup" ]]; then
  983. ok "Hermes: enabled plugin $plugin in $config (backup: $backup)"
  984. else
  985. ok "Hermes: created config.yaml with plugins.enabled: $plugin"
  986. fi
  987. }
  988. install_hermes() {
  989. local src="$INTEGRATIONS/hermes/agency-agents-router"
  990. local hermes_home; hermes_home="$(hermes_home_dir)"
  991. local dest; dest="$(resolve_dest hermes "${hermes_home}/plugins/agency-agents-router")"
  992. # HERMES_PLUGIN_DIR is ambiguous: its name invites setting it to the plugins
  993. # parent (~/.hermes/plugins) rather than the full plugin path. Always target
  994. # the agency-agents-router subdir so we never rm -rf a shared plugins dir that
  995. # holds other plugins.
  996. if [[ "$(basename "$dest")" != "agency-agents-router" ]]; then
  997. dest="${dest%/}/agency-agents-router"
  998. fi
  999. [[ -f "$src/plugin.yaml" && -f "$src/__init__.py" && -f "$src/data/agents.json" ]] || {
  1000. err "integrations/hermes/agency-agents-router missing. Run ./scripts/convert.sh --tool hermes first."
  1001. return 1
  1002. }
  1003. mkdir -p "$(dirname "$dest")"
  1004. # Safety net: only ever remove our own plugin directory, never a parent.
  1005. if [[ "$(basename "$dest")" != "agency-agents-router" ]]; then
  1006. err "Hermes: refusing to remove '$dest' — expected an agency-agents-router directory."
  1007. return 1
  1008. fi
  1009. rm -rf "$dest"
  1010. if $USE_LINK; then
  1011. ln -s "$src" "$dest"
  1012. else
  1013. cp -R "$src" "$dest"
  1014. fi
  1015. ensure_hermes_plugin_enabled || warn "Hermes: plugin installed but config.yaml was not updated."
  1016. local count
  1017. count="$(python3 - "$src/data/agents.json" <<'PY'
  1018. from pathlib import Path
  1019. import json, sys
  1020. print(len(json.loads(Path(sys.argv[1]).read_text())))
  1021. PY
  1022. )"
  1023. ok "Hermes: lazy-router plugin ($count agents on disk) -> $dest"
  1024. warn "Hermes: restart sessions/gateway so the new plugin toolset is discovered."
  1025. if $SELECTION_ACTIVE; then
  1026. warn "Hermes: selection flags ignored; router keeps the full roster on disk and loads agents lazily."
  1027. fi
  1028. }
  1029. install_tool() {
  1030. ensure_converted "$1"
  1031. case "$1" in
  1032. claude-code) install_claude_code ;;
  1033. copilot) install_copilot ;;
  1034. antigravity) install_antigravity ;;
  1035. gemini-cli) install_gemini_cli ;;
  1036. opencode) install_opencode ;;
  1037. openclaw) install_openclaw ;;
  1038. cursor) install_cursor ;;
  1039. aider) install_aider ;;
  1040. windsurf) install_windsurf ;;
  1041. qwen) install_qwen ;;
  1042. kimi) install_kimi ;;
  1043. codex) install_codex ;;
  1044. osaurus) install_osaurus ;;
  1045. hermes) install_hermes ;;
  1046. vibe) install_vibe ;;
  1047. esac
  1048. }
  1049. # ---------------------------------------------------------------------------
  1050. # Entry point
  1051. # ---------------------------------------------------------------------------
  1052. main() {
  1053. local tool="all"
  1054. local interactive_mode="auto"
  1055. local use_parallel=false
  1056. local parallel_jobs
  1057. parallel_jobs="$(parallel_jobs_default)"
  1058. local list_what=""
  1059. while [[ $# -gt 0 ]]; do
  1060. case "$1" in
  1061. --tool) tool="${2:?'--tool requires a value'}"; shift 2; interactive_mode="no" ;;
  1062. --division)
  1063. local _d
  1064. IFS=',' read -ra _divs <<< "${2:?'--division requires a value'}"
  1065. for _d in "${_divs[@]}"; do
  1066. _d="$(printf '%s' "$_d" | xargs)"; [[ -z "$_d" ]] && continue
  1067. validate_division "$_d"; FILTER_DIVISIONS+=("$_d")
  1068. done
  1069. interactive_mode="no"; shift 2 ;;
  1070. --agent)
  1071. local _a
  1072. IFS=',' read -ra _ags <<< "${2:?'--agent requires a value'}"
  1073. for _a in "${_ags[@]}"; do
  1074. _a="$(printf '%s' "$_a" | xargs)"; [[ -n "$_a" ]] && FILTER_AGENTS+=("$_a")
  1075. done
  1076. interactive_mode="no"; shift 2 ;;
  1077. --agents-file) AGENTS_FILE="${2:?'--agents-file requires a value'}"; interactive_mode="no"; shift 2 ;;
  1078. --link) USE_LINK=true; shift ;;
  1079. --path) OVERRIDE_PATH="${2:?'--path requires a value'}"; shift 2 ;;
  1080. --no-convert) AUTO_CONVERT=false; shift ;;
  1081. --dry-run) DRY_RUN=true; interactive_mode="no"; shift ;;
  1082. --list) if [[ -z "${2:-}" || "${2:-}" == --* ]]; then list_what="all"; shift; else list_what="$2"; shift 2; fi ;;
  1083. --interactive) interactive_mode="yes"; shift ;;
  1084. --no-interactive) interactive_mode="no"; shift ;;
  1085. --parallel) use_parallel=true; shift ;;
  1086. --jobs) parallel_jobs="${2:?'--jobs requires a value'}"; shift 2 ;;
  1087. --help|-h) usage ;;
  1088. *) err "Unknown option: $1"; usage ;;
  1089. esac
  1090. done
  1091. [[ -n "$list_what" ]] && { do_list "$list_what"; exit 0; }
  1092. build_selection
  1093. check_integrations
  1094. # Validate explicit tool(s). --tool accepts a comma-separated list (like
  1095. # --division / --agent), e.g. --tool claude-code,cursor.
  1096. local _tool_list=()
  1097. if [[ "$tool" != "all" ]]; then
  1098. local _t
  1099. IFS=',' read -ra _tool_list <<< "$tool"
  1100. local _cleaned=()
  1101. for _t in "${_tool_list[@]}"; do
  1102. _t="$(printf '%s' "$_t" | xargs)"; [[ -z "$_t" ]] && continue
  1103. local valid=false _vt
  1104. for _vt in "${ALL_TOOLS[@]}"; do [[ "$_vt" == "$_t" ]] && valid=true && break; done
  1105. $valid || { err "Unknown tool '$_t'. Valid: ${ALL_TOOLS[*]}"; exit 1; }
  1106. _cleaned+=("$_t")
  1107. done
  1108. _tool_list=("${_cleaned[@]}")
  1109. fi
  1110. # Decide whether to show interactive UI
  1111. local use_interactive=false
  1112. if [[ "$interactive_mode" == "yes" ]]; then
  1113. use_interactive=true
  1114. elif [[ "$interactive_mode" == "auto" && -t 0 && -t 1 && "$tool" == "all" ]]; then
  1115. use_interactive=true
  1116. fi
  1117. SELECTED_TOOLS=()
  1118. if $use_interactive && interactive_wizard; then
  1119. : # wizard committed SELECTED_TOOLS + FILTER_DIVISIONS
  1120. elif [[ "$tool" != "all" ]]; then
  1121. SELECTED_TOOLS=("${_tool_list[@]}")
  1122. else
  1123. # Non-interactive (or no TTY): auto-detect
  1124. header "The Agency -- Scanning for installed tools..."
  1125. printf "\n"
  1126. local t
  1127. for t in "${ALL_TOOLS[@]}"; do
  1128. if is_detected "$t" 2>/dev/null; then
  1129. SELECTED_TOOLS+=("$t")
  1130. printf " ${C_GREEN}[*]${C_RESET} %s ${C_DIM}detected${C_RESET}\n" "$(tool_label "$t")"
  1131. else
  1132. printf " ${C_DIM}[ ] %s not found${C_RESET}\n" "$(tool_label "$t")"
  1133. fi
  1134. done
  1135. fi
  1136. if [[ ${#SELECTED_TOOLS[@]} -eq 0 ]]; then
  1137. warn "No tools selected or detected. Nothing to install."
  1138. printf "\n"
  1139. dim " Tip: use --tool <name> to force-install a specific tool."
  1140. dim " Available: ${ALL_TOOLS[*]}"
  1141. exit 0
  1142. fi
  1143. # --dry-run: print the plan and exit without writing anything.
  1144. if $DRY_RUN; then
  1145. local agents; agents="$(selected_agent_count)"
  1146. printf "\n"; header "The Agency -- Dry run (nothing written)"
  1147. printf " Tools: %s\n" "${SELECTED_TOOLS[*]}"
  1148. if $SELECTION_ACTIVE; then
  1149. [[ ${#FILTER_DIVISIONS[@]} -gt 0 ]] && printf " Teams: %s\n" "${FILTER_DIVISIONS[*]}"
  1150. [[ ${#FILTER_AGENTS[@]} -gt 0 ]] && printf " Agents: %s\n" "${FILTER_AGENTS[*]}"
  1151. [[ -n "$AGENTS_FILE" ]] && printf " File: %s\n" "$AGENTS_FILE"
  1152. else
  1153. printf " Teams: all (%s)\n" "${#ALL_DIVISIONS[@]}"
  1154. fi
  1155. printf " Agents: %s Mode: %s\n" "$agents" "$($USE_LINK && echo symlink || echo copy)"
  1156. local _t _cap
  1157. for _t in "${SELECTED_TOOLS[@]}"; do
  1158. _cap="$(tool_cap "$_t")"
  1159. [[ "$_cap" -gt 0 && "$agents" -gt "$_cap" ]] && \
  1160. warn "$_t caps ~$_cap — ~$(( agents - _cap )) of $agents won't register (anomalyco/opencode#27988)"
  1161. done
  1162. printf "\n"; exit 0
  1163. fi
  1164. # When parent runs install.sh --parallel, it spawns workers with AGENCY_INSTALL_WORKER=1
  1165. # so each worker only runs install_tool(s) and skips header/done box (avoids duplicate output).
  1166. if [[ -n "${AGENCY_INSTALL_WORKER:-}" ]]; then
  1167. local t
  1168. for t in "${SELECTED_TOOLS[@]}"; do
  1169. install_tool "$t"
  1170. done
  1171. return 0
  1172. fi
  1173. printf "\n"
  1174. header "The Agency -- Installing agents"
  1175. printf " Repo: %s\n" "$REPO_ROOT"
  1176. local n_selected=${#SELECTED_TOOLS[@]}
  1177. printf " Installing: %s\n" "${SELECTED_TOOLS[*]}"
  1178. if $SELECTION_ACTIVE; then
  1179. [[ ${#FILTER_DIVISIONS[@]} -gt 0 ]] && printf " Teams: %s\n" "${FILTER_DIVISIONS[*]}"
  1180. printf " Agents: %s of %s\n" "$(selected_agent_count)" "$(selected_agent_count_all)"
  1181. fi
  1182. $USE_LINK && printf " Mode: ${C_CYAN}symlink${C_RESET} (--link)\n"
  1183. if $use_parallel; then
  1184. ok "Installing $n_selected tools in parallel (output buffered per tool)."
  1185. fi
  1186. printf "\n"
  1187. local installed=0 t i=0
  1188. if $use_parallel; then
  1189. local install_out_dir
  1190. install_out_dir="$(mktemp -d)"
  1191. export AGENCY_INSTALL_OUT_DIR="$install_out_dir"
  1192. export AGENCY_INSTALL_SCRIPT="$SCRIPT_DIR/install.sh"
  1193. export AGENCY_INSTALL_EXTRA="$(worker_flags)"
  1194. 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'
  1195. for t in "${SELECTED_TOOLS[@]}"; do
  1196. [[ -f "$install_out_dir/$t" ]] && cat "$install_out_dir/$t"
  1197. done
  1198. rm -rf "$install_out_dir"
  1199. installed=$n_selected
  1200. else
  1201. for t in "${SELECTED_TOOLS[@]}"; do
  1202. (( i++ )) || true
  1203. progress_bar "$i" "$n_selected"
  1204. printf "\n"
  1205. printf " ${C_DIM}[%s/%s]${C_RESET} %s\n" "$i" "$n_selected" "$t"
  1206. install_tool "$t"
  1207. (( installed++ )) || true
  1208. done
  1209. fi
  1210. # Done box
  1211. local msg=" Done! Installed $installed tool(s)."
  1212. printf "\n"
  1213. box_top
  1214. box_row "${C_GREEN}${C_BOLD}${msg}${C_RESET}"
  1215. box_bot
  1216. printf "\n"
  1217. dim " Run ./scripts/convert.sh to regenerate after adding or editing agents."
  1218. printf "\n"
  1219. }
  1220. main "$@"