Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.
- icehunter/ dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
closest analog to our agent's Dune docker control plane (compose
lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/ Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
Hyper-V self-host path + game-config schema
See docs/reference-repos/README.md for the full index + how we use each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178 lines
5.1 KiB
Bash
178 lines
5.1 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
send-dune-broadcast --title TITLE --message MESSAGE [--duration SECONDS]
|
|
send-dune-broadcast TITLE MESSAGE [SECONDS]
|
|
|
|
Options:
|
|
--namespace NAME Override auto-detected Kubernetes namespace
|
|
--mq-pod NAME Override auto-detected Game RabbitMQ pod
|
|
USAGE
|
|
}
|
|
|
|
namespace=""
|
|
mq_pod=""
|
|
title=""
|
|
message=""
|
|
duration="30"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--title|-t)
|
|
[[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }
|
|
title="$2"
|
|
shift 2
|
|
;;
|
|
--message|-m|--body|-b)
|
|
[[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }
|
|
message="$2"
|
|
shift 2
|
|
;;
|
|
--duration|-d|--duration-seconds)
|
|
[[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }
|
|
duration="$2"
|
|
shift 2
|
|
;;
|
|
--namespace|-n)
|
|
[[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }
|
|
namespace="$2"
|
|
shift 2
|
|
;;
|
|
--mq-pod)
|
|
[[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }
|
|
mq_pod="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--*)
|
|
echo "unknown option: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
if [[ -z "$title" ]]; then
|
|
title="$1"
|
|
elif [[ -z "$message" ]]; then
|
|
message="$1"
|
|
elif [[ "$duration" == "30" ]]; then
|
|
duration="$1"
|
|
else
|
|
echo "unexpected argument: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$title" || -z "$message" ]]; then
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
|
|
if ! [[ "$duration" =~ ^[0-9]+$ ]]; then
|
|
echo "duration must be an integer number of seconds" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if command -v kubectl >/dev/null 2>&1 && kubectl version --client >/dev/null 2>&1; then
|
|
kubectl_cmd=(kubectl)
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
kubectl_cmd=(sudo kubectl)
|
|
else
|
|
echo "kubectl not found, and sudo is unavailable" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$namespace" ]]; then
|
|
mapfile -t namespace_candidates < <("${kubectl_cmd[@]}" get pods -A --no-headers -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name 2>/dev/null \
|
|
| awk '$1 ~ /^funcom-seabass-/ && $2 ~ /-mq-game-sts-0$/ { print $1 }' \
|
|
| sort -u)
|
|
|
|
if [[ ${#namespace_candidates[@]} -eq 0 ]]; then
|
|
echo "could not auto-detect a funcom-seabass namespace with a Game RMQ pod" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ${#namespace_candidates[@]} -gt 1 ]]; then
|
|
echo "multiple candidate namespaces found; pass --namespace NAME" >&2
|
|
printf ' %s\n' "${namespace_candidates[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
namespace="${namespace_candidates[0]}"
|
|
fi
|
|
|
|
if [[ -z "$mq_pod" ]]; then
|
|
mq_pod=$("${kubectl_cmd[@]}" get pods -n "$namespace" --no-headers -o custom-columns=NAME:.metadata.name 2>/dev/null \
|
|
| awk '/-mq-game-sts-0$/ { print; exit }')
|
|
|
|
if [[ -z "$mq_pod" ]]; then
|
|
echo "could not auto-detect Game RMQ pod in namespace: $namespace" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
json_escape() {
|
|
python3 -c 'import json,sys; print(json.dumps(sys.argv[1])[1:-1])' "$1"
|
|
}
|
|
|
|
title_json=$(json_escape "$title")
|
|
message_json=$(json_escape "$message")
|
|
|
|
eval_file=$(mktemp)
|
|
cleanup() {
|
|
rm -f "$eval_file"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
cat > "$eval_file" <<EOF
|
|
Title = unicode:characters_to_binary(<<"$title_json">>, utf8),
|
|
Body = unicode:characters_to_binary(<<"$message_json">>, utf8),
|
|
Duration = $duration,
|
|
EntryEn = #{<<"Key">> => <<"en">>, <<"Title">> => Title, <<"Body">> => Body},
|
|
EntryEnUs = #{<<"Key">> => <<"en-US">>, <<"Title">> => Title, <<"Body">> => Body},
|
|
Inner = iolist_to_binary(rabbit_json:encode(#{
|
|
<<"ServerCommand">> => <<"ServiceBroadcast">>,
|
|
<<"BroadcastType">> => <<"Generic">>,
|
|
<<"BroadcastPayload">> => #{
|
|
<<"BroadcastDuration">> => Duration,
|
|
<<"LocalizedText">> => [EntryEn, EntryEnUs]
|
|
}
|
|
})),
|
|
Outer = iolist_to_binary(rabbit_json:encode(#{
|
|
<<"Version">> => 2,
|
|
<<"AuthToken">> => <<"Nu6VmPWUMvdPMeB7qErr">>,
|
|
<<"MessageContent">> => Inner
|
|
})),
|
|
XName = rabbit_misc:r(<<"/">>, exchange, <<"heartbeats">>),
|
|
X = rabbit_exchange:lookup_or_die(XName),
|
|
MsgId = list_to_binary("manual-service-broadcast-" ++ integer_to_list(erlang:system_time(millisecond))),
|
|
P = {list_to_atom("P_basic"), <<"Content">>, undefined, [], undefined,
|
|
undefined, undefined, undefined, undefined, MsgId, undefined,
|
|
undefined, <<"fls">>, <<"fls_backend">>, undefined},
|
|
Content = rabbit_basic:build_content(P, Outer),
|
|
{ok, Msg} = rabbit_basic:message(XName, <<"notifications">>, Content),
|
|
Result = rabbit_queue_type:publish_at_most_once(X, Msg),
|
|
io:format("publish=~p exchange=heartbeats routing=notifications app_id=fls_backend user_id=fls duration=~p title_bytes=~p body_bytes=~p~n",
|
|
[Result, Duration, byte_size(Title), byte_size(Body)]).
|
|
EOF
|
|
|
|
"${kubectl_cmd[@]}" exec -i -n "$namespace" "$mq_pod" -- sh -lc '
|
|
set -eu
|
|
export PATH=/opt/rabbitmq/sbin:/opt/erlang/lib/erlang/bin:/opt/erlang/lib/erlang/erts-14.2.5.12/bin:/bin:/usr/bin:/usr/local/bin:$PATH
|
|
cat > /tmp/dune-service-broadcast.erl
|
|
expr=$(cat /tmp/dune-service-broadcast.erl)
|
|
/opt/rabbitmq/sbin/rabbitmqctl eval "$expr"
|
|
rm -f /tmp/dune-service-broadcast.erl
|
|
' < "$eval_file" >/dev/null
|
|
|
|
echo "Message successfully broadcasted"
|