Skip to content

Commit 58b423d

Browse files
committed
feat(showcase): add unified CLI dispatcher and shared utilities
bin/showcase auto-discovers cmd-*.sh plugins, routes built-in commands (up/down/build/ps/ports), and provides stage_shared/restore_symlinks for Docker Compose volume management. _common.sh provides shared variables (SHOWCASE_ROOT, COMPOSE_CMD, COMPOSE_FILE), output helpers (die/info/warn/success), and container utilities (slug_to_container, slug_to_port, wait_healthy).
1 parent 8286d70 commit 58b423d

2 files changed

Lines changed: 232 additions & 2 deletions

File tree

showcase/bin/showcase

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,124 @@
11
#!/usr/bin/env bash
2-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
3-
exec npx tsx "$SCRIPT_DIR/../harness/src/cli.ts" "$@"
2+
# showcase — unified CLI for the CopilotKit showcase platform.
3+
#
4+
# Dispatches to built-in compose commands (up, down, build, ps, ports, logs)
5+
# and to plugin commands defined in scripts/cli/cmd-*.sh files.
6+
7+
set -euo pipefail
8+
9+
# ── Bootstrap ────────────────────────────────────────────────────────────────
10+
11+
BIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12+
SHOWCASE_ROOT="$(cd "$BIN_DIR/.." && pwd)"
13+
export SHOWCASE_ROOT
14+
15+
# shellcheck source=../scripts/cli/_common.sh
16+
source "$SHOWCASE_ROOT/scripts/cli/_common.sh"
17+
18+
# ── Auto-discover plugin commands (cmd-*.sh) ─────────────────────────────────
19+
20+
# Plugin names and their descriptions, stored as parallel arrays for bash 3
21+
# compatibility (no associative arrays on macOS default bash).
22+
_plugin_names=()
23+
_plugin_descs=()
24+
25+
for cmd_file in "$SHOWCASE_ROOT"/scripts/cli/cmd-*.sh; do
26+
[ -f "$cmd_file" ] || continue
27+
# shellcheck disable=SC1090
28+
source "$cmd_file"
29+
# Extract command name: cmd-foo-bar.sh → foo-bar
30+
_name="$(basename "$cmd_file" .sh)"
31+
_name="${_name#cmd-}"
32+
_plugin_names+=("$_name")
33+
# Each cmd file should define CMD_<NAME>_DESC (dashes→underscores, uppercased)
34+
# e.g. cmd-aimock-rebuild.sh defines CMD_AIMOCK_REBUILD_DESC
35+
_desc_var="CMD_${_name//-/_}"
36+
_desc_var="$(echo "$_desc_var" | tr '[:lower:]' '[:upper:]')_DESC"
37+
_plugin_descs+=("${!_desc_var:-}")
38+
done
39+
40+
# ── Built-in commands ────────────────────────────────────────────────────────
41+
42+
cmd_up() {
43+
require_env
44+
trap restore_symlinks EXIT
45+
stage_shared
46+
$COMPOSE_CMD up -d --build "$@"
47+
}
48+
49+
cmd_down() {
50+
$COMPOSE_CMD down "$@"
51+
}
52+
53+
cmd_build() {
54+
trap restore_symlinks EXIT
55+
stage_shared
56+
$COMPOSE_CMD build "$@"
57+
}
58+
59+
cmd_ps() {
60+
$COMPOSE_CMD ps "$@"
61+
}
62+
63+
cmd_ports() {
64+
if command -v jq &>/dev/null; then
65+
jq -r 'to_entries[] | "\(.key)\t→ localhost:\(.value)"' "$PORTS_FILE"
66+
else
67+
cat "$PORTS_FILE"
68+
fi
69+
}
70+
71+
# ── Usage ────────────────────────────────────────────────────────────────────
72+
73+
usage() {
74+
cat <<'HEADER'
75+
Usage: showcase <command> [options]
76+
77+
Core commands:
78+
up [slug...] Start containers (rebuilds if source changed)
79+
down [slug...] Stop containers
80+
build [slug...] Build Docker images
81+
ps Show running containers
82+
ports Print slug → host port mapping
83+
HEADER
84+
85+
# Print plugin commands if any are loaded
86+
if [ ${#_plugin_names[@]} -gt 0 ]; then
87+
echo ""
88+
echo "Plugin commands:"
89+
local i
90+
for i in "${!_plugin_names[@]}"; do
91+
printf " %-17s %s\n" "${_plugin_names[$i]}" "${_plugin_descs[$i]}"
92+
done
93+
fi
94+
95+
cat <<'FOOTER'
96+
97+
Run 'showcase <command> --help' for details on a specific command.
98+
FOOTER
99+
}
100+
101+
# ── Dispatch ─────────────────────────────────────────────────────────────────
102+
103+
subcmd="${1:-}"
104+
shift || true
105+
106+
case "$subcmd" in
107+
""|"-h"|"--help"|"help")
108+
usage
109+
[ -z "$subcmd" ] && exit 1
110+
exit 0
111+
;;
112+
*)
113+
# Convert dashes to underscores for function lookup: foo-bar → cmd_foo_bar
114+
func_name="cmd_${subcmd//-/_}"
115+
if type "$func_name" 2>/dev/null | head -1 | grep -q 'function'; then
116+
"$func_name" "$@"
117+
else
118+
echo "Unknown command: $subcmd" >&2
119+
echo ""
120+
usage
121+
exit 1
122+
fi
123+
;;
124+
esac

showcase/scripts/cli/_common.sh

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env bash
2+
# Shared variables and helper functions for the showcase CLI.
3+
# Sourced by bin/showcase — not meant to be executed directly.
4+
5+
# ── Paths ────────────────────────────────────────────────────────────────────
6+
7+
SHOWCASE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
8+
COMPOSE_FILE="$SHOWCASE_ROOT/docker-compose.local.yml"
9+
COMPOSE_CMD="docker compose -f $COMPOSE_FILE"
10+
ENV_FILE="$SHOWCASE_ROOT/.env"
11+
PORTS_FILE="$SHOWCASE_ROOT/shared/local-ports.json"
12+
AIMOCK_COMPOSE="$SHOWCASE_ROOT/tests/docker-compose.integrations.yml"
13+
14+
# ── Output helpers ───────────────────────────────────────────────────────────
15+
16+
die() {
17+
printf '\033[1;31m✗ %s\033[0m\n' "$1" >&2
18+
exit 1
19+
}
20+
21+
info() {
22+
printf '\033[0;36m▸ %s\033[0m\n' "$1"
23+
}
24+
25+
warn() {
26+
printf '\033[1;33m⚠ %s\033[0m\n' "$1" >&2
27+
}
28+
29+
success() {
30+
printf '\033[0;32m✓ %s\033[0m\n' "$1"
31+
}
32+
33+
# ── Validation helpers ───────────────────────────────────────────────────────
34+
35+
need_slug() {
36+
[ -n "${1:-}" ] || die "slug required"
37+
}
38+
39+
require_env() {
40+
[ -f "$ENV_FILE" ] || die "Missing $ENV_FILE. Copy showcase/.env.example to showcase/.env and fill in keys."
41+
}
42+
43+
# ── Docker / Compose helpers ─────────────────────────────────────────────────
44+
45+
stage_shared() {
46+
# Dereference tools/ and shared-tools/ symlinks into real copies so Docker
47+
# COPY can follow them (Docker build contexts can't traverse symlinks that
48+
# point outside the context).
49+
for pkg_dir in "$SHOWCASE_ROOT"/integrations/*/; do
50+
for link_name in tools shared-tools; do
51+
local link_path="$pkg_dir/$link_name"
52+
if [ -L "$link_path" ]; then
53+
local target
54+
target="$(readlink "$link_path")"
55+
# Resolve relative symlink targets against the link's directory
56+
if [[ "$target" != /* ]]; then
57+
target="$(cd "$(dirname "$link_path")" && cd "$(dirname "$target")" && pwd)/$(basename "$target")"
58+
fi
59+
if [ -d "$target" ]; then
60+
rm "$link_path"
61+
rsync -a "$target/" "$link_path/"
62+
fi
63+
fi
64+
done
65+
done
66+
}
67+
68+
restore_symlinks() {
69+
# Restore tools/ and shared-tools/ symlinks replaced by stage_shared.
70+
(cd "$SHOWCASE_ROOT" && git checkout -- integrations/*/tools integrations/*/shared-tools 2>/dev/null || true)
71+
}
72+
73+
slug_to_container() {
74+
echo "showcase-${1}"
75+
}
76+
77+
slug_to_port() {
78+
local slug="${1:?slug required}"
79+
if command -v jq &>/dev/null; then
80+
jq -r --arg s "$slug" '.[$s] // empty' "$PORTS_FILE"
81+
else
82+
# Fallback: simple grep/sed if jq is not available
83+
grep "\"$slug\"" "$PORTS_FILE" | sed 's/[^0-9]//g'
84+
fi
85+
}
86+
87+
is_service_healthy() {
88+
local slug="${1:?slug required}"
89+
local container
90+
container="$(slug_to_container "$slug")"
91+
local health
92+
health="$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "missing")"
93+
[ "$health" = "healthy" ]
94+
}
95+
96+
wait_healthy() {
97+
local slug="${1:?slug required}"
98+
local timeout="${2:-30}"
99+
local elapsed=0
100+
info "Waiting for $slug to become healthy (timeout ${timeout}s)..."
101+
while ! is_service_healthy "$slug"; do
102+
if [ "$elapsed" -ge "$timeout" ]; then
103+
die "$slug did not become healthy within ${timeout}s"
104+
fi
105+
sleep 2
106+
elapsed=$((elapsed + 2))
107+
done
108+
success "$slug is healthy (${elapsed}s)"
109+
}

0 commit comments

Comments
 (0)