diff --git a/README.md b/README.md index ed17adc..9178fc8 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,19 @@ RestartSec=5 WantedBy=multi-user.target ``` +The service step is a **separate program** — `install-service.sh` — so you can also run it on its own (the installer just calls it after OpenCode is installed): + +```bash +./install-service.sh # interactive +./install-service.sh --port 4096 +./install-service.sh --host 0.0.0.0 --port 8080 +./install-service.sh --yes # non-interactive defaults +./install-service.sh --uninstall # stop and remove the service +./install-service.sh --dry-run # show the unit, change nothing +``` + +It refuses to run non-interactively unless you pass `--yes`, and backs up any existing unit before replacing it. + - **Default bind is `127.0.0.1`** — the server has **no authentication of its own**, so keep it loopback-only and put a reverse proxy (Zoraxy, nginx, …) in front. Choose `0.0.0.0` only if you deliberately want it exposed on your network. - **Non-systemd** hosts: it prints the command to run the web UI manually instead. - **Non-interactive** (e.g. `curl | bash`, CI): the service is **not** set up unless you pass `--service`. diff --git a/install-service.sh b/install-service.sh new file mode 100755 index 0000000..7dd8063 --- /dev/null +++ b/install-service.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# +# OpenAgents Control — Architect Fork · web UI service installer +# ============================================================================ +# Creates (or removes) a systemd service that runs the OpenCode web UI as a +# background process: +# +# opencode serve --hostname --port +# +# Run it on its own, or let install.sh call it after OpenCode is installed. +# +# Usage: +# ./install-service.sh # interactive +# ./install-service.sh --port 4096 # interactive, prefilled port +# ./install-service.sh --host 0.0.0.0 --port 8080 +# ./install-service.sh --yes # non-interactive defaults +# ./install-service.sh --uninstall # stop and remove the service +# ./install-service.sh --dry-run # show the unit, change nothing +# +# Environment: +# OPENCODE_SERVICE_NAME unit name (default: opencode.service) +# OAC_FORCE_SYSTEMD 1/0 to force systemd detection (containers/CI) +# ============================================================================ + +set -euo pipefail + +SERVICE_NAME="${OPENCODE_SERVICE_NAME:-opencode.service}" +DEFAULT_HOST="127.0.0.1" +DEFAULT_PORT="4096" +OPENCODE_BIN_DIR="${HOME}/.opencode/bin" + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- +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; } + +# Prompt on the controlling terminal — works even under `curl … | bash`. +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/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 +} + +is_systemd() { + if [ "${OAC_FORCE_SYSTEMD:-}" = "1" ]; then return 0; fi + if [ "${OAC_FORCE_SYSTEMD:-}" = "0" ]; then return 1; fi + command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] +} + +# --------------------------------------------------------------------------- +# Arguments +# --------------------------------------------------------------------------- +HOST=""; PORT=""; WORKDIR=""; SVC_USER=""; SVC_GROUP="" +ENABLE="" # 1 | 0 | empty=ask +ASSUME_YES=0 +DRY_RUN=0 +UNINSTALL=0 + +usage() { + sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//' +} + +while [ $# -gt 0 ]; do + case "$1" in + --host) [ $# -ge 2 ] || die "--host needs a value"; HOST="$2"; shift ;; + --host=*) HOST="${1#*=}" ;; + --port) [ $# -ge 2 ] || die "--port needs a value"; PORT="$2"; shift ;; + --port=*) PORT="${1#*=}" ;; + --dir) [ $# -ge 2 ] || die "--dir needs a value"; WORKDIR="$2"; shift ;; + --dir=*) WORKDIR="${1#*=}" ;; + --user) [ $# -ge 2 ] || die "--user needs a value"; SVC_USER="$2"; shift ;; + --user=*) SVC_USER="${1#*=}" ;; + --enable) ENABLE=1 ;; + --no-enable) ENABLE=0 ;; + -y|--yes) ASSUME_YES=1 ;; + --uninstall) UNINSTALL=1 ;; + --dry-run) DRY_RUN=1 ;; + -h|--help) usage; exit 0 ;; + *) error "Unknown option: $1"; echo; usage; exit 1 ;; + esac + shift +done + +# --------------------------------------------------------------------------- +# Privilege +# --------------------------------------------------------------------------- +SUDO="" +[ "$(id -u)" != "0" ] && SUDO="sudo" +UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}" + +# --------------------------------------------------------------------------- +# Uninstall +# --------------------------------------------------------------------------- +if [ "$UNINSTALL" = 1 ]; then + if ! is_systemd; then + warn "systemd not detected — nothing to uninstall here."; exit 0 + fi + if [ "$DRY_RUN" = 1 ]; then + info "[dry-run] would stop, disable and delete ${UNIT_PATH}"; exit 0 + fi + $SUDO systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true + if [ -e "$UNIT_PATH" ]; then + $SUDO rm -f "$UNIT_PATH" && success "Removed ${UNIT_PATH}" + else + info "No unit at ${UNIT_PATH}" + fi + $SUDO systemctl daemon-reload 2>/dev/null || true + exit 0 +fi + +# --------------------------------------------------------------------------- +# Locate OpenCode +# --------------------------------------------------------------------------- +have_opencode || die "OpenCode is not installed (looked on PATH and ${OPENCODE_BIN_DIR}). Install it first." +OC_BIN="$(opencode_path)" + +# --------------------------------------------------------------------------- +# Prompt (interactive only) +# --------------------------------------------------------------------------- +INTERACTIVE=0 +if [ "$ASSUME_YES" = 0 ] && have_tty; then INTERACTIVE=1; fi + +if [ "$INTERACTIVE" = 1 ] && [ "$DRY_RUN" = 0 ]; then + printf "\n${BOLD}Set up the OpenCode web UI as a systemd service?${NC} [Y/n]: " + ans=""; tty_read ans + case "$ans" in + [Nn]*) info "Cancelled."; exit 0 ;; + esac +fi + +[ -n "$HOST" ] || HOST="$DEFAULT_HOST" +[ -n "$PORT" ] || PORT="$DEFAULT_PORT" +[ -n "$WORKDIR" ] || WORKDIR="$(pwd)" +[ -n "$SVC_USER" ] || SVC_USER="$(id -un)" +[ -n "$SVC_GROUP" ] || SVC_GROUP="$(id -gn)" + +if [ "$INTERACTIVE" = 1 ] && [ "$DRY_RUN" = 0 ]; then + v="" + printf " Bind address [%s]: " "$HOST"; tty_read v; [ -n "$v" ] && HOST="$v" + printf " Port [%s]: " "$PORT"; tty_read v; [ -n "$v" ] && PORT="$v" + printf " Working directory [%s]: " "$WORKDIR"; tty_read v; [ -n "$v" ] && WORKDIR="$v" + printf " Start now and enable on boot? [Y/n]: "; v=""; tty_read v + case "$v" in [Nn]*) ENABLE=0 ;; *) ENABLE=1 ;; esac +fi +[ -n "$ENABLE" ] || ENABLE=1 + +# --------------------------------------------------------------------------- +# Build the unit +# --------------------------------------------------------------------------- +UNIT="$(mktemp 2>/dev/null || mktemp -t oac-unit)" +cat > "$UNIT" </dev/null; then + info "Backed up the existing unit to ${backup}" + fi +fi + +if ! $SUDO cp "$UNIT" "$UNIT_PATH"; then + die "Could not write ${UNIT_PATH} (needs root). Re-run with sudo." +fi +$SUDO systemctl daemon-reload 2>/dev/null || true + +if [ "$ENABLE" = 1 ]; then + if $SUDO systemctl enable --now "$SERVICE_NAME" 2>/dev/null; then + success "Web UI service enabled — http://${HOST}:${PORT}" + else + warn "Unit installed; enable it later: sudo systemctl enable --now ${SERVICE_NAME}" + fi +else + success "Unit installed — start it with: sudo systemctl start ${SERVICE_NAME}" +fi diff --git a/install.sh b/install.sh index b2713f0..9475cf2 100755 --- a/install.sh +++ b/install.sh @@ -51,7 +51,6 @@ 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" -SERVICE_UNIT_NAME="opencode.service" # --------------------------------------------------------------------------- # Colours (only when writing to a terminal) @@ -130,7 +129,7 @@ Runtime: --opencode-version V Install a specific OpenCode version --opencode-no-path Don't let the OpenCode installer edit your shell PATH -Service (offered interactively at the end): +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 @@ -331,114 +330,45 @@ ensure_opencode() { } # --------------------------------------------------------------------------- -# Web UI service (systemd) — offered interactively +# Web UI service — delegated to the standalone install-service.sh (its own process) # --------------------------------------------------------------------------- -is_systemd() { - if [ "${OAC_FORCE_SYSTEMD:-}" = "1" ]; then return 0; fi # override for containers/CI - if [ "${OAC_FORCE_SYSTEMD:-}" = "0" ]; then return 1; fi - command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] -} +SERVICE_SCRIPT_URL="https://${REPO_HOST}/${REPO_OWNER}/${REPO_NAME}/raw/branch/${BRANCH}/install-service.sh" -setup_service() { +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 # non-interactive: only set up the service when explicitly asked - fi + if have_tty; then interactive=1; else return 0; fi fi - if [ "$interactive" = 1 ]; then - printf "\n${BOLD}Set up the OpenCode web UI as a systemd service?${NC} [y/N]: " - ans=""; tty_read ans - case "$ans" in [Yy]*) ;; *) info "Skipping the web UI service."; return 0 ;; esac - fi - - if ! is_systemd; then - warn "systemd not detected — cannot install a service here." - info "Start the web UI manually: $(opencode_path) serve --hostname 127.0.0.1 --port 4096" - return 0 - fi - - local host port enable dir u g oc_bin v - oc_bin="$(opencode_path)" - host="127.0.0.1" - port="4096" - enable=1 - dir="$(pwd)" - u="$(id -un)" - g="$(id -gn)" - - if [ "$interactive" = 1 ]; then - printf " Bind address [%s]: " "$host"; v=""; tty_read v; [ -n "$v" ] && host="$v" - printf " Port [%s]: " "$port"; v=""; tty_read v; [ -n "$v" ] && port="$v" - printf " Working directory [%s]: " "$dir"; v=""; tty_read v; [ -n "$v" ] && dir="$v" - printf " Start now and enable on boot? [Y/n]: "; v=""; tty_read v - case "$v" in [Nn]*) enable=0 ;; esac - fi - - local unit - unit="$(mktemp 2>/dev/null || mktemp -t oac-unit)" - cat > "$unit" </dev/null; then - rm -f "$unit" - warn "Could not write /etc/systemd/system/${SERVICE_UNIT_NAME} (needs root)." - info "Re-run with sudo, or copy the unit and run: sudo systemctl enable --now ${SERVICE_UNIT_NAME}" - return 0 - fi - rm -f "$unit" - $sudo systemctl daemon-reload 2>/dev/null || true - - if [ "$enable" = 1 ]; then - if $sudo systemctl enable --now "$SERVICE_UNIT_NAME" 2>/dev/null; then - success "Web UI service enabled — http://${host}:${port}" - else - warn "Unit installed; enable it later: sudo systemctl enable --now ${SERVICE_UNIT_NAME}" - 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 - success "Unit installed — start it with: sudo systemctl start ${SERVICE_UNIT_NAME}" + 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 - return 0 + + 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" } # --------------------------------------------------------------------------- @@ -692,7 +622,7 @@ main() { if [ "$DRY_RUN" != 1 ]; then show_post_install - setup_service + run_service_setup fi }