- Refactor monolithic index.js (2646 lines) into src/{webex,integrations,
cards,flows,commands,services} modules; replace node-fetch/form-data
with native fetch/FormData; move all secrets to .env via dotenv
- Add dockerized remote SIW agent (docker/remote-agent/) with cross-arch
buildx packaging (arm64 Mac -> linux/amd64), idempotent install.sh
deploy bundle, and docker-free ZIP inspector for arch verification
- Bot hosts a WebSocket server; agent proxies SIW requests with a
per-request insecure:true flag, replacing the process-wide
NODE_TLS_REJECT_UNAUTHORIZED bypass
- Add ESLint flat config + Prettier, rewrite Dockerfile as non-root
multi-stage node:22-alpine build, README covering setup / deploy /
remote agent workflow
- Fix parseStoreArg to read trigger.prompt correctly (was indexing past
the framework's post-match slice); register /help as regex (string
matcher only compares the first token); switch catch-all to /.+/
(previous /.*/gim was stateful due to the g flag); remove
/fixDisplayNames command and its flow/card
Co-authored-by: Cursor <cursoragent@cursor.com>
334 lines
14 KiB
Bash
Executable file
334 lines
14 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# Package the wbxStoreProvision remote agent into a self-contained ZIP
|
|
# for offline / manual transfer to a remote Docker host.
|
|
#
|
|
# What this script does:
|
|
# 1. Reads the version from docker/remote-agent/package.json.
|
|
# 2. Builds `wbxprov-remote-agent:<version>` from the local source tree.
|
|
# 3. `docker save`s the image, gzip-compressed, into a temp staging dir.
|
|
# 4. Copies deploy/docker-compose.yml, deploy/install.sh, deploy/README.md,
|
|
# and .env.example into the staging dir. Rewrites the compose file's
|
|
# __VERSION__ placeholder to match the built image tag.
|
|
# 5. Writes VERSION and SHA256SUMS files for identification / integrity.
|
|
# 6. Zips the whole staging dir into docker/remote-agent/dist/.
|
|
#
|
|
# Usage:
|
|
# ./docker/remote-agent/package.sh # tag=package.json, platform=linux/amd64
|
|
# ./docker/remote-agent/package.sh --tag 1.0.1 # override tag
|
|
# ./docker/remote-agent/package.sh --platform linux/arm64 # ARM Linux target
|
|
# ./docker/remote-agent/package.sh --platform linux/amd64 # explicit default (Linux RH/Rocky/CentOS/Ubuntu on Intel)
|
|
#
|
|
# The image is ALWAYS built for the target platform via `docker buildx
|
|
# build --platform ...` so the tarball you ship matches the remote host.
|
|
# Default is linux/amd64 because that's the overwhelmingly common Linux
|
|
# server architecture; override with --platform if your remote host is
|
|
# something else (e.g. linux/arm64 for a Raspberry Pi or ARM-based server).
|
|
#
|
|
# Requires: docker (with buildx), zip, node (for reading package.json),
|
|
# sha256sum OR shasum (macOS ships shasum by default).
|
|
|
|
set -euo pipefail
|
|
|
|
# --- Locations --------------------------------------------------------------
|
|
|
|
# The build context is this directory (docker/remote-agent), which contains
|
|
# both the Dockerfile and the single agent source file. This is intentionally
|
|
# self-contained — the agent doesn't need any of the bot's source.
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
CONTEXT_DIR="$SCRIPT_DIR"
|
|
DIST_DIR="$SCRIPT_DIR/dist"
|
|
DEPLOY_DIR="$SCRIPT_DIR/deploy"
|
|
DOCKERFILE="$SCRIPT_DIR/Dockerfile"
|
|
|
|
# --- Colors -----------------------------------------------------------------
|
|
|
|
GRN=$'\033[0;32m'
|
|
YLW=$'\033[1;33m'
|
|
RED=$'\033[0;31m'
|
|
RST=$'\033[0m'
|
|
|
|
log() { printf '%s[package]%s %s\n' "$GRN" "$RST" "$*"; }
|
|
warn() { printf '%s[package]%s %s\n' "$YLW" "$RST" "$*"; }
|
|
die() { printf '%s[package]%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; }
|
|
|
|
# --- Argument parsing -------------------------------------------------------
|
|
|
|
VERSION=""
|
|
# Default target platform. Overwhelming majority of Linux server hosts
|
|
# (RHEL, Rocky, CentOS, Ubuntu, Debian) run on x86_64. Override with
|
|
# --platform for ARM Linux (linux/arm64) or anything else.
|
|
PLATFORM="linux/amd64"
|
|
CHECK_ONLY=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--tag)
|
|
shift
|
|
VERSION="${1:-}"
|
|
shift || true
|
|
;;
|
|
--platform)
|
|
shift
|
|
PLATFORM="${1:-}"
|
|
shift || true
|
|
;;
|
|
--check)
|
|
# Preflight only: verify docker/buildx/binfmt/builder are set up
|
|
# for the requested --platform. Do not build anything.
|
|
CHECK_ONLY=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
grep '^#' "$0" | sed 's/^# \{0,1\}//'
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "Unknown argument: $1 (try --help)"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$PLATFORM" ]] || die "--platform requires a value (e.g. linux/amd64, linux/arm64)"
|
|
|
|
# --- Preflight --------------------------------------------------------------
|
|
|
|
command -v docker >/dev/null 2>&1 || die "docker not found on PATH."
|
|
command -v zip >/dev/null 2>&1 || die "zip not found on PATH."
|
|
command -v node >/dev/null 2>&1 || die "node not found on PATH."
|
|
|
|
# buildx is required so we can cross-build for a specific target platform
|
|
# on any host (e.g. build linux/amd64 from an Apple Silicon Mac).
|
|
docker buildx version >/dev/null 2>&1 \
|
|
|| die "docker buildx not available. Install Docker Desktop or the buildx plugin."
|
|
|
|
if [[ -z "$VERSION" ]]; then
|
|
VERSION="$(node -p "require('$SCRIPT_DIR/package.json').version")"
|
|
fi
|
|
[[ -n "$VERSION" ]] || die "Could not determine version."
|
|
|
|
log "Packaging wbxprov-remote-agent version: ${VERSION}"
|
|
log "Target platform: ${PLATFORM}"
|
|
|
|
IMAGE_TAG="wbxprov-remote-agent:${VERSION}"
|
|
BUNDLE_NAME="wbxprov-remote-agent-${VERSION}"
|
|
STAGING_DIR="$(mktemp -d)"
|
|
STAGING_ROOT="$STAGING_DIR/$BUNDLE_NAME"
|
|
mkdir -p "$STAGING_ROOT"
|
|
|
|
# Guarantee cleanup even on error.
|
|
cleanup() { rm -rf "$STAGING_DIR"; }
|
|
trap cleanup EXIT
|
|
|
|
# Normalize host arch into a linux/* platform string for cross-build detection.
|
|
HOST_ARCH_RAW="$(uname -m)"
|
|
case "$HOST_ARCH_RAW" in
|
|
x86_64|amd64) HOST_LINUX_PLATFORM="linux/amd64" ;;
|
|
aarch64|arm64) HOST_LINUX_PLATFORM="linux/arm64" ;;
|
|
armv7l) HOST_LINUX_PLATFORM="linux/arm/v7" ;;
|
|
*) HOST_LINUX_PLATFORM="linux/${HOST_ARCH_RAW}" ;;
|
|
esac
|
|
|
|
EXPECTED_ARCH="${PLATFORM##*/}"
|
|
|
|
# --- 1. Ensure a cross-arch-capable buildx builder --------------------------
|
|
# The DEFAULT buildx builder on Docker Desktop uses the "docker" driver, which
|
|
# is tied to the daemon's native platform. Passing --platform linux/amd64 on
|
|
# an arm64 host with that driver can silently produce an arm64 image (which
|
|
# is exactly the "image arch does not match host" failure the install.sh
|
|
# sanity check catches on the target machine).
|
|
#
|
|
# We work around it by creating (once) a dedicated builder with the
|
|
# "docker-container" driver, which spins up an isolated BuildKit instance
|
|
# capable of cross-arch builds when QEMU/binfmt is available.
|
|
|
|
BUILDER_NAME="wbxprov-remote-agent-builder"
|
|
CURRENT_BUILDER_DRIVER=""
|
|
if docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
|
|
CURRENT_BUILDER_DRIVER="$(docker buildx inspect "$BUILDER_NAME" 2>/dev/null | awk -F': *' '/^Driver:/ {print $2; exit}')"
|
|
fi
|
|
|
|
if [[ -z "$CURRENT_BUILDER_DRIVER" ]]; then
|
|
log "Creating dedicated buildx builder '${BUILDER_NAME}' (docker-container driver)..."
|
|
docker buildx create \
|
|
--name "$BUILDER_NAME" \
|
|
--driver docker-container \
|
|
--bootstrap >/dev/null
|
|
elif [[ "$CURRENT_BUILDER_DRIVER" != "docker-container" ]]; then
|
|
# A builder with this exact name exists but uses the wrong driver — the
|
|
# plain `docker` driver, most likely — which is the exact silent-fail
|
|
# source: it's pinned to the daemon's native arch and will happily ignore
|
|
# --platform. Recreate it correctly.
|
|
warn "Existing builder '${BUILDER_NAME}' uses driver '${CURRENT_BUILDER_DRIVER}', not 'docker-container'."
|
|
warn "Recreating it so cross-arch builds actually respect --platform..."
|
|
docker buildx rm "$BUILDER_NAME" >/dev/null 2>&1 || true
|
|
docker buildx create \
|
|
--name "$BUILDER_NAME" \
|
|
--driver docker-container \
|
|
--bootstrap >/dev/null
|
|
else
|
|
# Right builder, right driver — just make sure it's up.
|
|
docker buildx inspect --bootstrap "$BUILDER_NAME" >/dev/null
|
|
fi
|
|
|
|
# --- 2. Cross-arch binfmt (only when needed) --------------------------------
|
|
# Docker Desktop ships QEMU/binfmt handlers by default so this usually no-ops,
|
|
# but plain Docker Engine, Colima, or rootless setups often don't. When we're
|
|
# cross-building, best-effort install binfmt for the target arch. If it fails
|
|
# (e.g. no --privileged, no internet, no image), warn but continue — the
|
|
# subsequent build step will fail fast with a clearer error if binfmt truly
|
|
# is missing.
|
|
|
|
if [[ "$PLATFORM" != "$HOST_LINUX_PLATFORM" ]]; then
|
|
log "Cross-arch build (${HOST_LINUX_PLATFORM} -> ${PLATFORM}); ensuring binfmt handlers..."
|
|
if ! docker run --privileged --rm tonistiigi/binfmt --install "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
|
warn "Could not auto-install binfmt for ${EXPECTED_ARCH}. If the build fails, install it manually:"
|
|
warn " docker run --privileged --rm tonistiigi/binfmt --install all"
|
|
fi
|
|
fi
|
|
|
|
# --- 2a. --check preflight short-circuit ------------------------------------
|
|
# If --check was passed, we've now verified: docker daemon reachable, buildx
|
|
# available, a docker-container builder exists (or was just recreated) for
|
|
# cross-arch, and binfmt handlers were attempted. Report and exit without
|
|
# building anything.
|
|
|
|
if (( CHECK_ONLY == 1 )); then
|
|
log ""
|
|
log "=========================================================="
|
|
log " Preflight OK for target platform: ${PLATFORM}"
|
|
log " host platform: ${HOST_LINUX_PLATFORM}"
|
|
log " builder: ${BUILDER_NAME} (docker-container)"
|
|
if [[ "$PLATFORM" != "$HOST_LINUX_PLATFORM" ]]; then
|
|
log " cross-arch: yes (binfmt registered above)"
|
|
else
|
|
log " cross-arch: no (native build)"
|
|
fi
|
|
log "=========================================================="
|
|
log "Re-run without --check to actually produce a bundle."
|
|
exit 0
|
|
fi
|
|
|
|
# --- 3. Build directly to a portable tarball --------------------------------
|
|
# `--output type=docker,dest=...` writes a `docker load`-compatible tarball
|
|
# straight to disk. This intentionally bypasses `--load` (and therefore the
|
|
# question of whether the local daemon can even store cross-arch images).
|
|
|
|
UNZIPPED_TAR="$STAGING_ROOT/${BUNDLE_NAME}.tar"
|
|
log "Building ${IMAGE_TAG} for ${PLATFORM} -> $(basename "$UNZIPPED_TAR") (context = ${CONTEXT_DIR})..."
|
|
docker buildx build \
|
|
--builder "$BUILDER_NAME" \
|
|
--platform "$PLATFORM" \
|
|
--output "type=docker,dest=${UNZIPPED_TAR},name=${IMAGE_TAG}" \
|
|
-f "$DOCKERFILE" \
|
|
"$CONTEXT_DIR"
|
|
|
|
[[ -s "$UNZIPPED_TAR" ]] || die "buildx produced no output tarball. Aborting."
|
|
|
|
# --- 4. Verify the built image actually matches --platform ------------------
|
|
# Regression guard: if buildx (or binfmt) silently ignored the requested
|
|
# platform, catch it here instead of shipping a broken bundle that only
|
|
# fails on the remote host with an "exec format error".
|
|
|
|
log "Verifying built image architecture..."
|
|
docker load -i "$UNZIPPED_TAR" >/dev/null
|
|
ACTUAL_ARCH="$(docker image inspect --format '{{.Architecture}}' "$IMAGE_TAG")"
|
|
if [[ "$ACTUAL_ARCH" != "$EXPECTED_ARCH" ]]; then
|
|
die "Built image architecture is '${ACTUAL_ARCH}' but '${EXPECTED_ARCH}' was requested.
|
|
This usually means buildx couldn't cross-compile for ${PLATFORM}.
|
|
Try installing binfmt handlers explicitly:
|
|
docker run --privileged --rm tonistiigi/binfmt --install all
|
|
Then re-run:
|
|
$0 --platform ${PLATFORM}"
|
|
fi
|
|
log "Verified: image architecture is ${ACTUAL_ARCH} (matches requested ${EXPECTED_ARCH})."
|
|
|
|
# Also tag :latest locally for convenience (only when it matches the host,
|
|
# so we don't leave a broken cross-arch :latest sitting in the daemon).
|
|
if [[ "$PLATFORM" == "$HOST_LINUX_PLATFORM" ]]; then
|
|
docker tag "$IMAGE_TAG" "wbxprov-remote-agent:latest" 2>/dev/null || true
|
|
fi
|
|
|
|
# --- 5. Compress the tarball ------------------------------------------------
|
|
|
|
IMAGE_TARBALL="${BUNDLE_NAME}.tar.gz"
|
|
log "Compressing image tarball -> ${IMAGE_TARBALL}..."
|
|
gzip -c "$UNZIPPED_TAR" > "$STAGING_ROOT/$IMAGE_TARBALL"
|
|
rm -f "$UNZIPPED_TAR"
|
|
|
|
TAR_SIZE_MB="$(du -m "$STAGING_ROOT/$IMAGE_TARBALL" | cut -f1)"
|
|
log "Image tarball size: ${TAR_SIZE_MB} MB"
|
|
|
|
# --- 6. Copy deploy assets --------------------------------------------------
|
|
|
|
log "Copying deploy assets into bundle..."
|
|
cp "$SCRIPT_DIR/.env.example" "$STAGING_ROOT/.env.example"
|
|
cp "$DEPLOY_DIR/install.sh" "$STAGING_ROOT/install.sh"
|
|
cp "$DEPLOY_DIR/README.md" "$STAGING_ROOT/README.md"
|
|
|
|
# Template the version into the runtime compose file so it references the
|
|
# specific image tag we just built.
|
|
sed "s|__VERSION__|${VERSION}|g" \
|
|
"$DEPLOY_DIR/docker-compose.yml" > "$STAGING_ROOT/docker-compose.yml"
|
|
|
|
chmod +x "$STAGING_ROOT/install.sh"
|
|
|
|
# --- 7. VERSION + SHA256SUMS -----------------------------------------------
|
|
|
|
printf '%s\n' "$VERSION" > "$STAGING_ROOT/VERSION"
|
|
|
|
log "Computing SHA-256 checksum for the image tarball..."
|
|
pushd "$STAGING_ROOT" >/dev/null
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$IMAGE_TARBALL" > SHA256SUMS
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
# macOS: shasum -a 256 emits the same "<hash> <filename>" format
|
|
# `sha256sum -c` understands.
|
|
shasum -a 256 "$IMAGE_TARBALL" > SHA256SUMS
|
|
else
|
|
warn "No sha256sum/shasum available — skipping checksum file."
|
|
fi
|
|
popd >/dev/null
|
|
|
|
# --- 8. Zip -----------------------------------------------------------------
|
|
|
|
mkdir -p "$DIST_DIR"
|
|
ZIP_PATH="$DIST_DIR/${BUNDLE_NAME}.zip"
|
|
rm -f "$ZIP_PATH"
|
|
|
|
log "Creating ZIP: ${ZIP_PATH}"
|
|
# Zip from inside the temp dir so the archive contains the top-level folder
|
|
# with the bundle name (matching what install.sh expects when unzipped).
|
|
(cd "$STAGING_DIR" && zip -qr "$ZIP_PATH" "$BUNDLE_NAME")
|
|
|
|
ZIP_SIZE_MB="$(du -m "$ZIP_PATH" | cut -f1)"
|
|
|
|
# --- 9. Independent post-build inspection ----------------------------------
|
|
# Final belt-and-suspenders: run inspect-bundle.sh against the ZIP we just
|
|
# produced. This reads the image config straight out of the tarball WITHOUT
|
|
# going through the docker daemon, so it catches anything a misbehaving
|
|
# daemon or `docker image inspect` might have hidden earlier.
|
|
|
|
INSPECT_SCRIPT="$SCRIPT_DIR/inspect-bundle.sh"
|
|
if [[ -x "$INSPECT_SCRIPT" ]]; then
|
|
log "Running independent (no-docker) inspection on the final ZIP..."
|
|
EXPECTED_OS=linux EXPECTED_ARCH="$EXPECTED_ARCH" \
|
|
"$INSPECT_SCRIPT" "$ZIP_PATH" \
|
|
|| die "Independent inspection FAILED. Bundle at $ZIP_PATH is not fit to ship."
|
|
else
|
|
warn "inspect-bundle.sh not found or not executable; skipping independent verification."
|
|
fi
|
|
|
|
# --- 10. Done ---------------------------------------------------------------
|
|
|
|
log ""
|
|
log "=========================================================="
|
|
log " Bundle ready:"
|
|
log " $ZIP_PATH"
|
|
log " (${ZIP_SIZE_MB} MB, built for ${PLATFORM})"
|
|
log "=========================================================="
|
|
log ""
|
|
log "Transfer to the remote host, then:"
|
|
log " unzip ${BUNDLE_NAME}.zip"
|
|
log " cd ${BUNDLE_NAME}"
|
|
log " ./install.sh"
|