- new install-service.sh: creates/removes the OpenCode web UI systemd service as its own process (interactive by default, flags for automation, --uninstall, --dry-run, backs up an existing unit) - install.sh no longer embeds the service logic; run_service_setup locates the sibling script (or downloads it) and invokes it as a separate process - README documents running install-service.sh on its own
629 lines
23 KiB
Bash
Executable file
629 lines
23 KiB
Bash
Executable file
#!/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_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
|
||
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)"
|
||
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
|