#!/usr/bin/env bash # # OpenAgents Control — Architect Fork · installer # ============================================================================ # All-in-one bootstrap: installs OpenCode (the runtime) when it is missing, then # the OAC Architect Fork — the agents, subagents, commands, context, skills and # tools — into a project's .opencode/ directory or the global # ~/.config/opencode/ directory. # # Adapted from Darren Hinde's OpenAgents Control installer (MIT). # upstream: https://github.com/darrenhinde/OpenAgentsControl # fork: https://git.lohmar.co.uk/cclohmar/NewOpenAgents # # Unlike upstream, this fork ships the COMPLETE .opencode payload at the repo # root (no registry.json), so the installer syncs the whole payload with # collision handling instead of resolving per-component from a registry. # # Usage: # ./install.sh # interactive: choose location # ./install.sh --local # into ./.opencode # ./install.sh --global # into ~/.config/opencode # ./install.sh --install-dir PATH # into an exact path # ./install.sh --source DIR # install from a local clone # ./install.sh --global --yes # non-interactive, skip existing files # ./install.sh --global --force # non-interactive, overwrite existing # ./install.sh --no-opencode # install the fork only (skip OpenCode) # ./install.sh --dry-run # show the plan, change nothing # # Environment: # OAC_TOKEN Forgejo token with repo:read (for a private remote fetch) # OPENCODE_BRANCH branch/tag to install from (default: main) # OPENCODE_INSTALL_DIR alternative to --install-dir # ============================================================================ set -euo pipefail # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- REPO_HOST="git.lohmar.co.uk" REPO_OWNER="cclohmar" REPO_NAME="NewOpenAgents" REPO_URL="https://${REPO_HOST}/${REPO_OWNER}/${REPO_NAME}" BRANCH="${OPENCODE_BRANCH:-main}" TOKEN="${OAC_TOKEN:-}" # The payload that makes up an install (paths relative to the repo root) PAYLOAD_DIRS="agent command context skills tool" PAYLOAD_FILES="env.example package.json" # OpenCode runtime — the official installer places the binary in $HOME/.opencode/bin OPENCODE_INSTALLER_URL="${OPENCODE_INSTALLER_URL:-https://opencode.ai/v2/install}" OPENCODE_BIN_DIR="${HOME}/.opencode/bin" # --------------------------------------------------------------------------- # Colours (only when writing to a terminal) # --------------------------------------------------------------------------- if [ -t 1 ]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m' BOLD='\033[1m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; MAGENTA=''; BOLD=''; NC='' fi info() { printf "${BLUE}ℹ${NC} %s\n" "$1"; } success() { printf "${GREEN}✓${NC} %s\n" "$1"; } warn() { printf "${YELLOW}⚠${NC} %s\n" "$1"; } error() { printf "${RED}✗${NC} %s\n" "$1" >&2; } step() { printf "\n${MAGENTA}${BOLD}▶${NC} %s\n\n" "$1"; } die() { error "$1"; exit 1; } print_header() { printf "${CYAN}${BOLD}" cat <<'BANNER' ╔════════════════════════════════════════════════════════════════╗ ║ ║ ║ OpenAgents Control — Architect Fork · Installer ║ ║ the Controlling Architect + the build agents ║ ║ ║ ╚════════════════════════════════════════════════════════════════╝ BANNER printf "${NC}\n" } usage() { cat </dev/null 2>&1 || die "tar is required but not installed" if [ -z "$SOURCE" ] && [ -z "$TOKEN" ]; then command -v git >/dev/null 2>&1 || die "git is required (or pass --token and install curl)" fi if [ -n "$TOKEN" ]; then command -v curl >/dev/null 2>&1 || die "curl is required when using --token" fi # --------------------------------------------------------------------------- # OpenCode runtime (all-in-one bootstrap) # --------------------------------------------------------------------------- have_opencode() { command -v opencode >/dev/null 2>&1 && return 0 [ -x "${OPENCODE_BIN_DIR}/opencode" ] && return 0 return 1 } opencode_path() { if command -v opencode >/dev/null 2>&1; then command -v opencode else printf '%s' "${OPENCODE_BIN_DIR}/opencode" fi } ensure_opencode() { if [ "$DRY_RUN" = 1 ]; then if have_opencode; then info "[dry-run] OpenCode already present — $(opencode_path)" elif [ "$OPENCODE_MODE" = "no" ]; then info "[dry-run] would skip OpenCode (--no-opencode)" else info "[dry-run] would install OpenCode from ${OPENCODE_INSTALLER_URL}" fi return 0 fi if have_opencode && [ "$OPENCODE_MODE" != "yes" ]; then success "OpenCode is already installed — $(opencode_path)" return 0 fi if [ "$OPENCODE_MODE" = "no" ]; then info "Skipping the OpenCode install (--no-opencode)" return 0 fi if [ "$OPENCODE_MODE" = "auto" ] && ! have_opencode && [ -t 0 ] && [ -t 1 ]; then printf "${BOLD}OpenCode is not installed. Install it now?${NC} [Y/n]: " ans="" read -r ans || true case "$ans" in [Nn]*) info "Skipping OpenCode."; return 0 ;; esac fi command -v curl >/dev/null 2>&1 || die "curl is required to install OpenCode" step "Installing OpenCode…" local tmp tmp="$(mktemp 2>/dev/null || mktemp -t oac)" if ! curl -fsSL "$OPENCODE_INSTALLER_URL" -o "$tmp"; then rm -f "$tmp" die "Could not download the OpenCode installer (${OPENCODE_INSTALLER_URL})" fi local ok=0 if [ -n "$OPENCODE_VERSION" ] && [ "$OPENCODE_NO_PATH" = 1 ]; then bash "$tmp" --no-modify-path --version "$OPENCODE_VERSION" && ok=1 elif [ -n "$OPENCODE_VERSION" ]; then bash "$tmp" --version "$OPENCODE_VERSION" && ok=1 elif [ "$OPENCODE_NO_PATH" = 1 ]; then bash "$tmp" --no-modify-path && ok=1 else bash "$tmp" && ok=1 fi rm -f "$tmp" [ "$ok" = 1 ] || die "OpenCode installation failed" if have_opencode; then success "OpenCode installed — $(opencode_path)" case ":$PATH:" in *":${OPENCODE_BIN_DIR}:"*) : ;; *) warn "Add OpenCode to your PATH: export PATH=\"${OPENCODE_BIN_DIR}:\$PATH\"" ;; esac else warn "The OpenCode installer finished, but 'opencode' is not on PATH yet. Re-open your shell and try again." fi } # --------------------------------------------------------------------------- # Resolve the source payload # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd)" TMP_DIR="" cleanup() { if [ -n "$TMP_DIR" ]; then rm -rf "$TMP_DIR" 2>/dev/null || true; fi; } trap cleanup EXIT INT TERM resolve_source() { # 1) explicit --source if [ -n "$SOURCE" ]; then [ -d "$SOURCE/agent" ] || die "--source '$SOURCE' does not look like the OAC repo (no agent/ dir)" SOURCE="$(cd "$SOURCE" && pwd)" info "Using local source: $SOURCE" return fi # 2) the script's own directory, when run from a clone if [ -d "$SCRIPT_DIR/agent" ] && [ -d "$SCRIPT_DIR/context" ]; then SOURCE="$SCRIPT_DIR" info "Using the clone this script lives in: $SOURCE" return fi # 3) download TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t oac)" if [ -n "$TOKEN" ]; then step "Downloading the fork (branch: ${BRANCH})…" local archive="https://${REPO_HOST}/${REPO_OWNER}/${REPO_NAME}/archive/${BRANCH}.tar.gz" curl -fsSL -H "Authorization: token ${TOKEN}" "$archive" -o "$TMP_DIR/repo.tar.gz" \ || die "Download failed. Check --token / --branch." tar -xzf "$TMP_DIR/repo.tar.gz" -C "$TMP_DIR" SOURCE="$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d -print -quit)" [ -n "$SOURCE" ] || die "Could not find the extracted payload" success "Downloaded." else step "Cloning the fork (branch: ${BRANCH})…" git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$TMP_DIR/repo" \ || die "Clone failed. For a private repo, configure git credentials or pass --token." SOURCE="$TMP_DIR/repo" success "Cloned." fi } # --------------------------------------------------------------------------- # Resolve the install directory # --------------------------------------------------------------------------- global_path() { printf '%s/.config/opencode' "$HOME"; } choose_location_interactive() { local gp; gp="$(global_path)" echo -e "${BOLD}Where should the fork be installed?${NC}\n" echo -e " ${GREEN}1)${NC} Local — ${CYAN}./.opencode${NC} (this project only)" echo -e " ${BLUE}2)${NC} Global — ${CYAN}${gp}${NC} (every project)" echo -e " ${MAGENTA}3)${NC} Custom — enter a path" echo -e " 4) Cancel" echo printf "Enter your choice [1-4]: " read -r choice case "$choice" in 1) INSTALL_DIR="$(pwd)/.opencode" ;; 2) INSTALL_DIR="$gp" ;; 3) printf "Enter installation path: "; read -r custom [ -n "$custom" ] || die "No path entered" case "$custom" in "~"*) custom="${HOME}${custom#\~}" ;; esac INSTALL_DIR="$custom" ;; 4) info "Cancelled."; exit 0 ;; *) die "Invalid choice" ;; esac } resolve_install_dir() { if [ -n "$INSTALL_DIR" ]; then return fi case "$LOCATION" in local) INSTALL_DIR="$(pwd)/.opencode" ;; global) INSTALL_DIR="$(global_path)" ;; custom) die "--install-dir requires a path" ;; *) if [ -t 0 ] && [ -t 1 ]; then choose_location_interactive else INSTALL_DIR="$(pwd)/.opencode" info "No location given — defaulting to ./.opencode" fi ;; esac } # --------------------------------------------------------------------------- # Collision strategy # --------------------------------------------------------------------------- STRATEGY="overwrite" # skip | overwrite | backup do_choose_strategy() { echo warn "Some items already exist in ${INSTALL_DIR}" echo echo -e "${BOLD}How should existing items be handled?${NC}\n" echo -e " ${GREEN}1)${NC} Skip — keep existing items, install only new ones" echo -e " ${YELLOW}2)${NC} Overwrite — replace existing items (your changes are lost)" echo -e " ${CYAN}3)${NC} Backup — back up existing items, then overwrite" echo -e " ${RED}4)${NC} Cancel" echo printf "Enter your choice [1-4]: " read -r choice case "$choice" in 1) STRATEGY="skip" ;; 2) printf "Type 'yes' to confirm overwrite: "; read -r c [ "$c" = "yes" ] || { info "Cancelled."; exit 0; } STRATEGY="overwrite" ;; 3) STRATEGY="backup" ;; 4) info "Cancelled."; exit 0 ;; *) die "Invalid choice" ;; esac } # --------------------------------------------------------------------------- # Install # --------------------------------------------------------------------------- BACKUP_DIR="" copy_entry() { local name="$1" kind="$2" src="$3" dest="$4" if [ -e "$dest" ]; then case "$STRATEGY" in skip) info "Skipped existing ${kind}: ${name}" return 0 ;; backup) local rel="${dest#${INSTALL_DIR}/}" mkdir -p "$BACKUP_DIR/$(dirname "$rel")" cp -R "$dest" "$BACKUP_DIR/$rel" ;; esac [ "$DRY_RUN" = 1 ] || rm -rf "$dest" fi if [ "$DRY_RUN" = 1 ]; then info "[dry-run] would install ${kind}: ${name} -> ${dest}" else cp -R "$src" "$(dirname "$dest")/" success "Installed ${kind}: ${name}" fi } perform_install() { local installed=0 skipped=0 # Scan for collisions to decide the strategy local have_collision=0 e for e in $PAYLOAD_DIRS $PAYLOAD_FILES; do if [ -e "$INSTALL_DIR/$e" ]; then have_collision=1; break; fi done if [ "$have_collision" = 1 ]; then if [ "$FORCE" = 1 ]; then STRATEGY="overwrite" elif [ "$ASSUME_YES" = 1 ]; then STRATEGY="skip" info "Non-interactive: existing items will be skipped" elif [ -t 0 ] && [ -t 1 ]; then do_choose_strategy else STRATEGY="skip" info "Non-interactive: existing items will be skipped" fi fi if [ "$STRATEGY" = "backup" ] && [ "$DRY_RUN" != 1 ]; then BACKUP_DIR="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)" mkdir -p "$BACKUP_DIR" info "Backing up to ${BACKUP_DIR}" fi if [ "$DRY_RUN" != 1 ]; then mkdir -p "$INSTALL_DIR" fi local d f for d in $PAYLOAD_DIRS; do [ -d "$SOURCE/$d" ] || continue if [ -e "$INSTALL_DIR/$d" ] && [ "$STRATEGY" = "skip" ]; then info "Skipped existing directory: ${d}/" skipped=$((skipped + 1)) continue fi copy_entry "$d/" "directory" "$SOURCE/$d" "$INSTALL_DIR/$d" installed=$((installed + 1)) done for f in $PAYLOAD_FILES; do [ -f "$SOURCE/$f" ] || continue if [ -e "$INSTALL_DIR/$f" ] && [ "$STRATEGY" = "skip" ]; then info "Skipped existing file: ${f}" skipped=$((skipped + 1)) continue fi copy_entry "$f" "file" "$SOURCE/$f" "$INSTALL_DIR/$f" installed=$((installed + 1)) done echo if [ "$DRY_RUN" = 1 ]; then warn "Dry run complete — nothing was changed." else success "Installation complete." echo -e " Destination: ${CYAN}${INSTALL_DIR}${NC}" [ "$skipped" -gt 0 ] && echo -e " Skipped: ${CYAN}${skipped}${NC}" [ -n "$BACKUP_DIR" ] && echo -e " Backup: ${CYAN}${BACKUP_DIR}${NC}" fi } show_post_install() { step "Next steps" echo "1. Review what was installed in ${CYAN}${INSTALL_DIR}${NC}" if [ -f "$INSTALL_DIR/env.example" ]; then echo "2. (Optional) configure integrations: ${CYAN}cp ${INSTALL_DIR}/env.example .env${NC}" fi if have_opencode; then echo "3. Start OpenCode and pick an agent:" echo -e " ${CYAN}opencode${NC} → choose ${BOLD}OpenArchitect${NC} to spec/verify, ${BOLD}OpenAgent${NC} to build" else echo "3. Install OpenCode, then pick an agent:" echo -e " ${CYAN}curl -fsSL ${OPENCODE_INSTALLER_URL} | bash${NC}" fi echo info "Docs: ${REPO_URL}" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { print_header ensure_opencode resolve_source resolve_install_dir info "Source: ${SOURCE}" info "Destination: ${INSTALL_DIR}" [ "$DRY_RUN" = 1 ] && warn "Dry run — no changes will be written" perform_install [ "$DRY_RUN" != 1 ] && show_post_install } main