#!/bin/sh
# ==============================================================================
#  OpenSCM agent installer
# ==============================================================================
#
#  Installs the OpenSCM compliance agent (scmclient) on this machine and points
#  it at your OpenSCM server.
#
#      curl -fsSL https://repo.openscm.io/install.sh | sh -s -- \
#          --server https://scm.example.com
#
#  To remove it again:
#
#      curl -fsSL https://repo.openscm.io/install.sh | sh -s -- --uninstall
#
#  Add --purge to also drop the agent config, its logs and the package
#  repository. --purge removes only the AGENT's files: /etc/openscm is shared
#  with scmserver and holds its private signing key.
#
#  ---------------------------------------------------------------------------
#  WHAT THIS SCRIPT CHANGES ON YOUR SYSTEM
#  ---------------------------------------------------------------------------
#
#  1. Adds the OpenSCM package repository and its signing key
#       Debian/Ubuntu  /usr/share/keyrings/openscm.gpg
#                      /etc/apt/sources.list.d/openscm.list
#       RHEL/Fedora    /etc/yum.repos.d/openscm.repo
#     Arch, FreeBSD and macOS have no repository, so a single signed package is
#     downloaded instead and nothing repository-related is added.
#
#  2. Installs the `scmclient` package using your system's own package manager
#     (apt / dnf / yum / pacman / pkg / installer). Nothing is compiled, and no
#     binary is fetched from anywhere except repo.openscm.io.
#
#  3. Writes the agent config, backing up any existing one first
#       Linux          /etc/openscm/scmclient.config
#       FreeBSD/macOS  /usr/local/etc/openscm/scmclient.config
#
#  4. Enables and starts the scmclient service (systemd / rc.d / launchd).
#
#  It does NOT open ports, modify firewall rules, create users, or touch
#  anything else. Run with --dry-run to print every action without doing any
#  of it.
#
#  ---------------------------------------------------------------------------
#  WHAT HAPPENS AFTERWARDS
#  ---------------------------------------------------------------------------
#
#  The agent registers with your server and appears under Systems as PENDING.
#  It receives no compliance tests until an administrator approves it there.
#
#  Passing --token skips that: a system enrolling with a valid enrollment token
#  is approved automatically, which is how you roll out to many machines at
#  once. Mint tokens in the OpenSCM UI under Enrollment. The token is written
#  into the agent config, which this script creates mode 640.
#
#  ---------------------------------------------------------------------------
#  OPTIONS
#  ---------------------------------------------------------------------------
#
#    --server URL       Your OpenSCM server URL              (required)
#    --organization ID  Organization / tenant                (default: default)
#    --channel NAME     stable | testing                     (default: stable)
#    --version X.Y.Z    Pin a version                        (default: newest)
#    --heartbeat SECS   Check-in interval                    (default: 300)
#    --token TOKEN      Enrollment token; auto-approves this system
#                       (also read from $OPENSCM_ENROLLMENT_TOKEN, which
#                        keeps it out of the command line and shell history)
#    --uninstall        Remove the agent (keeps config and repository)
#    --purge            With --uninstall: also remove the agent config,
#                       its logs and the package repository
#    --dry-run          Print every action, change nothing
#    --help             Show this text
#
#  Requires root (or sudo), and curl or wget. Supports Debian, Ubuntu, RHEL,
#  Fedora, CentOS, Rocky, Alma, Arch, FreeBSD and macOS on x86_64, ARM, ARMv7,
#  i686, ppc64le, riscv64, s390x and loongarch64 as published.
#
#  ---------------------------------------------------------------------------
#  NOTES FOR MAINTAINERS
#  ---------------------------------------------------------------------------
#
#  Deliberately POSIX sh, not bash: the shell that runs this is whatever a
#  freshly-provisioned box happens to have. Alpine and minimal Debian images
#  ship dash or busybox ash with no bash at all, and this script exists to be
#  the FIRST thing run on such a machine.
#
#  On Debian and RHEL it configures the package REPOSITORY rather than
#  downloading a single .deb/.rpm, so `apt upgrade` keeps the agent current
#  afterwards.
#
# ==============================================================================

set -eu

REPO_HOST="https://repo.openscm.io"
SERVER_URL=""
ORGANIZATION="default"
CHANNEL="stable"
VERSION=""
HEARTBEAT="300"
DRY_RUN=0
# Also accepted from the environment, so the token need not appear in the
# command line (visible in `ps`) or in shell history.
TOKEN="${OPENSCM_ENROLLMENT_TOKEN:-}"
UNINSTALL=0
PURGE=0

RED=''; GREEN=''; YELLOW=''; BOLD=''; RESET=''
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    RED="$(printf '\033[0;31m')"; GREEN="$(printf '\033[0;32m')"
    YELLOW="$(printf '\033[1;33m')"; BOLD="$(printf '\033[1m')"
    RESET="$(printf '\033[0m')"
fi

info() { printf '%s==>%s %s%s%s\n' "$GREEN" "$RESET" "$BOLD" "$*" "$RESET"; }
# Five steps: check, repository, install, configure, start. Kept in a variable
# so adding a step cannot leave the counter reading [5/4].
STEP=0
STEPS_TOTAL=5
step() {
    STEP=$((STEP + 1))
    printf '\n%s[%s/%s]%s %s%s%s\n' "$GREEN" "$STEP" "$STEPS_TOTAL" "$RESET" "$BOLD" "$*" "$RESET"
}
warn() { printf '%s  ! %s%s\n' "$YELLOW" "$*" "$RESET" >&2; }
die()  { printf '%sError:%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }

# Every state-changing command goes through run(), which is what makes
# --dry-run trustworthy rather than approximate.
run() {
    if [ "$DRY_RUN" -eq 1 ]; then
        printf '    would run: %s\n' "$*"
    else
        "$@"
    fi
}

usage() {
    # Piped through `curl | sh`, $0 is "sh" or "-" and the banner above is not
    # on disk to read back, so fall back to an inline summary rather than
    # printing nothing.
    if [ -r "$0" ] && grep -q '^#  OpenSCM agent installer' "$0" 2>/dev/null; then
        sed -n '2,/NOTES FOR MAINTAINERS/p' "$0" | sed '$d;$d' | sed 's/^# \{0,2\}//'
    else
        cat <<'USAGE'
OpenSCM agent installer

  curl -fsSL https://repo.openscm.io/install.sh | sh -s -- --server URL

  --server URL       Your OpenSCM server URL              (required)
  --organization ID  Organization / tenant                (default: default)
  --channel NAME     stable | testing                     (default: stable)
  --version X.Y.Z    Pin a version                        (default: newest)
  --heartbeat SECS   Check-in interval                    (default: 300)
  --token TOKEN      Enrollment token, auto-approves this system
                     (or set $OPENSCM_ENROLLMENT_TOKEN)
  --uninstall        Remove the agent (keeps config and repository)
  --purge            With --uninstall: also remove config, logs, repo
  --dry-run          Print every action, change nothing
  --help             Show this text

Installs the scmclient package, writes the agent config, and starts the
service. The machine then appears in OpenSCM under Systems as pending.
USAGE
    fi
    exit 0
}

# ── Arguments ────────────────────────────────────────────────────────────────
while [ $# -gt 0 ]; do
    case "$1" in
        --server)       SERVER_URL="${2:-}";   shift 2 ;;
        --organization) ORGANIZATION="${2:-}"; shift 2 ;;
        --channel)      CHANNEL="${2:-}";      shift 2 ;;
        --version)      VERSION="${2:-}";      shift 2 ;;
        --heartbeat)    HEARTBEAT="${2:-}";    shift 2 ;;
        --token|--enrollment-token)
                        TOKEN="${2:-}";        shift 2 ;;
        --uninstall)    UNINSTALL=1;           shift ;;
        --purge)        PURGE=1;               shift ;;
        --dry-run)      DRY_RUN=1;             shift ;;
        --help|-h)      usage ;;
        *) die "Unknown argument: $1 (try --help)" ;;
    esac
done

if [ "$UNINSTALL" -eq 0 ]; then
    [ -n "$SERVER_URL" ] || die "--server is required, e.g. --server https://scm.example.com"
fi

case "$CHANNEL" in
    stable|testing) ;;
    *) die "--channel must be 'stable' or 'testing', got '$CHANNEL'" ;;
esac

# A URL without a scheme silently becomes a relative path in the agent's HTTP
# client and fails at the first heartbeat, long after this script has claimed
# success. Reject it here where the message can still be useful.
if [ "$UNINSTALL" -eq 0 ]; then
    case "$SERVER_URL" in
        http://*|https://*) ;;
        *) die "--server must start with http:// or https:// (got '$SERVER_URL')" ;;
    esac
fi
# Trailing slashes concatenate into '//api/...' paths.
SERVER_URL="${SERVER_URL%/}"

[ "$PURGE" -eq 1 ] && [ "$UNINSTALL" -eq 0 ] \
    && die "--purge only applies with --uninstall"

BASE="${REPO_HOST}/${CHANNEL}"

# ── Privilege ────────────────────────────────────────────────────────────────
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
    command -v sudo >/dev/null 2>&1 \
        || die "This installer needs root. Re-run as root or install sudo."
    SUDO="sudo"
fi

# ── Download helper ──────────────────────────────────────────────────────────
if command -v curl >/dev/null 2>&1; then
    fetch()   { curl -fsSL "$1"; }
    fetch_to(){ curl -fsSL "$1" -o "$2"; }
elif command -v wget >/dev/null 2>&1; then
    fetch()   { wget -qO- "$1"; }
    fetch_to(){ wget -qO "$2" "$1"; }
else
    die "Neither curl nor wget is available."
fi

# ── Detect OS ────────────────────────────────────────────────────────────────
OS="$(uname -s)"
MACHINE="$(uname -m)"
PLATFORM=""
case "$OS" in
    Linux)
        # /etc/os-release ID_LIKE is what actually distinguishes the families;
        # ID alone misses the many derivatives (Mint, Rocky, Alma, EndeavourOS).
        if [ -r /etc/os-release ]; then
            . /etc/os-release
            case "${ID:-}:${ID_LIKE:-}" in
                *debian*|*ubuntu*) PLATFORM="debian" ;;
                *rhel*|*fedora*|*centos*) PLATFORM="redhat" ;;
                *suse*) PLATFORM="suse" ;;
                *arch*) PLATFORM="arch" ;;
            esac
        fi
        # Fall back to which package manager exists, for distros with a thin
        # or missing os-release.
        if [ -z "$PLATFORM" ]; then
            if   command -v apt-get >/dev/null 2>&1; then PLATFORM="debian"
            elif command -v zypper  >/dev/null 2>&1; then PLATFORM="suse"
            elif command -v dnf     >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then PLATFORM="redhat"
            elif command -v pacman  >/dev/null 2>&1; then PLATFORM="arch"
            fi
        fi
        ;;
    FreeBSD) PLATFORM="freebsd" ;;
    Darwin)  PLATFORM="macos" ;;
    *) die "Unsupported operating system: $OS" ;;
esac

[ -n "$PLATFORM" ] || die "Could not identify this Linux distribution. Install manually: ${REPO_HOST}/docs"

case "$PLATFORM" in
    freebsd|macos) CONFIG_DIR="/usr/local/etc/openscm" ;;
    *)             CONFIG_DIR="/etc/openscm" ;;
esac
CONFIG="${CONFIG_DIR}/scmclient.config"
# ── Uninstall ────────────────────────────────────────────────────────────────
# Kept in this script rather than a separate uninstall.sh because everything it
# needs — platform detection, service names, config paths — is already here.
# A second script would duplicate all of it and drift.

# Is the SERVER installed from the same repository? If so the repository must
# survive --purge, or scmserver stops receiving updates as a side effect of
# removing the agent.
server_present() {
    command -v scmserver >/dev/null 2>&1
}

do_uninstall() {
    STEPS_TOTAL=3
    [ "$PURGE" -eq 1 ] && STEPS_TOTAL=4

    step "Checking this machine"
    echo "    OS / CPU      ${PLATFORM} on ${MACHINE}"
    if [ "$PURGE" -eq 1 ]; then
        echo "    Mode          purge — config, logs and repository will be removed too"
    else
        echo "    Mode          remove the package, keep config and repository"
    fi
    [ "$DRY_RUN" -eq 1 ] && echo "    Dry run       nothing will be changed"

    step "Stopping the agent"
    case "$PLATFORM" in
        freebsd)
            run $SUDO service scmclient stop
            run $SUDO sysrc -x scmclient_enable
            ;;
        macos)
            if [ "$DRY_RUN" -eq 1 ]; then
                printf '    would run: launchctl bootout system/io.openscm.scmclient\n'
            else
                $SUDO launchctl bootout system/io.openscm.scmclient 2>/dev/null || true
            fi
            ;;
        *)
            if command -v systemctl >/dev/null 2>&1; then
                run $SUDO systemctl stop scmclient
                run $SUDO systemctl disable scmclient
            fi
            ;;
    esac

    step "Removing scmclient"
    case "$PLATFORM" in
        debian)  run $SUDO apt-get remove -y scmclient ;;
        redhat)
            if command -v dnf >/dev/null 2>&1; then run $SUDO dnf remove -y scmclient
            else                                     run $SUDO yum remove -y scmclient; fi
            ;;
        suse)    run $SUDO zypper --non-interactive remove scmclient ;;
        arch)    run $SUDO pacman -R --noconfirm scmclient ;;
        freebsd) run $SUDO pkg delete -y scmclient ;;
        macos)
            # macOS .pkg installs have no uninstaller, so remove the payload
            # explicitly and drop the receipt, otherwise a later reinstall sees
            # the package as already present.
            run $SUDO rm -f /usr/local/bin/scmclient
            run $SUDO rm -f /Library/LaunchDaemons/io.openscm.scmclient.plist
            run $SUDO pkgutil --forget io.openscm.scmclient
            ;;
    esac

    if [ "$PURGE" -eq 1 ]; then
        step "Removing configuration and repository"
        # Only the agent's OWN files. /etc/openscm (and its macOS/FreeBSD
        # equivalent) is SHARED with scmserver: it holds scmserver.config and
        # keys/scmserver.key, the server's private signing key, which is
        # unrecoverable. `rm -rf` on the directory would destroy the server.
        run $SUDO rm -f "$CONFIG"
        if [ "$DRY_RUN" -eq 1 ]; then
            printf '    would run: rm -f %s.*.bak\n' "$CONFIG"
        else
            $SUDO sh -c "rm -f '${CONFIG}'.*.bak" 2>/dev/null || true
        fi
        # Removes the directory only when nothing else is left in it.
        run $SUDO rmdir "$CONFIG_DIR" 2>/dev/null || true

        if server_present; then
            warn "scmserver is installed — leaving /var/log/openscm in place."
        else
            run $SUDO rm -rf /var/log/openscm
        fi

        if server_present; then
            warn "scmserver is installed — leaving the OpenSCM repository configured."
        else
            case "$PLATFORM" in
                debian)
                    run $SUDO rm -f /etc/apt/sources.list.d/openscm.list
                    run $SUDO rm -f /usr/share/keyrings/openscm.gpg
                    ;;
                redhat)  run $SUDO rm -f /etc/yum.repos.d/openscm.repo ;;
                suse)    run $SUDO zypper --non-interactive removerepo openscm ;;
                *)       ;;   # Arch, FreeBSD and macOS never had one configured
            esac
        fi
    fi

    echo
    if [ "$DRY_RUN" -eq 1 ]; then
        info "Dry run complete — nothing was changed."
    else
        info "scmclient removed."
    fi
    echo
    if [ "$PURGE" -eq 0 ]; then
        echo "  Config left at ${BOLD}${CONFIG}${RESET}. Re-run with --purge to remove it,"
        echo "  along with logs and the package repository."
    fi
    echo "  The system stays in OpenSCM under ${BOLD}Systems${RESET} until you delete it there;"
    echo "  it will simply stop checking in."
    echo
    exit 0
}

[ "$UNINSTALL" -eq 1 ] && do_uninstall


# ── Detect CPU ───────────────────────────────────────────────────────────────
# Each packaging ecosystem spells the same CPU differently, so map once, here,
# rather than at every use site.
# apt, dnf and zypper resolve the architecture themselves from the repository
# metadata, so only Arch needs a name here — it installs a package URL
# directly. An empty ARCH_ARCH means this CPU has no Arch build; the Debian and
# RPM repositories cover more architectures than Arch does.
#
# These names are the ones the repository actually publishes, which are NOT
# always what the ecosystem's own convention would suggest.
case "$MACHINE" in
    x86_64|amd64)  ARCH_ARCH="x86_64" ;;
    aarch64|arm64) ARCH_ARCH="aarch64" ;;
    armv7l|armv7)  ARCH_ARCH="armhf" ;;
    ppc64le)       ARCH_ARCH="ppc64le" ;;
    riscv64)       ARCH_ARCH="riscv64" ;;
    i386|i686)     ARCH_ARCH="" ;;
    s390x)         ARCH_ARCH="" ;;
    loongarch64)   ARCH_ARCH="" ;;
    *) die "Unsupported CPU architecture: $MACHINE" ;;
esac

step "Checking this machine"
echo "    OS / CPU      ${PLATFORM} on ${MACHINE}"
echo "    Channel       ${CHANNEL}"
echo "    Server        ${SERVER_URL}"
echo "    Organization  ${ORGANIZATION}"
if [ -n "$TOKEN" ]; then
    # Deliberately not echoed. It is a credential, and installer output
    # routinely ends up in CI logs and terminal scrollback.
    echo "    Enrollment    token provided (auto-approves this system)"
fi
if [ "$DRY_RUN" -eq 1 ]; then
    echo "    Mode          dry run — nothing will be changed"
fi

# ── Resolve version (only needed where there is no package repository) ───────
# Debian and RHEL get the repository itself, so their package manager picks the
# newest build and keeps it current afterwards. Arch, FreeBSD and macOS install
# one versioned file, so the version has to be resolved up front.
resolve_version() {
    [ -n "$VERSION" ] && return 0
    VERSION="$(fetch "${BASE}/latest" 2>/dev/null | tr -d ' \t\r\n')" || VERSION=""
    [ -n "$VERSION" ] \
        || die "Could not determine the newest version from ${BASE}/latest — pass --version X.Y.Z"
}

# ── Install ──────────────────────────────────────────────────────────────────
install_debian() {
    step "Adding the OpenSCM package repository"
    run $SUDO mkdir -p /usr/share/keyrings
    if [ "$DRY_RUN" -eq 1 ]; then
        printf '    would run: fetch %s/openscm.gpg -> /usr/share/keyrings/openscm.gpg\n' "$REPO_HOST"
    else
        fetch "${REPO_HOST}/openscm.gpg" \
            | $SUDO gpg --batch --yes --dearmor -o /usr/share/keyrings/openscm.gpg \
            || die "Failed to install the repository signing key."
    fi

    line="deb [signed-by=/usr/share/keyrings/openscm.gpg] ${REPO_HOST}/${CHANNEL}/debian stable main"
    if [ "$DRY_RUN" -eq 1 ]; then
        printf '    would write /etc/apt/sources.list.d/openscm.list: %s\n' "$line"
    else
        echo "$line" | $SUDO tee /etc/apt/sources.list.d/openscm.list >/dev/null
    fi

    step "Installing scmclient"
    run $SUDO apt-get update
    run $SUDO apt-get install -y scmclient
}

install_redhat() {
    step "Adding the OpenSCM package repository"
    if [ "$DRY_RUN" -eq 1 ]; then
        printf '    would write /etc/yum.repos.d/openscm.repo\n'
    else
        $SUDO tee /etc/yum.repos.d/openscm.repo >/dev/null <<EOF
[openscm]
name=OpenSCM (${CHANNEL})
baseurl=${REPO_HOST}/${CHANNEL}/redhat
enabled=1
gpgcheck=1
gpgkey=${REPO_HOST}/openscm.gpg
EOF
    fi

    step "Installing scmclient"
    if command -v dnf >/dev/null 2>&1; then
        run $SUDO dnf install -y scmclient
    else
        run $SUDO yum install -y scmclient
    fi
}

install_suse() {
    step "Adding the OpenSCM package repository"
    # openSUSE consumes the same RPM repository as the RHEL family.
    run $SUDO zypper --non-interactive removerepo openscm
    run $SUDO zypper --non-interactive addrepo "${BASE}/redhat/" openscm
    run $SUDO zypper --non-interactive --gpg-auto-import-keys refresh

    step "Installing scmclient"
    run $SUDO zypper --non-interactive install scmclient
}

install_arch() {
    [ -n "$ARCH_ARCH" ] || die "No Arch package is published for ${MACHINE}."
    step "No package repository on this platform — using a signed package"
    resolve_version
    # Hyphen-separated, verified against the live repository listing.
    # docs/start/downloads.md and installation.md show underscores here;
    # those URLs 404 and are wrong.
    url="${BASE}/arch/scmclient-${VERSION}-1-${ARCH_ARCH}.pkg.tar.zst"
    step "Installing scmclient ${VERSION}"
    run $SUDO pacman -U --noconfirm "$url"
}

install_freebsd() {
    case "$MACHINE" in
        x86_64|amd64) ;;
        *) die "FreeBSD packages are published for amd64 only." ;;
    esac
    step "No package repository on this platform — using a signed package"
    resolve_version
    url="${BASE}/freebsd/scmclient-${VERSION}-1-freebsd-amd64.pkg"
    step "Installing scmclient ${VERSION}"
    run $SUDO pkg add "$url"
}

install_macos() {
    step "No package repository on this platform — using a signed package"
    resolve_version
    url="${BASE}/macos/scmclient_${VERSION}-1_macos.pkg"
    tmp="$(mktemp -t scmclient).pkg"
    step "Installing scmclient ${VERSION}"
    if [ "$DRY_RUN" -eq 1 ]; then
        printf '    would run: fetch %s && installer -pkg <tmp> -target /\n' "$url"
    else
        fetch_to "$url" "$tmp" || die "Download failed: $url"
        $SUDO installer -pkg "$tmp" -target / || { rm -f "$tmp"; die "installer failed."; }
        rm -f "$tmp"
    fi
}

case "$PLATFORM" in
    debian)  install_debian  ;;
    redhat)  install_redhat  ;;
    suse)    install_suse    ;;
    arch)    install_arch    ;;
    freebsd) install_freebsd ;;
    macos)   install_macos   ;;
esac

# ── Configure ────────────────────────────────────────────────────────────────
step "Writing the agent config"
echo "    ${CONFIG}"
run $SUDO mkdir -p "$CONFIG_DIR"

# The package ships a default config. Keep a copy of anything that was already
# there: an operator re-running this to repoint an agent should not silently
# lose hand-edited settings.
if [ -f "$CONFIG" ] && [ "$DRY_RUN" -eq 0 ]; then
    backup="${CONFIG}.$(date +%Y%m%d%H%M%S).bak"
    $SUDO cp "$CONFIG" "$backup"
    warn "Existing config saved as ${backup}"
fi

if [ "$DRY_RUN" -eq 1 ]; then
    printf '    would write:\n'
    printf '      [server]\n      url = "%s"\n      organization = "%s"\n' "$SERVER_URL" "$ORGANIZATION"
    [ -n "$TOKEN" ] && printf '      enrollment_token = "***"\n'
    printf '      [client]\n      heartbeat = "%s"\n      loglevel = "info"\n' "$HEARTBEAT"
else
    {
        printf '[server]\n'
        printf 'url = "%s"\n' "$SERVER_URL"
        printf 'organization = "%s"\n' "$ORGANIZATION"
        # Present only when supplied: the agent treats the key as optional, and
        # an empty string is not the same as absent.
        [ -n "$TOKEN" ] && printf 'enrollment_token = "%s"\n' "$TOKEN"
        printf '\n[client]\n'
        printf 'heartbeat = "%s"\n' "$HEARTBEAT"
        printf 'loglevel = "info"\n'
    } | $SUDO tee "$CONFIG" >/dev/null
    # The config is not secret today, but it identifies the server and will
    # carry an enrollment token whenever one is added, so do not leave it
    # world-readable.
    $SUDO chmod 640 "$CONFIG" 2>/dev/null || true
fi

# ── Start ────────────────────────────────────────────────────────────────────
step "Starting the agent"
case "$PLATFORM" in
    freebsd)
        # sysrc is not in the installation guide, but without it the service
        # does not come back after a reboot. Harmless if the package set it.
        if [ "$DRY_RUN" -eq 1 ]; then
            printf '    would run: sysrc scmclient_enable=YES && service scmclient restart\n'
        else
            $SUDO sysrc scmclient_enable=YES >/dev/null 2>&1 || true
            $SUDO service scmclient restart || warn "Could not restart scmclient."
        fi
        ;;
    macos)
        # bootout then bootstrap, per the installation guide. NOT
        # `launchctl kickstart`, which requires the service to be bootstrapped
        # already — on a fresh install it is not, so kickstart fails and the
        # agent never starts. bootout is allowed to fail for the same reason.
        if [ "$DRY_RUN" -eq 1 ]; then
            printf '    would run: launchctl bootout/bootstrap system/io.openscm.scmclient\n'
        else
            $SUDO launchctl bootout system/io.openscm.scmclient 2>/dev/null || true
            $SUDO launchctl bootstrap system /Library/LaunchDaemons/io.openscm.scmclient.plist \
                || warn "launchctl bootstrap failed — start the agent manually."
        fi
        ;;
    *)
        if command -v systemctl >/dev/null 2>&1; then
            run $SUDO systemctl enable scmclient
            run $SUDO systemctl restart scmclient
        else
            warn "systemd not found — start scmclient with your init system."
        fi
        ;;
esac

# ── Done ─────────────────────────────────────────────────────────────────────
echo
if [ "$DRY_RUN" -eq 1 ]; then
    info "Dry run complete — nothing was changed."
else
    info "scmclient installed and pointed at ${SERVER_URL}"
fi
echo
if [ -n "$TOKEN" ]; then
    echo "  This machine was enrolled with a token, so it appears in OpenSCM under"
    echo "  ${BOLD}Systems${RESET} already ${BOLD}approved${RESET} and will start receiving compliance tests."
else
    echo "  This machine now appears in OpenSCM under ${BOLD}Systems${RESET} as ${BOLD}pending${RESET}."
    echo "  Approve it there and it will start receiving compliance tests."
fi
echo
