Files
peardata/scripts/install.sh
T
Raven Scott 584e1c9c42
CI / test (push) Successful in 1m1s
Release rolling / release (push) Successful in 7m11s
Update For Docker Socket Support
2026-07-19 13:26:52 -04:00

750 lines
25 KiB
Bash
Executable File

#!/usr/bin/env bash
# peardata interactive installer
#
# One-liner (from this repo on Gitea):
# curl -fsSL https://git.ssh.surf/snxraven/peardata/raw/branch/main/scripts/install.sh | bash
#
# Optional install host (if you mirror the script):
# curl -fsSL https://install.peardata.boats | bash
#
# Non-interactive:
# curl -fsSL …/install.sh | bash -s -- --server --yes
# curl -fsSL …/install.sh | bash -s -- --client --yes
# PEARDATA_ROLE=server bash scripts/install.sh --yes
#
# Env overrides:
# PEARDATA_ROLE=server|client|both
# PEARDATA_VERSION / PEARDATA_TAG=rolling
# PEARDATA_GITEA_URL=https://git.ssh.surf
# PEARDATA_OWNER=snxraven PEARDATA_REPO=peardata
# PEARDATA_SERVER_DIR=/opt/peardata
# PEARDATA_CLIENT_DIR=... (default: ~/.local/share/peardata or ~/Applications)
# PEARDATA_YES=1 — assume defaults / non-interactive where possible
#
set -euo pipefail
# ─── defaults ───────────────────────────────────────────────────────────────
GITEA_URL="${PEARDATA_GITEA_URL:-https://git.ssh.surf}"
OWNER="${PEARDATA_OWNER:-snxraven}"
REPO="${PEARDATA_REPO:-peardata}"
TAG="${PEARDATA_TAG:-rolling}"
VERSION="${PEARDATA_VERSION:-}" # empty → resolve from release assets
ROLE="${PEARDATA_ROLE:-}"
ASSUME_YES="${PEARDATA_YES:-0}"
SERVER_DIR="${PEARDATA_SERVER_DIR:-/opt/peardata}"
TMPDIR_ROOT="${TMPDIR:-/tmp}"
INSTALL_TMP=""
# ─── colors / UI ────────────────────────────────────────────────────────────
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
C_TEAL="$(tput setaf 6)"
C_BOLD="$(tput bold)"
C_DIM="$(tput dim)"
C_RED="$(tput setaf 1)"
C_GREEN="$(tput setaf 2)"
C_YELLOW="$(tput setaf 3)"
C_RESET="$(tput sgr0)"
else
C_TEAL="" C_BOLD="" C_DIM="" C_RED="" C_GREEN="" C_YELLOW="" C_RESET=""
fi
log() { printf '%s[peardata]%s %s\n' "$C_TEAL" "$C_RESET" "$*"; }
ok() { printf '%s[peardata]%s %s✓%s %s\n' "$C_TEAL" "$C_RESET" "$C_GREEN" "$C_RESET" "$*"; }
warn() { printf '%s[peardata]%s %s!%s %s\n' "$C_TEAL" "$C_RESET" "$C_YELLOW" "$C_RESET" "$*" >&2; }
err() { printf '%s[peardata]%s %s✗%s %s\n' "$C_TEAL" "$C_RESET" "$C_RED" "$C_RESET" "$*" >&2; }
die() { err "$*"; exit 1; }
banner() {
cat <<EOF
${C_TEAL}${C_BOLD} ╔═══════════════════════════════════════╗
║ p e a r d a t a ║
║ P2P real-time host monitoring ║
╚═══════════════════════════════════════╝${C_RESET}
EOF
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
# ─── detect host ────────────────────────────────────────────────────────────
detect_os() {
local u
u="$(uname -s 2>/dev/null || echo unknown)"
case "$u" in
Linux*) echo linux ;;
Darwin*) echo darwin ;;
MINGW*|MSYS*|CYGWIN*) echo win32 ;;
*) echo "unknown" ;;
esac
}
detect_arch() {
local m
m="$(uname -m 2>/dev/null || echo unknown)"
case "$m" in
x86_64|amd64) echo x64 ;;
aarch64|arm64) echo arm64 ;;
armv7*|armhf) die "32-bit / armv7 is not supported (64-bit only)" ;;
i386|i686) die "32-bit x86 is not supported (64-bit only)" ;;
*) die "Unsupported architecture: $m" ;;
esac
}
host_triple() {
local os arch
os="$(detect_os)"
arch="$(detect_arch)"
[[ "$os" == "unknown" ]] && die "Unsupported OS: $(uname -s)"
[[ "$os" == "win32" ]] && die "Windows install via bash is limited — download a .tar.gz from the rolling release instead."
echo "${os}-${arch}"
}
# First existing Docker Engine unix socket (empty if none).
detect_docker_socket() {
local p
for p in \
"${PEARDATA_DOCKER_SOCKET:-}" \
/var/run/docker.sock \
/run/docker.sock \
/var/run/podman/podman.sock \
/run/podman/podman.sock
do
[[ -n "$p" && -S "$p" ]] || continue
printf '%s\n' "$p"
return 0
done
return 1
}
# ─── args ───────────────────────────────────────────────────────────────────
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--server|-s) ROLE=server; shift ;;
--client|-c) ROLE=client; shift ;;
--both|-b) ROLE=both; shift ;;
--yes|-y) ASSUME_YES=1; shift ;;
--tag) TAG="${2:-}"; shift 2 ;;
--version) VERSION="${2:-}"; shift 2 ;;
--server-dir) SERVER_DIR="${2:-}"; shift 2 ;;
--help|-h)
cat <<EOF
Usage: install.sh [options]
--server, -s Install peardata-server (Linux Bare binary + systemd)
--client, -c Install peardata desktop client
--both, -b Install both
--yes, -y Non-interactive defaults
--tag TAG Release tag (default: rolling)
--version VER Pin asset version (default: auto from release)
--server-dir DIR Server install path (default: /opt/peardata)
--help This help
One-liner:
curl -fsSL ${GITEA_URL}/${OWNER}/${REPO}/raw/branch/main/scripts/install.sh | bash
Non-interactive:
curl -fsSL …/install.sh | bash -s -- --server --yes
curl -fsSL …/install.sh | bash -s -- --client --yes
EOF
exit 0
;;
*) die "Unknown option: $1 (try --help)" ;;
esac
done
}
# ─── prompts ────────────────────────────────────────────────────────────────
ask() {
local prompt="$1" default="${2:-}" reply
if [[ "$ASSUME_YES" == "1" ]]; then
echo "${default}"
return
fi
if [[ ! -t 0 ]]; then
if [[ -r /dev/tty ]]; then
if [[ -n "$default" ]]; then
printf '%s [%s]: ' "$prompt" "$default" >/dev/tty
else
printf '%s: ' "$prompt" >/dev/tty
fi
IFS= read -r reply </dev/tty || true
else
echo "${default}"
return
fi
else
if [[ -n "$default" ]]; then
read -r -p "${prompt} [${default}]: " reply || true
else
read -r -p "${prompt}: " reply || true
fi
fi
if [[ -z "${reply// }" ]]; then
echo "$default"
else
echo "$reply"
fi
}
ask_choice() {
local prompt="$1" default="$2"
local ans
ans="$(ask "$prompt" "$default")"
echo "$ans" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]'
}
confirm() {
local prompt="$1" default="${2:-y}"
local ans
ans="$(ask "$prompt (y/n)" "$default")"
case "$(echo "$ans" | tr '[:upper:]' '[:lower:]')" in
y|yes) return 0 ;;
*) return 1 ;;
esac
}
# ─── privileges / download ──────────────────────────────────────────────────
have_sudo() {
[[ "$(id -u)" -eq 0 ]] && return 0
command -v sudo >/dev/null 2>&1
}
run_root() {
if [[ "$(id -u)" -eq 0 ]]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
die "Need root privileges for: $*"
fi
}
download() {
local url="$1" out="$2"
log "Downloading $(basename "$url")…"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 -o "$out" "$url" \
|| die "Download failed: $url"
elif command -v wget >/dev/null 2>&1; then
wget -q -O "$out" "$url" || die "Download failed: $url"
else
die "Need curl or wget"
fi
}
json_get_assets() {
local api="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}/releases/tags/${TAG}"
local json
json="$(curl -fsSL --connect-timeout 15 "$api" 2>/dev/null || true)"
if [[ -z "$json" ]]; then
return 1
fi
printf '%s' "$json" | tr ',' '\n' | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | grep -E '^peardata-(server|client)-' || true
}
resolve_version() {
if [[ -n "$VERSION" ]]; then
echo "$VERSION"
return
fi
local assets name
assets="$(json_get_assets || true)"
if [[ -n "$assets" ]]; then
name="$(printf '%s\n' "$assets" | grep -E "^peardata-server-[0-9]" | head -1 || true)"
if [[ -z "$name" ]]; then
name="$(printf '%s\n' "$assets" | grep -E "^peardata-client-[0-9]" | head -1 || true)"
fi
if [[ -n "$name" ]]; then
# peardata-server-0.1.0-linux-x64.tar.gz
echo "$name" | sed -E 's/^peardata-(server|client)-([0-9][^/]*)-(linux|darwin|win32)-.*/\2/'
return
fi
fi
local ver
ver="$(curl -fsSL "${GITEA_URL}/${OWNER}/${REPO}/raw/branch/main/package.json" 2>/dev/null \
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 || true)"
if [[ -n "$ver" ]]; then
echo "$ver"
return
fi
die "Could not resolve release version — set PEARDATA_VERSION=x.y.z"
}
asset_url() {
local name="$1"
echo "${GITEA_URL}/${OWNER}/${REPO}/releases/download/${TAG}/${name}"
}
# ─── server install (Linux Bare binary + systemd) ───────────────────────────
install_server() {
local host="$1" version="$2"
local os arch archive url tarball work bin_src unit_path
os="${host%%-*}"
arch="${host#*-}"
[[ "$os" == "linux" ]] || die "Server install is supported on Linux only (detected: $os). Download a client for $host, or run the agent on a Linux host."
need_cmd tar
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
die "Need curl or wget"
fi
archive="peardata-server-${version}-${host}.tar.gz"
url="$(asset_url "$archive")"
work="${INSTALL_TMP}/server"
mkdir -p "$work"
tarball="${work}/${archive}"
download "$url" "$tarball"
if curl -fsSL --connect-timeout 10 -o "${tarball}.sha256" "${url}.sha256" 2>/dev/null; then
if command -v sha256sum >/dev/null 2>&1; then
(cd "$work" && sha256sum -c "$(basename "$tarball").sha256") && ok "Checksum OK" || warn "Checksum verify failed (continuing)"
elif command -v shasum >/dev/null 2>&1; then
(cd "$work" && shasum -a 256 -c "$(basename "$tarball").sha256") && ok "Checksum OK" || warn "Checksum verify failed (continuing)"
fi
fi
mkdir -p "${work}/extract"
tar -xzf "$tarball" -C "${work}/extract"
bin_src="$(find "${work}/extract" -type f -name 'peardata-server' | head -1 || true)"
[[ -n "$bin_src" ]] || die "peardata-server binary not found in archive"
chmod +x "$bin_src"
log "Installing server to ${SERVER_DIR} (requires root)…"
have_sudo || die "Server install needs root/sudo"
run_root mkdir -p "${SERVER_DIR}/data"
run_root install -m 0755 "$bin_src" "${SERVER_DIR}/peardata-server"
if [[ ! -f "${SERVER_DIR}/.env" ]]; then
run_root touch "${SERVER_DIR}/.env"
run_root chmod 600 "${SERVER_DIR}/.env"
fi
if ! id peardata >/dev/null 2>&1; then
log "Creating system user peardata…"
if command -v useradd >/dev/null 2>&1; then
run_root useradd --system --home "$SERVER_DIR" --shell /usr/sbin/nologin peardata 2>/dev/null \
|| run_root useradd --system --home "$SERVER_DIR" --shell /bin/false peardata
elif command -v adduser >/dev/null 2>&1; then
run_root adduser --system --home "$SERVER_DIR" --shell /usr/sbin/nologin --no-create-home peardata || true
else
die "Cannot create peardata user (need useradd or adduser)"
fi
fi
id peardata >/dev/null 2>&1 || die "peardata user was not created"
# Host journal access for Logs tab (journalctl as peardata)
if getent group systemd-journal >/dev/null 2>&1; then
log "Adding peardata to systemd-journal for host log access…"
if command -v usermod >/dev/null 2>&1; then
run_root usermod -aG systemd-journal peardata || warn "usermod -aG systemd-journal peardata failed"
elif command -v gpasswd >/dev/null 2>&1; then
run_root gpasswd -a peardata systemd-journal || warn "gpasswd -a peardata systemd-journal failed"
else
warn "Could not add peardata to systemd-journal (no usermod/gpasswd)"
fi
else
warn "Group systemd-journal not found — host Journal in Logs may be unavailable"
fi
# Ensure journal is enabled in .env (default on; operators may set PEARDATA_JOURNAL=0)
if ! run_root grep -qE '^PEARDATA_JOURNAL=' "${SERVER_DIR}/.env" 2>/dev/null; then
log "Enabling PEARDATA_JOURNAL=1 in ${SERVER_DIR}/.env"
run_root sh -c "printf '\\n# Host journal for Logs tab (set to 0 to disable)\\nPEARDATA_JOURNAL=1\\n' >> '${SERVER_DIR}/.env'"
fi
# Docker socket access — container names (e.g. dozzle) + per-container metrics
local docker_detected=0
local docker_sock=""
local docker_group_name=""
docker_sock="$(detect_docker_socket || true)"
if [[ -n "$docker_sock" ]]; then
docker_detected=1
elif getent group docker >/dev/null 2>&1; then
docker_detected=1
docker_sock="/var/run/docker.sock"
elif command -v docker >/dev/null 2>&1; then
docker_detected=1
docker_sock="/var/run/docker.sock"
fi
if getent group docker >/dev/null 2>&1; then
docker_group_name="docker"
fi
if [[ "$docker_detected" -eq 1 ]]; then
ok "Docker detected${docker_sock:+ (socket ${docker_sock})}"
if [[ -n "$docker_group_name" ]]; then
log "Adding peardata to ${docker_group_name} for Docker socket / container names…"
if command -v usermod >/dev/null 2>&1; then
run_root usermod -aG "$docker_group_name" peardata || warn "usermod -aG ${docker_group_name} peardata failed"
elif command -v gpasswd >/dev/null 2>&1; then
run_root gpasswd -a peardata "$docker_group_name" || warn "gpasswd -a peardata ${docker_group_name} failed"
else
warn "Could not add peardata to ${docker_group_name} (no usermod/gpasswd)"
fi
else
warn "Docker socket present but no 'docker' group — grant peardata read access to ${docker_sock} manually"
fi
if ! run_root grep -qE '^PEARDATA_DOCKER=' "${SERVER_DIR}/.env" 2>/dev/null; then
log "Enabling PEARDATA_DOCKER=1 in ${SERVER_DIR}/.env (container metrics + names)"
run_root sh -c "printf '\\n# Docker collector (names via socket; set to 0 to disable)\\nPEARDATA_DOCKER=1\\nPEARDATA_DOCKER_SOCKET=%s\\n' '${docker_sock}' >> '${SERVER_DIR}/.env'"
else
# Keep collector flag; refresh socket path if unset
if ! run_root grep -qE '^PEARDATA_DOCKER_SOCKET=' "${SERVER_DIR}/.env" 2>/dev/null; then
run_root sh -c "printf 'PEARDATA_DOCKER_SOCKET=%s\\n' '${docker_sock}' >> '${SERVER_DIR}/.env'"
fi
fi
else
log "Docker not detected — skipping socket access (install Docker later, then re-run installer or: usermod -aG docker peardata && PEARDATA_DOCKER=1)"
fi
run_root chown -R peardata:peardata "$SERVER_DIR" 2>/dev/null || run_root chown -R peardata "$SERVER_DIR"
unit_path="/etc/systemd/system/peardata.service"
# systemd SupplementaryGroups replaces the user's supplementary set — list every group we need
local supp_groups=()
if getent group systemd-journal >/dev/null 2>&1; then
supp_groups+=("systemd-journal")
fi
if [[ "$docker_detected" -eq 1 && -n "$docker_group_name" ]]; then
supp_groups+=("$docker_group_name")
fi
local supp_groups_line=""
if [[ ${#supp_groups[@]} -gt 0 ]]; then
supp_groups_line="SupplementaryGroups=${supp_groups[*]}"
fi
local docker_after_line=""
local docker_wants_line=""
if [[ "$docker_detected" -eq 1 ]]; then
docker_after_line="After=docker.service"
docker_wants_line="Wants=docker.service"
fi
log "Writing ${unit_path}…"
run_root tee "$unit_path" >/dev/null <<EOF
[Unit]
Description=PearData PearMonitor agent (HyperDHT + REST metrics)
Documentation=${GITEA_URL}/${OWNER}/${REPO}
After=network-online.target
Wants=network-online.target
${docker_after_line}
${docker_wants_line}
[Service]
Type=simple
WorkingDirectory=${SERVER_DIR}
ExecStart=${SERVER_DIR}/peardata-server
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
User=peardata
Group=peardata
${supp_groups_line}
NoNewPrivileges=true
PrivateTmp=true
Environment=NODE_ENV=production
Environment=PEARDATA_JOURNAL=1
EnvironmentFile=-${SERVER_DIR}/.env
# Identity + HyperDB / ring buffers
ReadWritePaths=${SERVER_DIR}
[Install]
WantedBy=multi-user.target
EOF
if command -v systemctl >/dev/null 2>&1; then
run_root systemctl daemon-reload
if confirm "Enable and start peardata.service now?" "y"; then
run_root systemctl enable --now peardata.service
ok "Service peardata.service is enabled and started"
sleep 2
run_root systemctl --no-pager --full status peardata.service || true
log "Logs: journalctl -u peardata -f"
else
ok "Unit installed. Start later with: sudo systemctl enable --now peardata"
fi
else
warn "systemctl not found — binary installed at ${SERVER_DIR}/peardata-server (start manually)"
fi
local env_pk=""
if [[ -f "${SERVER_DIR}/.env" ]]; then
env_pk="$(run_root grep -E '^SERVER_PUBLIC_KEY=' "${SERVER_DIR}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]' || true)"
fi
local docker_summary="not detected (optional)"
if [[ "$docker_detected" -eq 1 ]]; then
docker_summary="enabled · peardata ∈ ${docker_group_name:-docker} · ${docker_sock} (human container names)"
fi
cat <<EOF
${C_BOLD}Server installed${C_RESET}
Binary: ${SERVER_DIR}/peardata-server
Config: ${SERVER_DIR}/.env ${C_BOLD}← check this file for your keys${C_RESET}
Data: ${SERVER_DIR}/data
Service: peardata.service
Journal: peardata ∈ systemd-journal · PEARDATA_JOURNAL=1 (Logs tab)
Docker: ${docker_summary}
REST: http://127.0.0.1:18888/api/v3/info (localhost by default)
${C_BOLD}Keys (written on first successful start)${C_RESET}
After peardata has started once:
sudo grep -E '^(SERVER_PUBLIC_KEY|SERVER_SEED)=' ${SERVER_DIR}/.env
${C_BOLD}SERVER_PUBLIC_KEY${C_RESET} 64 hex — share with clients (viewer if used alone)
${C_BOLD}SERVER_SEED${C_RESET} 64 hex — ${C_BOLD}admin only${C_RESET}; never share with operators
EOF
if [[ -n "$env_pk" && ${#env_pk} -eq 64 ]]; then
cat <<EOF
${C_GREEN}SERVER_PUBLIC_KEY (from .env):${C_RESET}
${env_pk}
EOF
else
cat <<EOF
If keys are missing, start/restart once then re-check .env:
sudo systemctl enable --now peardata
sudo systemctl restart peardata
sudo grep -E '^(SERVER_PUBLIC_KEY|SERVER_SEED)=' ${SERVER_DIR}/.env
Journal (also prints the public key on boot):
sudo journalctl -u peardata -n 80 --no-pager | grep -i publicKey
EOF
fi
cat <<EOF
${C_BOLD}Connect from the desktop client${C_RESET}
· Public key only → viewer (read + subscribe)
· Public key + SERVER_SEED → admin
· Full pd1.… invite → role from invite (operators; no seed)
Docs: ${GITEA_URL}/${OWNER}/${REPO}
EOF
}
# ─── client install ─────────────────────────────────────────────────────────
default_client_dir() {
local os="$1"
case "$os" in
darwin)
echo "${PEARDATA_CLIENT_DIR:-$HOME/Applications}"
;;
*)
echo "${PEARDATA_CLIENT_DIR:-$HOME/.local/share/peardata}"
;;
esac
}
install_desktop_entry_linux() {
local bin="$1" icon_src="${2:-}"
local apps="$HOME/.local/share/applications"
local icons="$HOME/.local/share/icons/hicolor/256x256/apps"
mkdir -p "$apps"
if [[ -n "$icon_src" && -f "$icon_src" ]]; then
mkdir -p "$icons"
cp -f "$icon_src" "$icons/peardata.png" 2>/dev/null || true
fi
cat >"${apps}/peardata.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=PearData
Comment=P2P real-time host monitoring
Exec=${bin}
Icon=peardata
Terminal=false
Categories=System;Monitor;Network;
StartupWMClass=peardata
EOF
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$apps" 2>/dev/null || true
fi
ok "Desktop entry: ${apps}/peardata.desktop"
}
install_client() {
local host="$1" version="$2"
local os arch archive url tarball work extract client_dir bin app
os="${host%%-*}"
arch="${host#*-}"
client_dir="$(default_client_dir "$os")"
need_cmd tar
archive="peardata-client-${version}-${host}.tar.gz"
url="$(asset_url "$archive")"
work="${INSTALL_TMP}/client"
mkdir -p "$work"
tarball="${work}/${archive}"
download "$url" "$tarball"
if curl -fsSL --connect-timeout 10 -o "${tarball}.sha256" "${url}.sha256" 2>/dev/null; then
if command -v sha256sum >/dev/null 2>&1; then
(cd "$work" && sha256sum -c "$(basename "$tarball").sha256") && ok "Checksum OK" || warn "Checksum verify failed (continuing)"
elif command -v shasum >/dev/null 2>&1; then
(cd "$work" && shasum -a 256 -c "$(basename "$tarball").sha256") && ok "Checksum OK" || warn "Checksum verify failed (continuing)"
fi
fi
extract="${work}/extract"
rm -rf "$extract"
mkdir -p "$extract"
tar -xzf "$tarball" -C "$extract"
# Archive contains peardata-<host>/... (or legacy PearData-<host>)
local payload
payload="$(find "$extract" -maxdepth 1 -type d \( -name 'peardata-*' -o -name 'PearData-*' \) | head -1 || true)"
[[ -n "$payload" ]] || payload="$extract"
[[ -d "$payload" ]] || die "Unexpected client archive layout"
case "$os" in
darwin)
app="$(find "$payload" -maxdepth 3 \( -name 'peardata.app' -o -name 'PearData.app' \) -type d | head -1 || true)"
[[ -n "$app" ]] || die "peardata.app not found in client archive"
mkdir -p "$client_dir"
local dest_app="${client_dir}/peardata.app"
rm -rf "$dest_app"
cp -a "$app" "$dest_app"
app="$dest_app"
if [[ -d "${client_dir}/peardata" && ! -d "${client_dir}/peardata/Contents" ]]; then
rm -rf "${client_dir}/peardata"
fi
xattr -cr "$app" 2>/dev/null || true
ok "Client app: $app"
if confirm "Open PearData now?" "y"; then
open "$app" || true
fi
cat <<EOF
${C_BOLD}Client installed (macOS)${C_RESET}
App: $app
If Gatekeeper blocks it: right-click → Open, or:
xattr -cr "$app"
Connect:
· Agent public key (SERVER_PUBLIC_KEY) → viewer
· Public key + SERVER_SEED → admin (never share the seed)
· Full pd1. invite from an admin → operator without the seed
On the agent host:
sudo grep -E '^(SERVER_PUBLIC_KEY|SERVER_SEED)=' /opt/peardata/.env
EOF
;;
linux)
mkdir -p "$client_dir"
rm -rf "${client_dir:?}/"* 2>/dev/null || true
cp -a "$payload"/. "$client_dir"/
bin="$(find "$client_dir" -type f -name 'peardata-client' | head -1 || true)"
[[ -n "$bin" ]] || die "peardata-client binary not found in archive"
chmod +x "$bin"
local bindir="$HOME/.local/bin"
mkdir -p "$bindir"
ln -sfn "$bin" "${bindir}/peardata-client"
ln -sfn "$bin" "${bindir}/peardata"
local icon
icon="$(find "$client_dir" -type f \( -name 'icon.png' -o -name '*256*.png' \) 2>/dev/null | head -1 || true)"
install_desktop_entry_linux "$bin" "$icon"
ok "Client binary: $bin"
ok "Launcher: ${bindir}/peardata-client (ensure ~/.local/bin is on PATH)"
cat <<EOF
${C_BOLD}Client installed (Linux)${C_RESET}
Run: peardata-client
or: ${bin}
Connect:
· Agent public key (from /opt/peardata/.env → SERVER_PUBLIC_KEY) → viewer
· Public key + SERVER_SEED → admin
· Full pd1. invite → operator/admin without the seed
On the agent host: sudo grep -E '^(SERVER_PUBLIC_KEY|SERVER_SEED)=' /opt/peardata/.env
EOF
if confirm "Launch peardata-client now?" "n"; then
nohup "$bin" >/dev/null 2>&1 &
fi
;;
*)
die "Client install not automated for OS=$os"
;;
esac
}
# ─── main ───────────────────────────────────────────────────────────────────
cleanup() {
if [[ -n "${INSTALL_TMP}" && -d "${INSTALL_TMP}" ]]; then
rm -rf "${INSTALL_TMP}"
fi
}
trap cleanup EXIT
main() {
parse_args "$@"
banner
need_cmd uname
local host os arch version
host="$(host_triple)"
os="${host%%-*}"
arch="${host#*-}"
log "Detected host: ${C_BOLD}${host}${C_RESET} ($(uname -s) $(uname -m))"
log "Release: ${GITEA_URL}/${OWNER}/${REPO} @ ${TAG}"
version="$(resolve_version)"
log "Version: ${version}"
if [[ -z "$ROLE" ]]; then
echo "What do you want to install?"
echo " ${C_BOLD}1${C_RESET}) Server — PearMonitor agent (Linux Bare binary + systemd)"
echo " ${C_BOLD}2${C_RESET}) Client — Desktop GUI (connect with a public key)"
echo " ${C_BOLD}3${C_RESET}) Both"
echo
local choice
# Default client on macOS (no Linux server binary); server on Linux
local def="2"
[[ "$os" == "linux" ]] && def="1"
choice="$(ask_choice "Choose 1/2/3" "$def")"
case "$choice" in
1|s|server) ROLE=server ;;
2|c|client) ROLE=client ;;
3|b|both) ROLE=both ;;
*) die "Invalid choice: $choice" ;;
esac
fi
INSTALL_TMP="$(mktemp -d "${TMPDIR_ROOT}/peardata-install.XXXXXX")"
case "$ROLE" in
server)
install_server "$host" "$version"
;;
client)
install_client "$host" "$version"
;;
both)
install_server "$host" "$version"
install_client "$host" "$version"
;;
*)
die "Unknown role: $ROLE (use server|client|both)"
;;
esac
ok "All done. Docs: ${GITEA_URL}/${OWNER}/${REPO}"
}
main "$@"