From b4e32988697efb184589ff3c6a56b7dd9e4dd5ae Mon Sep 17 00:00:00 2001 From: Raghav Sood Date: Wed, 11 Mar 2026 17:56:14 +0800 Subject: [PATCH] feat: add installation script for one-line Flock setup Add install.sh - a comprehensive bash script that automates the entire installation process including dependency installation (Go, GitHub CLI, OpenCode), building Flock from source, creating configuration files, setting up system services (systemd/launchd), and configuring automatic daily updates via cron. The script is idempotent and supports --install-dir, --non-interactive, and --skip-auth flags. Update README with quick install instructions and the one-liner command. Fixes #65 --- README.md | 43 ++++ install.sh | 731 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 774 insertions(+) create mode 100644 install.sh diff --git a/README.md b/README.md index 5694756..3d29928 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,49 @@ Agent orchestration system that proxies and manages OpenCode instances. +## Installation + +### Quick Install (One-Liner) + +```bash +curl -sSL https://raw.githubusercontent.com/nbitslabs/flock/main/install.sh | bash +``` + +This will: +1. Install Go (if not present) +2. Install GitHub CLI (gh) +3. Install OpenCode +4. Build Flock from source +5. Create configuration files +6. Set up system services (systemd on Linux, launchd on macOS) +7. Configure auto-updates via cron + +### Manual Installation + +```bash +# Clone the repository +git clone https://github.com/nbitslabs/flock.git +cd flock + +# Build the binary +go build -o flock ./cmd/flock + +# Run +./flock +``` + +## Configuration + +After installation, configure your environment: + +```bash +# Authenticate with GitHub +gh auth login + +# Authenticate with OpenCode +opencode auth login +``` + ## Run ```bash diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..49455c8 --- /dev/null +++ b/install.sh @@ -0,0 +1,731 @@ +#!/usr/bin/env bash +set -euo pipefail + +INSTALL_DIR="${FLOCK_INSTALL_DIR:-$HOME/.flock}" +FLOCK_REPO="https://github.com/nbitslabs/flock.git" +NON_INTERACTIVE="${NON_INTERACTIVE:-false}" +SKIP_AUTH="${SKIP_AUTH:-false}" +DOMAIN="" + +log() { + echo "[flock-install] $1" +} + +detect_os() { + case "$(uname -s)" in + Linux*) echo "linux";; + Darwin*) echo "macos";; + *) echo "unknown";; + esac +} + +detect_package_manager() { + if command -v apt-get &> /dev/null; then + echo "apt" + elif command -v yum &> /dev/null; then + echo "yum" + elif command -v brew &> /dev/null; then + echo "brew" + else + echo "unknown" + fi +} + +install_golang() { + if command -v go &> /dev/null; then + local version + version=$(go version 2>/dev/null | grep -oP 'go\d+\.\d+' | sed 's/go//') + if [[ "${version%.*}" -ge 22 ]]; then + log "Go ${version} already installed" + return 0 + fi + fi + + log "Installing Go..." + local os_type + os_type=$(detect_os) + local arch + arch=$(uname -m) + [[ "$arch" == "x86_64" ]] && arch="amd64" || [[ "$arch" == "arm64" ]] && arch="arm64" + + local go_version="1.24.7" + local go_archive="go${go_version}.${os_type}-${arch}.tar.gz" + + curl -sL "https://go.dev/dl/${go_archive}" -o "/tmp/${go_archive}" + sudo rm -rf /usr/local/go + sudo tar -C /usr/local -xzf "/tmp/${go_archive}" + rm "/tmp/${go_archive}" + + export PATH="/usr/local/go/bin:$PATH" + log "Go installed successfully" +} + +install_github_cli() { + if command -v gh &> /dev/null; then + log "GitHub CLI already installed" + return 0 + fi + + log "Installing GitHub CLI..." + local os_type + os_type=$(detect_os) + + if [[ "$os_type" == "macos" ]]; then + brew install gh + else + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt update + sudo apt install gh + fi +} + +install_opencode() { + if command -v opencode &> /dev/null; then + log "OpenCode already installed" + return 0 + fi + + log "Installing OpenCode..." + local os_type + os_type=$(detect_os) + local arch + arch=$(uname -m) + [[ "$arch" == "x86_64" ]] && arch="x86_64" || [[ "$arch" == "arm64" ]] && arch="arm64" + + local opencode_url + opencode_url="https://github.com/opencodeai/opencode/releases/latest/download/opencode-${os_type}-${arch}" + + curl -sL "$opencode_url" -o "${INSTALL_DIR}/bin/opencode" + chmod +x "${INSTALL_DIR}/bin/opencode" + + add_to_path + log "OpenCode installed successfully" +} + +download_flock() { + log "Downloading Flock..." + + if [[ -d "${INSTALL_DIR}/flock/.git" ]]; then + cd "${INSTALL_DIR}/flock" + git fetch origin main + git reset --hard origin/main + log "Flock updated to latest" + else + rm -rf "${INSTALL_DIR}/flock" + git clone --depth 1 "$FLOCK_REPO" "${INSTALL_DIR}/flock" + log "Flock downloaded" + fi +} + +build_flock() { + log "Building Flock..." + + cd "${INSTALL_DIR}/flock" + go build -o "${INSTALL_DIR}/bin/flock" ./cmd/flock + + log "Flock built successfully" +} + +create_flock_config() { + log "Creating Flock configuration..." + + mkdir -p "${INSTALL_DIR}" + + cat > "${INSTALL_DIR}/flock.toml" << EOF +opencode_url = "http://127.0.0.1:4096" +addr = ":7070" +db = "${INSTALL_DIR}/flock.db" +data_dir = "${INSTALL_DIR}" +base_path = "${INSTALL_DIR}/worktrees" + +[agent] +enabled = true +heartbeat_interval_secs = 300 +stuck_threshold_secs = 600 +max_heartbeats_per_session = 20 +wait_for_idle_timeout_secs = 180 + +[auth] +username = "" +password = "" +EOF + + log "Flock config created at ${INSTALL_DIR}/flock.toml" +} + +create_opencode_config() { + log "Creating OpenCode configuration..." + + mkdir -p "${INSTALL_DIR}/opencode" + + cat > "${INSTALL_DIR}/opencode/config.json" << EOF +{ + "$schema": "https://opencode.ai/config.json", + "server": { + "port": 4096 + }, + "permission": { + "*": { + "*": "allow" + } + } +} +EOF + + log "OpenCode config created" +} + +setup_systemd_services() { + log "Setting up systemd services..." + + sudo tee /etc/systemd/system/flock.service > /dev/null << EOF +[Unit] +Description=Flock - Agent Orchestration System +After=network.target + +[Service] +Type=simple +User=$USER +WorkingDirectory=${INSTALL_DIR} +Environment="PATH=${INSTALL_DIR}/bin:/usr/local/go/bin:$PATH" +ExecStart=${INSTALL_DIR}/bin/flock -config ${INSTALL_DIR}/flock.toml +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + + sudo tee /etc/systemd/system/opencode.service > /dev/null << EOF +[Unit] +Description=OpenCode Server +After=network.target + +[Service] +Type=simple +User=$USER +WorkingDirectory=${INSTALL_DIR} +Environment="PATH=${INSTALL_DIR}/bin:/usr/local/go/bin:$PATH" +ExecStart=${INSTALL_DIR}/bin/opencode serve --config ${INSTALL_DIR}/opencode/config.json +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + + sudo systemctl daemon-reload + sudo systemctl enable flock + sudo systemctl enable opencode + + log "Systemd services configured" +} + +setup_launchd_services() { + log "Setting up launchd services..." + + mkdir -p "${HOME}/Library/LaunchAgents" + cat > "${HOME}/Library/LaunchAgents/com.flock.flock.plist" << EOF + + + + + Label + com.flock.flock + ProgramArguments + + ${INSTALL_DIR}/bin/flock + -config + ${INSTALL_DIR}/flock.toml + + WorkingDirectory + ${INSTALL_DIR} + RunAtLoad + + KeepAlive + + EnvironmentVariables + + PATH + ${INSTALL_DIR}/bin:/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin + + + +EOF + + cat > "${HOME}/Library/LaunchAgents/com.flock.opencode.plist" << EOF + + + + + Label + com.flock.opencode + ProgramArguments + + ${INSTALL_DIR}/bin/opencode + serve + --config + ${INSTALL_DIR}/opencode/config.json + + WorkingDirectory + ${INSTALL_DIR} + RunAtLoad + + KeepAlive + + EnvironmentVariables + + PATH + ${INSTALL_DIR}/bin:/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin + + + +EOF + + log "Launchd services configured" +} + +setup_services() { + local os_type + os_type=$(detect_os) + + if [[ "$os_type" == "macos" ]]; then + setup_launchd_services + else + setup_systemd_services + fi +} + +start_services() { + local os_type + os_type=$(detect_os) + + log "Starting services..." + + if [[ "$os_type" == "macos" ]]; then + launchctl load "${HOME}/Library/LaunchAgents/com.flock.opencode.plist" + launchctl load "${HOME}/Library/LaunchAgents/com.flock.flock.plist" + else + sudo systemctl start opencode + sudo systemctl start flock + fi + + log "Services started" +} + +authenticate_github() { + if [[ "$SKIP_AUTH" == "true" ]]; then + log "Skipping GitHub authentication (SKIP_AUTH=true)" + return 0 + fi + + if gh auth status &> /dev/null; then + log "GitHub CLI already authenticated" + return 0 + fi + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + log "GitHub CLI requires authentication. Run: gh auth login" + return 1 + fi + + log "Please authenticate with GitHub CLI..." + gh auth login + + log "GitHub CLI authenticated" +} + +authenticate_opencode() { + if [[ "$SKIP_AUTH" == "true" ]]; then + log "Skipping OpenCode authentication (SKIP_AUTH=true)" + return 0 + fi + + if [[ -f "${INSTALL_DIR}/opencode/config.json" ]]; then + local opencode_url + opencode_url="http://127.0.0.1:4096" + + if curl -s "${opencode_url}/session" &> /dev/null; then + log "OpenCode server accessible" + return 0 + fi + fi + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + log "OpenCode requires authentication. Run: opencode auth login" + return 1 + fi + + log "Please authenticate with OpenCode..." + log "Please run: opencode auth login" + + return 0 +} + +setup_flock_auth() { + if [[ "$SKIP_AUTH" == "true" ]]; then + log "Skipping Flock auth setup (SKIP_AUTH=true)" + return 0 + fi + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + if [[ -z "${FLOCK_USERNAME:-}" ]] || [[ -z "${FLOCK_PASSWORD:-}" ]]; then + log "FLOCK_USERNAME and FLOCK_PASSWORD environment variables required in non-interactive mode" + return 1 + fi + else + echo "" + echo "Configure Flock basic authentication (optional - press Enter to skip):" + read -p "Username: " username + read -s -p "Password: " password + echo "" + + FLOCK_USERNAME="$username" + FLOCK_PASSWORD="$password" + fi + + if [[ -n "$FLOCK_USERNAME" ]] && [[ -n "$FLOCK_PASSWORD" ]]; then + log "Setting up Flock basic authentication..." + + sed -i.bak "s/username = \"\"/username = \"$FLOCK_USERNAME\"/" "${INSTALL_DIR}/flock.toml" + sed -i "s/password = \"\"/password = \"$FLOCK_PASSWORD\"/" "${INSTALL_DIR}/flock.toml" + + log "Flock auth configured" + else + log "Flock auth skipped (no credentials provided)" + fi +} + +install_nginx() { + if command -v nginx &> /dev/null; then + log "Nginx already installed" + return 0 + fi + + log "Installing Nginx..." + local pkg_manager + pkg_manager=$(detect_package_manager) + + case "$pkg_manager" in + apt) + sudo apt update + sudo apt install -y nginx + ;; + yum) + sudo yum install -y nginx + ;; + brew) + brew install nginx + ;; + *) + log "Unknown package manager, cannot install nginx" + return 1 + ;; + esac + + log "Nginx installed" +} + +setup_reverse_proxy() { + if [[ -z "$DOMAIN" ]]; then + log "No domain specified, skipping reverse proxy setup" + return 0 + fi + + log "Setting up reverse proxy for domain: $DOMAIN" + + install_nginx + + log "Creating nginx configuration for $DOMAIN..." + + sudo tee "/etc/nginx/sites-available/flock" > /dev/null << EOF +server { + listen 80; + server_name $DOMAIN; + + location / { + proxy_pass http://127.0.0.1:7070; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_cache_bypass \$http_upgrade; + + proxy_buffering off; + proxy_read_timeout 86400; + } + + location /event { + proxy_pass http://127.0.0.1:7070; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + + proxy_buffering off; + proxy_read_timeout 86400; + } +} +EOF + + sudo ln -sf "/etc/nginx/sites-available/flock" "/etc/nginx/sites-enabled/flock" + sudo rm -f "/etc/nginx/sites-enabled/default" + + sudo nginx -t + + log "Nginx configuration created" +} + +install_certbot() { + if command -v certbot &> /dev/null; then + log "Certbot already installed" + return 0 + fi + + log "Installing Certbot..." + local pkg_manager + pkg_manager=$(detect_package_manager) + + case "$pkg_manager" in + apt) + sudo apt update + sudo apt install -y certbot python3-certbot-nginx + ;; + yum) + sudo yum install -y certbot python3-certbot-nginx + ;; + brew) + brew install certbot + ;; + *) + log "Unknown package manager, cannot install certbot" + return 1 + ;; + esac + + log "Certbot installed" +} + +setup_ssl() { + if [[ -z "$DOMAIN" ]]; then + return 0 + fi + + log "Setting up SSL certificate for $DOMAIN..." + + install_certbot + + log "Requesting SSL certificate..." + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + sudo certbot --nginx -d "$DOMAIN" --agree-tos --email "admin@$DOMAIN" --redirect + else + sudo certbot --nginx -d "$DOMAIN" --redirect + fi + + log "SSL certificate configured" + + sudo systemctl restart nginx + sudo systemctl restart flock +} + +setup_cronjob() { + log "Setting up cronjob for automatic updates..." + + local cron_script="${INSTALL_DIR}/bin/flock-update.sh" + + cat > "$cron_script" << 'CRONSCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +INSTALL_DIR="${FLOCK_INSTALL_DIR:-$HOME/.flock}" +LOG_FILE="${INSTALL_DIR}/logs/update.log" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" +} + +log "Starting Flock and OpenCode update..." + +if [[ -d "${INSTALL_DIR}/flock/.git" ]]; then + cd "${INSTALL_DIR}/flock" + git fetch origin main + if git diff --quiet origin/main -- .; then + log "Flock already up to date" + else + git reset --hard origin/main + go build -o "${INSTALL_DIR}/bin/flock" ./cmd/flock + log "Flock updated" + fi +fi + +if command -v opencode &> /dev/null; then + local current_version + current_version=$(opencode --version 2>/dev/null || echo "unknown") + opencode --version-check || true + log "OpenCode current version: $current_version" +fi + +log "Restarting services..." + +if command -v systemctl &> /dev/null; then + sudo systemctl restart opencode + sleep 2 + sudo systemctl restart flock +elif command -v launchctl &> /dev/null; then + launchctl unload "${HOME}/Library/LaunchAgents/com.flock.opencode.plist" 2>/dev/null || true + launchctl unload "${HOME}/Library/LaunchAgents/com.flock.flock.plist" 2>/dev/null || true + sleep 1 + launchctl load "${HOME}/Library/LaunchAgents/com.flock.opencode.plist" + launchctl load "${HOME}/Library/LaunchAgents/com.flock.flock.plist" +fi + +log "Update complete" +CRONSCRIPT + + chmod +x "$cron_script" + + (crontab -l 2>/dev/null | grep -v "flock-update.sh"; echo "0 3 * * * ${INSTALL_DIR}/bin/flock-update.sh >> ${INSTALL_DIR}/logs/update.log 2>&1") | crontab - + + log "Cronjob configured to run daily at 3 AM" +} + +add_to_path() { + local shell_rc + case "${SHELL:-bash}" in + *zsh) shell_rc="${HOME}/.zshrc";; + *) shell_rc="${HOME}/.bashrc";; + esac + + if ! grep -q "${INSTALL_DIR}/bin" "$shell_rc" 2>/dev/null; then + echo "export PATH=\"${INSTALL_DIR}/bin:\$PATH\"" >> "$shell_rc" + fi + export PATH="${INSTALL_DIR}/bin:$PATH" +} + +verify_installation() { + log "Verifying installation..." + + command -v flock || { log "ERROR: flock not found in PATH"; return 1; } + command -v opencode || { log "ERROR: opencode not found in PATH"; return 1; } + command -v gh || { log "ERROR: gh not found in PATH"; return 1; } + + [[ -f "${INSTALL_DIR}/flock.toml" ]] || { log "ERROR: flock.toml not found"; return 1; } + [[ -f "${INSTALL_DIR}/opencode/config.json" ]] || { log "ERROR: opencode config not found"; return 1; } + + log "Installation verified successfully!" + return 0 +} + +print_summary() { + echo "" + echo "============================================" + echo " Flock Installation Complete!" + echo "============================================" + echo "" + echo "Installation directory: ${INSTALL_DIR}" + echo "" + echo "Services:" + echo " - Flock: ${INSTALL_DIR}/bin/flock" + echo " - OpenCode: ${INSTALL_DIR}/bin/opencode" + echo "" + echo "Configuration:" + echo " - Flock: ${INSTALL_DIR}/flock.toml" + echo " - OpenCode: ${INSTALL_DIR}/opencode/config.json" + echo "" + + if [[ -n "$DOMAIN" ]]; then + echo "Reverse Proxy:" + echo " - Domain: https://${DOMAIN}" + echo " - SSL: Enabled (certbot)" + echo " - Nginx: Configured" + echo "" + else + echo "Next steps:" + echo " 1. Add to PATH: export PATH=\"${INSTALL_DIR}/bin:\$PATH\"" + echo " 2. Authenticate GitHub: gh auth login" + echo " 3. Authenticate OpenCode: opencode auth login" + echo " 4. Access Flock UI: http://localhost:7070" + echo "" + fi + + echo "To start services manually:" + if [[ "$(detect_os)" == "macos" ]]; then + echo " launchctl load ${HOME}/Library/LaunchAgents/com.flock.flock.plist" + else + echo " sudo systemctl start flock" + fi + echo "" +} + +main() { + log "Starting Flock installation..." + log "Install directory: ${INSTALL_DIR}" + log "OS: $(detect_os)" + + while [[ $# -gt 0 ]]; do + case "$1" in + --install-dir) + INSTALL_DIR="$2" + shift 2 + ;; + --domain) + DOMAIN="$2" + shift 2 + ;; + --non-interactive) + NON_INTERACTIVE="true" + shift + ;; + --skip-auth) + SKIP_AUTH="true" + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac + done + + mkdir -p "${INSTALL_DIR}/bin" + mkdir -p "${INSTALL_DIR}/logs" + + install_golang + install_github_cli + install_opencode + + download_flock + build_flock + + create_flock_config + create_opencode_config + + setup_services + start_services + + authenticate_github || true + authenticate_opencode || true + + setup_flock_auth + + setup_reverse_proxy + setup_ssl + + setup_cronjob + + add_to_path + verify_installation + print_summary + + log "Installation complete!" +} + +main "$@"