netanalyzer/docker/remote-agent/package.sh
Joseph McQueen b3c37bd7df feat: st command suite, Webex phone + Atlas AV integrations, dockerized remote agent
Rebrand NetAnalyzer -> StoreHealthAnalyzer and consolidate the store
reporting surface into a single `st [number]` command with focused
sub-modes.

Commands
- st [number]                - general info (SIW + brands + Meraki net link)
- st [number] network        - switches, APs, store server
- st [number] pos            - registers, payment terminals, customer display
- st [number] ios            - MDM-tracked iOS hardware
- st [number] phone          - wired 78xx + DECT basestations/handsets with
                               registration state, extensions and main DID
- st [number] av             - Atlas AMPs + MDM-tracked Apple TVs, video
                               walls, music players, LED displays
- Removed `analyze` in favor of the unified `st` surface

Integrations
- integrations/webex: Service App OAuth with rotating refresh tokens,
  seed + cleanup scripts, tokens/ storage (git-ignored)
- integrations/atlas: Xyte client + cached device discovery keyed on
  zero-padded 6-digit store numbers, cold-cache failure -> unavailable
  banner instead of a misleading empty result
- services/webexPhone, services/webexService, services/avService: shape
  raw upstream data into the report layer's contract
- utils/merakiMatcher: FQDN hostname extraction so payment terminals
  match Meraki descriptions; case-insensitive lookup
- utils/chunkReport: split long markdown replies at 7000-char boundaries

Reliability / ops
- server.js: awaited framework.stop() + 8s hard-kill timer so nodemon /
  Docker restarts don't leak WDM device registrations ("excessive device
  registrations")
- nodemon.json: SIGINT so the graceful path always runs
- scripts/cleanupWebexDevices.js: one-shot WDM cleanup utility
- Group-space routing: hears() regexes tolerate the leading @BotName
  prefix Webex prepends to mentions
- Replaced HTML-unsafe <number> placeholders with [number] in all help
  strings

Remote agent containerization
- docker/remote-agent/: multi-stage node:22-alpine image, non-root user,
  tini for signal handling, minimal deps (ws/axios/dotenv)
- docker/remote-agent/package.sh: docker buildx build defaulting to
  linux/amd64 (with override), saves image + assembles deploy/ + writes
  SHA256 + zips for offline transfer
- docker/remote-agent/deploy/: runtime docker-compose.yml, install.sh
  with platform sanity check, remote-host README
- .dockerignore + .gitignore updates for build artifacts and dist bundles
- npm run agent:package convenience script

Cleanup
- Dropped storeHealth.js / HealthReport.js and their tests/mocks in favor
  of the shared storeDetail pipeline
- Store model handles null SIW records gracefully; toSummary always
  ends with a newline so the Meraki link sits on its own line

Tests
- 144 tests across 14 suites passing; new coverage for atlasClient,
  atlasDevices, avService, avCategory classification, webexPhone,
  webexServiceAppAuth, storeDetail integration, siw, chunkReport and
  the updated meraki matcher

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 09:54:41 -04:00

193 lines
6.9 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Package the StoreHealthAnalyzer 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 `sha-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 --------------------------------------------------------------
# Absolute path to this script's directory; then one level up is the repo root.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
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"
while [[ $# -gt 0 ]]; do
case "$1" in
--tag)
shift
VERSION="${1:-}"
shift || true
;;
--platform)
shift
PLATFORM="${1:-}"
shift || true
;;
-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 sha-remote-agent version: ${VERSION}"
log "Target platform: ${PLATFORM}"
IMAGE_TAG="sha-remote-agent:${VERSION}"
BUNDLE_NAME="sha-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
# --- 1. Build the image -----------------------------------------------------
log "Building Docker image ${IMAGE_TAG} for ${PLATFORM} (context = ${REPO_ROOT})..."
# buildx with --load emits the image straight into the local Docker daemon
# so `docker save` in the next step picks it up. --load only supports one
# platform at a time, which matches our "one ZIP per target" workflow.
docker buildx build \
--platform "$PLATFORM" \
--load \
-f "$DOCKERFILE" \
-t "$IMAGE_TAG" \
-t "sha-remote-agent:latest" \
"$REPO_ROOT"
# --- 2. Save the image to a gzipped tarball --------------------------------
IMAGE_TARBALL="${BUNDLE_NAME}.tar.gz"
log "Saving image to ${IMAGE_TARBALL}..."
docker save "$IMAGE_TAG" | gzip > "$STAGING_ROOT/$IMAGE_TARBALL"
TAR_SIZE_MB="$(du -m "$STAGING_ROOT/$IMAGE_TARBALL" | cut -f1)"
log "Image tarball size: ${TAR_SIZE_MB} MB"
# --- 3. 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"
# --- 4. 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
# --- 5. 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)"
# --- 6. 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"