Files
copilotkit__copilotkit/showcase/bin/showcase
Jordan Ritter d178e67300 fix(showcase/harness): split --isolate compose into infra-up + target-build (A21b)
A21 (a9114a831) regression: positional slug after 'up' restricted which services start (only target+depends_on came up; infra profile services never started). With concurrent sibling stack on same host ports, health checks crossed to foreign pocketbase → cell results misrouted → 0.0s red. Fix: split into 2 compose calls — (1) compose infra up -d, then (2) compose --profile slug up -d --build slug. Preserves A21's target-only rebuild + restores full infra stack.

(cherry picked from commit d71ff46edb4a7b0a89f89d8126fd5fe3d5c7d24e)
2026-06-16 21:28:16 -07:00

219 lines
8.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# showcase — unified CLI for the CopilotKit showcase platform.
#
# Dispatches to built-in compose commands (up, down, build, ps, ports, logs)
# and to plugin commands defined in scripts/cli/cmd-*.sh files.
set -euo pipefail
# ── Bootstrap ────────────────────────────────────────────────────────────────
BIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SHOWCASE_ROOT="$(cd "$BIN_DIR/.." && pwd)"
export SHOWCASE_ROOT
# shellcheck source=../scripts/cli/_common.sh
source "$SHOWCASE_ROOT/scripts/cli/_common.sh"
# ── Auto-discover plugin commands (cmd-*.sh) ─────────────────────────────────
# Plugin names and their descriptions, stored as parallel arrays for bash 3
# compatibility (no associative arrays on macOS default bash).
_plugin_names=()
_plugin_descs=()
for cmd_file in "$SHOWCASE_ROOT"/scripts/cli/cmd-*.sh; do
[ -f "$cmd_file" ] || continue
# shellcheck disable=SC1090
source "$cmd_file"
# Extract command name: cmd-foo-bar.sh → foo-bar
_name="$(basename "$cmd_file" .sh)"
_name="${_name#cmd-}"
_plugin_names+=("$_name")
# Each cmd file should define CMD_<NAME>_DESC (dashes→underscores, uppercased)
# e.g. cmd-aimock-rebuild.sh defines CMD_AIMOCK_REBUILD_DESC
_desc_var="CMD_${_name//-/_}"
_desc_var="$(echo "$_desc_var" | tr '[:lower:]' '[:upper:]')_DESC"
_plugin_descs+=("${!_desc_var:-}")
done
# ── Built-in commands ────────────────────────────────────────────────────────
cmd_up() {
require_env
trap restore_symlinks EXIT
stage_shared
# Fleet topology: bring up the SAME shape prod runs — the control-plane
# container + worker container(s) + PocketBase + demos + aimock — NOT an
# in-process pool. The harness control-plane and pool worker(s) live in the
# `infra` profile (alongside aimock/pocketbase/dashboard), so always include
# `--profile infra`.
#
# Demo slugs (bare args like `langgraph-python`) select that demo's compose
# PROFILE — they must become `--profile <slug>` flags, NOT positional service
# names. A positional service arg restricts `up` to only that service + its
# deps, which (a) leaves the demo's own profile disabled and (b) makes
# `--scale harness-pool-worker=N` fail with "no such service:
# harness-pool-worker: disabled" because the worker is no longer in the active
# set. Raw compose flags (anything starting with `-`, e.g. `--no-build`) are
# passed through verbatim.
#
# HARNESS_POOL_COUNT drives the worker fleet size. Local default is 1 (a
# single worker container); staging/prod export HARNESS_POOL_COUNT=2. The
# compose `harness-pool-worker` service is scaled to that count via
# `--scale` so the same compose file scales from local (1) to prod (N)
# purely by the env var — no per-environment compose edits.
local pool_count="${HARNESS_POOL_COUNT:-1}"
export HARNESS_POOL_COUNT="$pool_count"
# Split args: bare slugs → `--profile <slug>` + build target; flags (-*) →
# pass through. `--dev` is intercepted (not passed to compose): it layers
# the dev overlay (docker-compose.dev.yml) which bind-mounts integration
# source + overrides the run command to a hot-reload entrypoint — fast
# iteration without an image rebuild. The built-image mode (no --dev) stays
# the faithful default.
local profile_args=()
local build_targets=()
local passthrough_args=()
local dev_mode=false
local arg
for arg in "$@"; do
if [[ "$arg" == "--dev" ]]; then
dev_mode=true
elif [[ "$arg" == -* ]]; then
passthrough_args+=("$arg")
else
profile_args+=(--profile "$arg")
build_targets+=("$arg")
fi
done
local compose_cmd="$COMPOSE_CMD"
if $dev_mode; then
compose_cmd="$compose_cmd -f $SHOWCASE_ROOT/docker-compose.dev.yml"
info "DEV MODE: bind-mounting integration source + hot reload (NOT the faithful built-image path)"
fi
info "Launching fleet topology: control-plane + ${pool_count} worker(s) + PocketBase + demos + aimock (HARNESS_POOL_COUNT=${pool_count})"
# Two-call strategy to preserve A21's BuildKit-contention fix (target-only
# rebuild) WITHOUT regressing infra startup that A21 inadvertently dropped
# (A21b, issue #5495).
#
# docker compose semantics: positional service names after `up` restrict
# WHICH services start to the named ones + their `depends_on` chain — not
# just which ones get `--build`-rebuilt. So a single
# `up -d --build <slug>` only brings up the slug + its depends_on (aimock),
# leaving the rest of the infra profile (pocketbase/dashboard/harness/
# harness-pool-worker) down. Under `--isolate` with a sibling stack on the
# same host ports, health checks then cross onto foreign containers and
# cells silently misroute → 0.0s red.
#
# Fix: when slugs are present, emit TWO compose calls:
# 1. <profiles> up -d (no --build, no positional services) — start all
# services in the active profiles using cached images.
# 2. <profiles> up -d --build <slug...> — force-rebuild ONLY the named
# services and ensure they're up. Other services from call (1) are
# no-ops.
# When no slugs are present (infra-only bring-up), keep the single blanket
# `--build` call so first-time bootstrap still builds missing images.
#
# bash 3.2 (macOS default) errors on `${arr[@]}` for an EMPTY array under
# `set -u`; the `${arr[@]+...}` guard expands to nothing when unset/empty.
if [[ ${#build_targets[@]} -gt 0 ]]; then
# Call 1: bring up ALL services in active profiles, no build.
$compose_cmd --profile infra ${profile_args[@]+"${profile_args[@]}"} up -d \
--scale harness-pool-worker="$pool_count" ${passthrough_args[@]+"${passthrough_args[@]}"}
# Call 2: rebuild ONLY the targeted slug(s) and ensure they're up.
$compose_cmd --profile infra ${profile_args[@]+"${profile_args[@]}"} up -d --build \
--scale harness-pool-worker="$pool_count" ${passthrough_args[@]+"${passthrough_args[@]}"} \
"${build_targets[@]}"
else
# Infra-only: single call with blanket --build for first-time bootstrap.
$compose_cmd --profile infra up -d --build \
--scale harness-pool-worker="$pool_count" ${passthrough_args[@]+"${passthrough_args[@]}"}
fi
}
cmd_down() {
$COMPOSE_CMD down "$@"
}
cmd_build() {
trap restore_symlinks EXIT
stage_shared
$COMPOSE_CMD build "$@"
}
cmd_ps() {
$COMPOSE_CMD ps "$@"
}
cmd_ports() {
if command -v jq &>/dev/null; then
jq -r 'to_entries[] | "\(.key)\t→ localhost:\(.value)"' "$PORTS_FILE"
else
cat "$PORTS_FILE"
fi
}
# ── Usage ────────────────────────────────────────────────────────────────────
usage() {
cat <<'HEADER'
Usage: showcase <command> [options]
Core commands:
up [slug...] Start containers (rebuilds if source changed)
--dev bind-mount integration source + hot reload
(fast iteration; NOT the faithful built-image path)
down [slug...] Stop containers
build [slug...] Build Docker images
ps Show running containers
ports Print slug → host port mapping
HEADER
# Print plugin commands if any are loaded
if [ ${#_plugin_names[@]} -gt 0 ]; then
echo ""
echo "Plugin commands:"
local i
for i in "${!_plugin_names[@]}"; do
printf " %-17s %s\n" "${_plugin_names[$i]}" "${_plugin_descs[$i]}"
done
fi
cat <<'FOOTER'
Run 'showcase <command> --help' for details on a specific command.
FOOTER
}
# ── Dispatch ─────────────────────────────────────────────────────────────────
subcmd="${1:-}"
shift || true
case "$subcmd" in
""|"-h"|"--help"|"help")
usage
[ -z "$subcmd" ] && exit 1
exit 0
;;
eval)
exec npx tsx "$SHOWCASE_ROOT/harness/src/cli.ts" eval "$@"
;;
*)
# Convert dashes to underscores for function lookup: foo-bar → cmd_foo_bar
func_name="cmd_${subcmd//-/_}"
if type "$func_name" 2>/dev/null | head -1 | grep -q 'function'; then
"$func_name" "$@"
else
echo "Unknown command: $subcmd" >&2
echo ""
usage
exit 1
fi
;;
esac