NewOpenAgents/install.sh
cclohmar 2e1f8ca525 feat(install): all-in-one bootstrap — install OpenCode then the fork
- install.sh now ensures the OpenCode runtime is present: detects it on PATH or at $HOME/.opencode/bin, and otherwise runs the official installer (opencode.ai/v2/install)
- new flags: --with-opencode, --no-opencode, --opencode-version V, --opencode-no-path
- --dry-run never installs the runtime; OPENCODE_INSTALLER_URL can be overridden (mirrors/tests)
- README documents the all-in-one behaviour and the new runtime flags
2026-10-13 06:25:16 +00:00

511 lines
19 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
# ============================================================================
# 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 <<EOF
OpenAgents Control — Architect Fork · installer
All-in-one bootstrap: installs OpenCode (the runtime) if it is missing, then
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)
Runtime:
--with-opencode Install/update OpenCode even if it is already present
--no-opencode Skip the OpenCode install
--opencode-version V Install a specific OpenCode version
--opencode-no-path Don't let the OpenCode installer edit your shell PATH
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=""
OPENCODE_MODE="auto" # auto | yes | no
OPENCODE_VERSION=""
OPENCODE_NO_PATH=0
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 ;;
--with-opencode|--opencode) OPENCODE_MODE="yes" ;;
--no-opencode) OPENCODE_MODE="no" ;;
--opencode-version) [ $# -ge 2 ] || die "--opencode-version needs a version"; OPENCODE_VERSION="$2"; shift ;;
--opencode-version=*) OPENCODE_VERSION="${1#*=}" ;;
--opencode-no-path) OPENCODE_NO_PATH=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
# ---------------------------------------------------------------------------
# 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