237 lines
8.9 KiB
Bash
237 lines
8.9 KiB
Bash
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# install.sh — Multi-Agent Mux (MAM) Orchestration Installer
|
||
# ==============================================================================
|
||
# Idempotent, robust installer to bootstrap MAM orchestration skills
|
||
# and Python backplane dependencies on any local workspace.
|
||
# ==============================================================================
|
||
set -euo pipefail
|
||
|
||
# --- Configuration & Defaults ---
|
||
TARGET_DIR="${1:-$(pwd)}"
|
||
VENV_NAME=".venv"
|
||
MIN_PYTHON_VERSION="3.9"
|
||
|
||
echo "===================================================================="
|
||
echo "⚡ Starting Multi-Agent Mux (MAM) Installation"
|
||
echo "📂 Target Workspace: $TARGET_DIR"
|
||
echo "===================================================================="
|
||
|
||
# --- 1. System Requirements Validation ---
|
||
echo "🔍 Checking system dependencies..."
|
||
|
||
check_cmd() {
|
||
local cmd="$1"
|
||
if ! command -v "$cmd" &>/dev/null; then
|
||
echo "❌ Error: '$cmd' is not installed. Please install it first." >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
check_cmd tmux
|
||
check_cmd python3
|
||
|
||
# Verify Python Version
|
||
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||
PYTHON_MAJOR="${MIN_PYTHON_VERSION%%.*}"
|
||
PYTHON_MINOR="${MIN_PYTHON_VERSION##*.}"
|
||
if python3 -c "import sys; exit(0 if sys.version_info >= ($PYTHON_MAJOR, $PYTHON_MINOR) else 1)"; then
|
||
echo "✅ Python $PYTHON_VERSION detected."
|
||
else
|
||
echo "❌ Error: Python version must be $MIN_PYTHON_VERSION or higher. Detected: $PYTHON_VERSION" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Verify PyYAML (needed by system python3 for atomic state writes)
|
||
if ! python3 -c "import yaml" &>/dev/null; then
|
||
echo "❌ Error: 'PyYAML' is not installed in the system python3. Please install it first" >&2
|
||
echo " (e.g., 'pip3 install PyYAML' or 'sudo apt-get install python3-yaml')." >&2
|
||
exit 1
|
||
fi
|
||
echo "✅ PyYAML (system dependency) detected."
|
||
|
||
# --- 2. Workspace Setup ---
|
||
mkdir -p "$TARGET_DIR"
|
||
cd "$TARGET_DIR"
|
||
|
||
REPO_URL="https://git.godopu.com/tmpl/multi-agent-mux.git"
|
||
ARCHIVE_URL="https://git.godopu.com/tmpl/multi-agent-mux/archive/main.tar.gz"
|
||
|
||
# Helper to verify presence of all core runtime files.
|
||
# Keying off a set of core files helps detect and recover from partial/interrupted installations.
|
||
check_assets_present() {
|
||
local dir="${1:-.}"
|
||
local core_files=(
|
||
".agents/skills/lib.sh"
|
||
".agents/skills/multi-agent-mux-create/scripts/create_session.sh"
|
||
".agents/skills/multi-agent-mux-delegate-job/scripts/registry.py"
|
||
".agents/skills/multi-agent-mux-status/scripts/status.sh"
|
||
)
|
||
for f in "${core_files[@]}"; do
|
||
if [ ! -f "$dir/$f" ]; then
|
||
return 1
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# Fetch the orchestration assets if missing or incomplete (for curl one-liner installs).
|
||
#
|
||
# Safety model (FW-D1): we NEVER extract the repo archive directly into the
|
||
# target. Running inside an existing project must not overwrite the target's
|
||
# own files (README.md, FUTURE_WORKS.md, …) or litter it with this repo's
|
||
# development docs. Instead we stage the download into a throwaway temp dir,
|
||
# verify it, then copy ONLY the runtime assets (.agents/, documents, .env.example)
|
||
# into the target with per-file no-clobber guards so a pre-existing target file
|
||
# always wins.
|
||
if ! check_assets_present "."; then
|
||
echo "📥 Orchestration skills not found or incomplete. Fetching from Gitea repository..."
|
||
STAGE_DIR="$(mktemp -d)"
|
||
trap 'rm -rf "$STAGE_DIR"' EXIT
|
||
|
||
if command -v git &>/dev/null; then
|
||
echo "🌐 Cloning repository (shallow) into a staging area..."
|
||
git clone --depth 1 "$REPO_URL" "$STAGE_DIR"
|
||
elif command -v curl &>/dev/null; then
|
||
echo "🌐 Downloading and extracting archive into a staging area..."
|
||
curl -fsSL "$ARCHIVE_URL" | tar -xz --strip-components=1 -C "$STAGE_DIR"
|
||
else
|
||
echo "❌ Error: neither 'git' nor 'curl' is available to fetch the skills." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Verify the staged tree before we trust and copy from it.
|
||
if ! check_assets_present "$STAGE_DIR"; then
|
||
echo "❌ Error: fetched source is missing core runtime assets. Aborting (no files copied)." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Copy ONLY runtime assets into the target, never overwriting an existing
|
||
# target file. We merge per-file (POSIX find + an explicit "[ ! -e ]" guard)
|
||
# instead of `cp -n`: `cp -n` is non-portable and now prints a deprecation
|
||
# warning on GNU coreutils 9.x, whereas the explicit guard is portable to
|
||
# BSD/macOS and makes the no-clobber intent obvious.
|
||
mkdir -p .agents
|
||
( cd "$STAGE_DIR/.agents" && find . -type f -print ) | while IFS= read -r rel; do
|
||
dest=".agents/${rel#./}"
|
||
if [ ! -e "$dest" ]; then
|
||
mkdir -p "$(dirname "$dest")"
|
||
cp "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; }
|
||
fi
|
||
done
|
||
|
||
# Copy non-dev documents if they don't already exist.
|
||
# We skip dev-specific docs like README.md, DONE.md, and FUTURE_WORKS.md.
|
||
for doc in AGENT.md AGENT.ko.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md INSTRUCTION.md; do
|
||
if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then
|
||
cp "$STAGE_DIR/$doc" . || { echo "❌ Error: Failed to copy $doc" >&2; exit 1; }
|
||
fi
|
||
done
|
||
|
||
if [ -f "$STAGE_DIR/deploy/remove.sh" ] && [ ! -e "remove.sh" ]; then
|
||
cp "$STAGE_DIR/deploy/remove.sh" remove.sh || { echo "❌ Error: Failed to copy remove.sh" >&2; exit 1; }
|
||
chmod +x remove.sh
|
||
fi
|
||
|
||
if [ -f "$STAGE_DIR/.env.example" ] && [ ! -e ".env.example" ]; then
|
||
cp "$STAGE_DIR/.env.example" . || { echo "❌ Error: Failed to copy .env.example" >&2; exit 1; }
|
||
fi
|
||
|
||
rm -rf "$STAGE_DIR"
|
||
trap - EXIT
|
||
echo "✅ Skills staged into workspace (existing files preserved)."
|
||
fi
|
||
|
||
# Sanity check: verify all core files, not just a single one — an empty or
|
||
# incomplete layout would yield a silently broken install.
|
||
if ! check_assets_present "."; then
|
||
echo "❌ Error: Core runtime assets missing after setup. Target layout might be invalid." >&2
|
||
exit 1
|
||
fi
|
||
echo "✅ Orchestration skills present."
|
||
|
||
echo "📂 Ensuring metadata directory structure (.mam/)..."
|
||
mkdir -p .mam/jobs .mam/delegate_job_logs
|
||
|
||
# File permission lockdown on database directory (if owned by the current user to prevent multi-user system issues)
|
||
if [ -O .mam ]; then
|
||
chmod 0700 .mam
|
||
fi
|
||
|
||
# --- 3. Check Network File System (NFS) Warnings ---
|
||
echo "💾 Detecting file system mount type..."
|
||
if command -v df &>/dev/null && command -v mount &>/dev/null; then
|
||
MOUNTPOINT="$(df --output=target . 2>/dev/null | tail -1 || echo "")"
|
||
if [ -n "$MOUNTPOINT" ]; then
|
||
if mount | grep -q "$MOUNTPOINT.*nfs\|$MOUNTPOINT.*cifs\|$MOUNTPOINT.*fuse.sshfs"; then
|
||
echo "⚠️ WARNING: Target directory is on a network filesystem (NFS/CIFS/SSHFS)."
|
||
echo " SQLite WAL journaling and file locks are UNRELIABLE on network storage."
|
||
echo " The sqlite3 registry will fall back to 'DELETE' journaling instead of WAL."
|
||
else
|
||
echo "✅ File system supports WAL (Local storage detected)."
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# --- 4. Python Virtual Environment Setup ---
|
||
echo "🐍 Bootstrapping Python virtual environment (.venv)..."
|
||
if [ ! -d "$VENV_NAME" ]; then
|
||
python3 -m venv "$VENV_NAME"
|
||
echo "✅ Virtual environment created."
|
||
else
|
||
echo "ℹ️ Virtual environment (.venv) already exists. Skipping creation."
|
||
fi
|
||
|
||
# Activate virtual environment
|
||
# shellcheck disable=SC1091
|
||
source "$VENV_NAME"/bin/activate
|
||
|
||
# Upgrade pip
|
||
pip install --upgrade pip
|
||
|
||
# Install requirements
|
||
REQ_FILE=".agents/skills/multi-agent-mux-delegate-job/requirements.txt"
|
||
if [ -f "$REQ_FILE" ]; then
|
||
echo "📦 Installing backplane dependencies from $REQ_FILE..."
|
||
pip install -r "$REQ_FILE"
|
||
echo "✅ Dependencies installed successfully."
|
||
else
|
||
echo "⚠️ WARNING: Could not find requirements file: $REQ_FILE"
|
||
echo " Installing default packages (paho-mqtt, pyyaml) manually..."
|
||
pip install "paho-mqtt>=2.0.0" pyyaml
|
||
fi
|
||
|
||
# --- 5. Generate Environment Template ---
|
||
ENV_FILE=".env"
|
||
ENV_EXAMPLE=".env.example"
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
if [ -f "$ENV_EXAMPLE" ]; then
|
||
echo "📝 Creating configuration from $ENV_EXAMPLE..."
|
||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||
else
|
||
echo "📝 Creating default $ENV_FILE..."
|
||
touch "$ENV_FILE"
|
||
fi
|
||
|
||
# Always append the active defaults to ensure they are set and not commented out
|
||
cat <<EOF >> "$ENV_FILE"
|
||
|
||
# === Installer-applied active defaults ===
|
||
MQTT_BROKER=broker.hivemq.com
|
||
MQTT_PORT=1883
|
||
MQTT_TLS=0
|
||
MQTT_CLIENT_ID_PREFIX=mam-agent
|
||
TMUX_SERVER_NAME=default
|
||
EOF
|
||
chmod 0600 "$ENV_FILE"
|
||
echo "✅ Config file .env initialized with chmod 0600."
|
||
else
|
||
echo "ℹ️ $ENV_FILE already exists. Skipping config override."
|
||
fi
|
||
|
||
echo "===================================================================="
|
||
echo "🎉 Installation complete!"
|
||
echo "✨ You can now run the status or monitor skills."
|
||
echo "💡 Hint: Try executing: .venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py list"
|
||
echo "===================================================================="
|