f2fc8f771e
CI / build (push) Successful in 18s
CI / push-validation (push) Successful in 19s
CI / lint (push) Failing after 24s
CI / helm (push) Successful in 23s
CI / quality (push) Successful in 32s
CI / security (push) Successful in 59s
CI / typecheck (push) Successful in 1m7s
CI / coverage (push) Has been skipped
CI / benchmark-regression (push) Has been skipped
CI / e2e_tests (push) Successful in 3m4s
CI / integration_tests (push) Failing after 4m4s
CI / unit_tests (push) Successful in 5m9s
CI / docker (push) Has been skipped
CI / status-check (push) Failing after 1s
CI / benchmark-publish (push) Has been cancelled
- Add check_opencode_running() function to detect existing OpenCode server - Skip server startup if OpenCode already running on target port - Display prominent warning when connecting to existing server - Only manage server lifecycle when script starts its own server - Add OWN_SERVER flag to track server ownership - Update cleanup logic to leave existing servers running This enables multiple developers and automation scripts to safely run in parallel without conflicting server management.
173 lines
6.7 KiB
Bash
Executable File
173 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# opencode-builder.sh
|
|
#
|
|
# Launches an OpenCode server, creates a session with the product-builder
|
|
# agent, and continuously sends "continue" messages in an infinite loop.
|
|
#
|
|
# Signal handling:
|
|
# Ctrl+C once — finishes the in-flight request, then stops the loop
|
|
# Ctrl+C twice — (within 2 seconds) kills the request and exits immediately
|
|
#
|
|
|
|
set -uo pipefail
|
|
|
|
# ── Configuration (override via environment) ──────────────────────────────
|
|
PORT="${OPENCODE_PORT:-4096}"
|
|
HOST="${OPENCODE_HOST:-127.0.0.1}"
|
|
BASE="http://${HOST}:${PORT}"
|
|
AGENT="product-builder"
|
|
PROMPT="Complete the current project's milestones up to and including v3.7.0 to a production ready state"
|
|
HEALTH_TIMEOUT=60 # seconds to wait for the server to become healthy
|
|
|
|
# ── Internal state ────────────────────────────────────────────────────────
|
|
SERVER_PID=""
|
|
CURL_PID=""
|
|
SESSION_ID=""
|
|
STOP_LOOP=false
|
|
LAST_SIGINT=0
|
|
OWN_SERVER=false
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
die() { printf "ERROR: %s\n" "$*" >&2; exit 1; }
|
|
log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; }
|
|
|
|
check_opencode_running() {
|
|
# Check if port is open and responding as OpenCode server
|
|
if curl -sf "${BASE}/global/health" >/dev/null 2>&1; then
|
|
return 0 # OpenCode server is running
|
|
else
|
|
return 1 # No OpenCode server detected
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
if $OWN_SERVER && [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
log "Stopping opencode server (PID $SERVER_PID)..."
|
|
kill "$SERVER_PID" 2>/dev/null
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
elif ! $OWN_SERVER; then
|
|
log "Leaving existing OpenCode server running (not managed by this script)"
|
|
fi
|
|
}
|
|
|
|
handle_sigint() {
|
|
local now
|
|
now=$(date +%s)
|
|
|
|
# Second Ctrl+C within 2 seconds → force-quit
|
|
if $STOP_LOOP && (( now - LAST_SIGINT <= 2 )); then
|
|
printf "\nForce quit!\n"
|
|
[[ -n "$CURL_PID" ]] && kill "$CURL_PID" 2>/dev/null || true
|
|
cleanup
|
|
exit 130
|
|
fi
|
|
|
|
# First Ctrl+C → graceful stop after current request
|
|
STOP_LOOP=true
|
|
LAST_SIGINT=$now
|
|
printf "\nWill stop after the current request completes. Ctrl+C again within 2s to force-quit.\n"
|
|
}
|
|
|
|
trap handle_sigint INT
|
|
trap cleanup EXIT
|
|
|
|
# ── Preflight checks ─────────────────────────────────────────────────────
|
|
for cmd in curl jq opencode; do
|
|
command -v "$cmd" &>/dev/null || die "'$cmd' is required but not found in PATH"
|
|
done
|
|
|
|
# ── Start the opencode server ─────────────────────────────────────────────
|
|
# Check if OpenCode server is already running
|
|
if check_opencode_running; then
|
|
echo "┌─────────────────────────────────────────────────────────────────┐"
|
|
echo "│ ⚠️ WARNING: OpenCode server already running on ${HOST}:${PORT}"
|
|
echo "│ 🔗 Connecting to existing server instead of starting new one"
|
|
echo "│ 💡 The script will NOT manage this server's lifecycle"
|
|
echo "└─────────────────────────────────────────────────────────────────┘"
|
|
echo ""
|
|
log "Using existing OpenCode server at ${HOST}:${PORT}"
|
|
OWN_SERVER=false
|
|
else
|
|
# Start our own server as before
|
|
log "Starting opencode server on ${HOST}:${PORT}..."
|
|
opencode serve --port "$PORT" --hostname "$HOST" &
|
|
SERVER_PID=$!
|
|
OWN_SERVER=true
|
|
|
|
log "Waiting for server to become healthy (up to ${HEALTH_TIMEOUT}s)..."
|
|
healthy=false
|
|
for (( i = 1; i <= HEALTH_TIMEOUT; i++ )); do
|
|
if curl -sf "${BASE}/global/health" >/dev/null 2>&1; then
|
|
healthy=true
|
|
break
|
|
fi
|
|
kill -0 "$SERVER_PID" 2>/dev/null || die "Server process exited unexpectedly"
|
|
sleep 1
|
|
done
|
|
$healthy || die "Server did not become healthy within ${HEALTH_TIMEOUT}s"
|
|
log "Server is ready."
|
|
fi
|
|
|
|
# ── Create a session ──────────────────────────────────────────────────────
|
|
log "Creating session..."
|
|
SESSION_ID=$(
|
|
curl -sf -X POST "${BASE}/session" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"title":"product-builder-auto"}' \
|
|
| jq -r '.id'
|
|
) || die "Failed to create session"
|
|
|
|
[[ -n "$SESSION_ID" && "$SESSION_ID" != "null" ]] || die "Session ID is empty or null"
|
|
log "Session ID: ${SESSION_ID}"
|
|
|
|
# ── send_message ──────────────────────────────────────────────────────────
|
|
# Sends a JSON body to POST /session/:id/message and blocks until the LLM
|
|
# responds. curl is run in a subprocess that ignores SIGINT (SIG_IGN is
|
|
# inherited across exec), so a single Ctrl+C cannot kill the in-flight
|
|
# request. The parent re-waits whenever its own trap handler interrupts
|
|
# the `wait` builtin.
|
|
send_message() {
|
|
local body=$1
|
|
|
|
( trap '' INT
|
|
exec curl -sf -X POST "${BASE}/session/${SESSION_ID}/message" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$body" \
|
|
-o /dev/null
|
|
) &
|
|
CURL_PID=$!
|
|
|
|
# Keep waiting until curl actually exits — `wait` can return early when
|
|
# our SIGINT handler fires, so we loop on kill -0 to re-wait.
|
|
while kill -0 "$CURL_PID" 2>/dev/null; do
|
|
wait "$CURL_PID" 2>/dev/null || true
|
|
done
|
|
CURL_PID=""
|
|
}
|
|
|
|
# ── Send the initial prompt ──────────────────────────────────────────────
|
|
INIT_BODY=$(jq -nc --arg a "$AGENT" --arg t "$PROMPT" \
|
|
'{agent: $a, parts: [{type: "text", text: $t}]}')
|
|
|
|
log "Sending initial prompt (agent: ${AGENT})..."
|
|
log "Prompt: ${PROMPT}"
|
|
echo ""
|
|
send_message "$INIT_BODY"
|
|
log "Initial prompt complete."
|
|
|
|
# ── Continue loop ─────────────────────────────────────────────────────────
|
|
CONT_BODY=$(jq -nc '{parts: [{type: "text", text: "continue"}]}')
|
|
N=0
|
|
|
|
while ! $STOP_LOOP; do
|
|
N=$((N + 1))
|
|
log "── continue #${N} ──"
|
|
send_message "$CONT_BODY"
|
|
log "continue #${N} complete."
|
|
done
|
|
|
|
echo ""
|
|
log "Loop stopped. Shutting down."
|
|
exit 0
|