NewOpenAgents/install.sh
cclohmar 5e84cf7af0 docs: adapt README to the Architect fork; add fork installer
- README rewritten around the Controlling Architect concept (spec -> build -> report -> verify), the architect/builder agency and the channels; upstream README preserved as README.upstream.md
- install.sh: fork-native installer adapted from Darren Hinde's OAC installer. Syncs the complete .opencode payload (no registry), supports local/global/custom, skip/overwrite/backup, --source/--token/--branch/--yes/--force/--dry-run
2026-10-12 09:11:39 +00:00

402 lines
15 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# OpenAgents Control — Architect Fork · installer
# ============================================================================
# Installs 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 --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"
# ---------------------------------------------------------------------------
# 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 <<EOF
OpenAgents Control — Architect Fork · installer
Installs the OAC Architect Fork into a project's .opencode/ or the global
~/.config/opencode/ directory.
Usage:
$(basename "$0") [OPTIONS]
Location:
--local Install into ./.opencode (default)
--global Install into ~/.config/opencode
--install-dir PATH Install into an exact path
(with no location flag, an interactive menu is shown)
Source:
--source DIR Install from a local copy instead of downloading
--branch REF Branch/tag to fetch (default: main)
--token TOKEN Forgejo token with repo:read (private fetch)
Behaviour:
-y, --yes Non-interactive; skip files that already exist
--force Non-interactive; overwrite existing files
--dry-run Show what would happen; change nothing
-h, --help Show this help
Environment:
OAC_TOKEN Token used for a private remote fetch
OPENCODE_BRANCH Branch/tag to fetch
OPENCODE_INSTALL_DIR Same as --install-dir
Examples:
git clone ${REPO_URL}.git ~/.oac && ~/.oac/install.sh --global
./install.sh --local
./install.sh --install-dir ~/my-agents --dry-run
EOF
}
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
INSTALL_DIR="${OPENCODE_INSTALL_DIR:-}"
LOCATION=""
ASSUME_YES=0
FORCE=0
DRY_RUN=0
SOURCE=""
while [ $# -gt 0 ]; do
case "$1" in
--local) LOCATION="local" ;;
--global) LOCATION="global" ;;
--install-dir) [ $# -ge 2 ] || die "--install-dir needs a path"; INSTALL_DIR="$2"; LOCATION="custom"; shift ;;
--install-dir=*) INSTALL_DIR="${1#*=}"; LOCATION="custom" ;;
--source) [ $# -ge 2 ] || die "--source needs a path"; SOURCE="$2"; shift ;;
--source=*) SOURCE="${1#*=}" ;;
--branch) [ $# -ge 2 ] || die "--branch needs a ref"; BRANCH="$2"; shift ;;
--branch=*) BRANCH="${1#*=}" ;;
--token) [ $# -ge 2 ] || die "--token needs a value"; TOKEN="$2"; shift ;;
--token=*) TOKEN="${1#*=}" ;;
-y|--yes) ASSUME_YES=1 ;;
--force) ASSUME_YES=1; FORCE=1 ;;
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) error "Unknown option: $1"; echo; usage; exit 1 ;;
esac
shift
done
# ---------------------------------------------------------------------------
# Dependencies
# ---------------------------------------------------------------------------
command -v tar >/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
# ---------------------------------------------------------------------------
# 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
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"
echo
info "Docs: ${REPO_URL}"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
print_header
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