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>
193 lines
6.9 KiB
Bash
Executable file
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"
|