NewOpenAgents/install.sh
cclohmar 91204e9d4e fix(install): resolve the newest OpenCode build actually published for the platform
- don't trust opencode.ai metadata (it can advertise a version whose platform binary isn't published yet, e.g. 'Version 2.0.28 is not available for linux-x64')
- resolve_latest_published() reads the platform package's npm dist-tag latest (@opencode/cli-<os>-<arch>[-baseline][-musl]) and passes --version to the official installer
- README note updated
2026-10-13 09:06:17 +00:00

670 lines
25 KiB
Bash
Executable file
Raw Permalink 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; }
# Prompting — works even under `curl … | bash`, where stdin is the script pipe.
# We prompt on the controlling terminal instead of relying on stdin.
have_tty() {
[ -t 1 ] || return 1
[ -t 0 ] && return 0
[ -r /dev/tty ]
}
tty_read() {
local __var="$1" __val=""
if [ -t 0 ] || [ ! -r /dev/tty ]; then
read -r __val || true
else
read -r __val </dev/tty || true
fi
printf -v "$__var" '%s' "$__val"
}
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
Service (via the standalone install-service.sh):
--service Set up the web UI service non-interactively (defaults)
--no-service Never offer the web UI service
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
SERVICE_MODE="auto" # auto | yes | no
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 ;;
--service) SERVICE_MODE="yes" ;;
--no-service) SERVICE_MODE="no" ;;
-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
}
# OpenCode's `latest` metadata can advertise a version whose platform binary is
# not published yet (e.g. "Version 2.0.28 is not available for linux-x64").
# Resolve the newest version that IS published for this platform, so the default
# install works without the user knowing a version.
resolve_latest_published() {
local os arch target scope url ver
case "$(uname -s)" in
Darwin) os=darwin ;;
Linux) os=linux ;;
MINGW*|MSYS*|CYGWIN*) os=windows ;;
*) return 0 ;;
esac
arch="$(uname -m)"
case "$arch" in
x86_64|amd64) arch=x64 ;;
aarch64|arm64) arch=arm64 ;;
esac
target="${os}-${arch}"
if [ "$arch" = "x64" ] && [ "$os" = "linux" ]; then
grep -qwi avx2 /proc/cpuinfo 2>/dev/null || target="${target}-baseline"
if [ -f /etc/alpine-release ] || { command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; }; then
target="${target}-musl"
fi
fi
command -v curl >/dev/null 2>&1 || return 0
for scope in "@opencode" "@opencode-ai"; do
url="https://registry.npmjs.org/-/package/${scope}/cli-${target}/dist-tags"
ver="$(curl -fsSL --max-time 15 "$url" 2>/dev/null | sed -n 's/.*"latest":"\([^"]*\)".*/\1/p')"
if [ -n "$ver" ]; then
printf '%s' "$ver"
return 0
fi
done
return 0
}
opencode_on_path() { command -v opencode >/dev/null 2>&1; }
# The official installer writes the PATH line to the shell rc but never activates
# it. Report that, and offer to make `opencode` work immediately via a symlink.
activate_opencode() {
opencode_on_path && return 0
have_opencode || return 0
warn "OpenCode is installed at ${OPENCODE_BIN_DIR} but is not on your PATH yet."
if [ "$OPENCODE_NO_PATH" = 1 ]; then
info "PATH was left untouched (--opencode-no-path). Add it yourself: export PATH=\"${OPENCODE_BIN_DIR}:\$PATH\""
return 0
fi
local rc=""
case "$(basename "${SHELL:-bash}")" in
bash) [ -f "$HOME/.bashrc" ] && rc="$HOME/.bashrc" ;;
zsh) [ -f "$HOME/.zshrc" ] && rc="$HOME/.zshrc" ;;
*) [ -f "$HOME/.profile" ] && rc="$HOME/.profile" ;;
esac
if [ -n "$rc" ] && grep -q "${OPENCODE_BIN_DIR}" "$rc" 2>/dev/null; then
info "A PATH line is already in ${rc} — run source ${rc} or open a new terminal."
else
info "Add it to your shell profile: export PATH=\"${OPENCODE_BIN_DIR}:\$PATH\""
fi
if have_tty; then
printf "${BOLD}Make 'opencode' available now (symlink into /usr/local/bin)?${NC} [y/N]: "
ans=""; tty_read ans
case "$ans" in
[Yy]*)
local sudo=""
[ "$(id -u)" != "0" ] && sudo="sudo"
if $sudo ln -sf "${OPENCODE_BIN_DIR}/opencode" /usr/local/bin/opencode 2>/dev/null; then
success "Linked /usr/local/bin/opencode — 'opencode' now works in this and every shell."
else
warn "Could not create the symlink — use: export PATH=\"${OPENCODE_BIN_DIR}:\$PATH\""
fi
;;
esac
fi
return 0
}
ensure_opencode() {
if [ "$DRY_RUN" = 1 ]; then
if [ "$OPENCODE_MODE" = "no" ]; then
info "[dry-run] would skip OpenCode (--no-opencode)"
elif have_opencode; then
info "[dry-run] OpenCode already present — $(opencode_path)"
else
info "[dry-run] would install OpenCode from ${OPENCODE_INSTALLER_URL}"
fi
return 0
fi
if [ "$OPENCODE_MODE" = "no" ]; then
info "Skipping the OpenCode install (--no-opencode)"
return 0
fi
if have_opencode && [ "$OPENCODE_MODE" != "yes" ]; then
success "OpenCode is already installed — $(opencode_path)"
activate_opencode
return 0
fi
if [ "$OPENCODE_MODE" = "auto" ] && ! have_opencode && have_tty; then
printf "${BOLD}OpenCode is not installed. Install it now?${NC} [Y/n]: "
ans=""
tty_read ans
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
local ver="$OPENCODE_VERSION"
if [ -z "$ver" ]; then
ver="$(resolve_latest_published)"
[ -n "$ver" ] && info "Using the newest build published for this platform: ${ver}"
fi
if [ -n "$ver" ] && [ "$OPENCODE_NO_PATH" = 1 ]; then
bash "$tmp" --no-modify-path --version "$ver" && ok=1
elif [ -n "$ver" ]; then
bash "$tmp" --version "$ver" && 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)"
else
warn "The OpenCode installer finished, but the binary was not found at ${OPENCODE_BIN_DIR}/opencode."
fi
activate_opencode
return 0
}
# ---------------------------------------------------------------------------
# Web UI service — delegated to the standalone install-service.sh (its own process)
# ---------------------------------------------------------------------------
SERVICE_SCRIPT_URL="https://${REPO_HOST}/${REPO_OWNER}/${REPO_NAME}/raw/branch/${BRANCH}/install-service.sh"
run_service_setup() {
[ "$SERVICE_MODE" = "no" ] && { info "Skipping the web UI service (--no-service)"; return 0; }
have_opencode || { info "OpenCode is not installed — skipping the web UI service."; return 0; }
local interactive=0
if [ "$SERVICE_MODE" = "auto" ]; then
if have_tty; then interactive=1; else return 0; fi
fi
# Locate the standalone service installer: beside this script, else download it.
local script="" tmp=""
if [ -f "$SCRIPT_DIR/install-service.sh" ]; then
script="$SCRIPT_DIR/install-service.sh"
else
command -v curl >/dev/null 2>&1 || { warn "curl is required to fetch the service installer"; return 0; }
tmp="$(mktemp 2>/dev/null || mktemp -t oac-svc)"
if curl -fsSL "$SERVICE_SCRIPT_URL" -o "$tmp" 2>/dev/null; then
script="$tmp"
else
rm -f "$tmp"
warn "Could not fetch the service installer (${SERVICE_SCRIPT_URL}) — skipping."
return 0
fi
fi
step "Web UI service"
local rc=0
if [ "$interactive" = 1 ]; then
bash "$script" || rc=$?
else
bash "$script" --yes || rc=$?
fi
[ -n "$tmp" ] && rm -f "$tmp"
return "$rc"
}
# ---------------------------------------------------------------------------
# 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]: "
tty_read choice
case "$choice" in
1) INSTALL_DIR="$(pwd)/.opencode" ;;
2) INSTALL_DIR="$gp" ;;
3) printf "Enter installation path: "; tty_read 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 have_tty; 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]: "
tty_read choice
case "$choice" in
1) STRATEGY="skip" ;;
2) printf "Type 'yes' to confirm overwrite: "; tty_read 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 have_tty; 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
return 0
}
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 opencode_on_path; 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"
elif have_opencode; then
echo "3. OpenCode is installed but not on PATH yet — get it available:"
echo -e " ${CYAN}export PATH=\"${OPENCODE_BIN_DIR}:\$PATH\"${NC} (or open a new terminal)"
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
if [ "$DRY_RUN" != 1 ]; then
show_post_install
run_service_setup
fi
}
main