installer
CI / test (push) Successful in 58s
Release rolling / release (push) Successful in 7m9s

This commit is contained in:
Raven Scott
2026-07-18 18:55:46 -04:00
parent 5f82722df8
commit b56d6b4c0e
8 changed files with 730 additions and 26 deletions
+18 -2
View File
@@ -19,6 +19,22 @@ Built from the [pear-app-template](https://github.com/snxraven) HyperDHT + proto
## Quick start
### One-line installer (recommended)
```bash
curl -fsSL https://git.ssh.surf/snxraven/peardata/raw/branch/main/scripts/install.sh | bash
```
```bash
# Server only (Linux Bare + systemd → /opt/peardata)
curl -fsSL https://git.ssh.surf/snxraven/peardata/raw/branch/main/scripts/install.sh | bash -s -- --server --yes
# Desktop client only
curl -fsSL https://git.ssh.surf/snxraven/peardata/raw/branch/main/scripts/install.sh | bash -s -- --client --yes
```
### From source
```bash
cd peardata
npm install
@@ -33,9 +49,9 @@ curl -s 'http://127.0.0.1:19999/api/v3/data?chart=system.cpu&after=-30&points=30
# Terminal C — mint an operator invite (optional)
npm run mint-invite -- operator
# Terminal D — Pear desktop
# Terminal D — Pear / Electron desktop
npm start
# or: pear run -d .
# or: npm run start:client
```
In the UI, paste the **agent public key** (viewer) or a **`pd1.` invite**.
+11 -8
View File
@@ -1,22 +1,25 @@
[Unit]
Description=PearData PearMonitor agent (HyperDHT + REST metrics)
Documentation=https://git.ssh.surf/snxraven/peardata
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/peardata
EnvironmentFile=-/opt/peardata/.env
ExecStart=/usr/bin/node /opt/peardata/bin/peardata-server.mjs
ExecStartPost=/usr/bin/node /opt/peardata/scripts/healthcheck.js
ExecStart=/opt/peardata/peardata-server
Restart=on-failure
RestartSec=3
# Hardening
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
User=peardata
Group=peardata
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/peardata/data /opt/peardata/.env
Environment=NODE_ENV=production
EnvironmentFile=-/opt/peardata/.env
# Identity + HyperDB / metric buffers
ReadWritePaths=/opt/peardata
[Install]
WantedBy=multi-user.target
+1
View File
@@ -61,6 +61,7 @@ DRY_RUN=1 bash scripts/gitea-rolling-release.sh
| Script | Role |
|--------|------|
| `scripts/install.sh` | End-user installer (server systemd + desktop client) |
| `scripts/hosts.cjs` | Host lists (`SERVER_LINUX`, `ALL_64`) |
| `scripts/make.cjs` | Orchestrate server/client builds |
| `scripts/bare-standalone.cjs` | Pack Bare `peardata-server` |
+44 -15
View File
@@ -2,24 +2,51 @@
## Requirements
- Node.js **≥ 20**
- Pear runtime (for desktop): install via Holepunch/Pear docs
- Linux recommended for full net/disk `/proc` collectors (macOS gets CPU/RAM/load)
- **Installer path:** Linux (server) and/or Linux/macOS (desktop client); `curl` + `tar`
- **From source:** Node.js **≥ 20**; Pear runtime optional for `pear run`
- Linux recommended for full net/disk `/proc` collectors
## Install
## One-line installer (recommended)
Pulls **`rolling`** release binaries (same forge as PearDock-style installs):
```bash
curl -fsSL https://git.ssh.surf/snxraven/peardata/raw/branch/main/scripts/install.sh | bash
```
Non-interactive:
```bash
# Linux agent → /opt/peardata + peardata.service
curl -fsSL …/scripts/install.sh | bash -s -- --server --yes
# Desktop client
curl -fsSL …/scripts/install.sh | bash -s -- --client --yes
```
| Role | What you get |
|------|----------------|
| Server | Bare `peardata-server` in `/opt/peardata`, user `peardata`, systemd unit |
| Client | Electron app (`~/Applications/peardata.app` on macOS, `~/.local/share/peardata` on Linux) |
Overrides: `PEARDATA_GITEA_URL`, `PEARDATA_OWNER`, `PEARDATA_REPO`, `PEARDATA_TAG`, `PEARDATA_VERSION`, `PEARDATA_SERVER_DIR`, `PEARDATA_YES=1`.
After the server starts once:
```bash
sudo grep -E '^(SERVER_PUBLIC_KEY|SERVER_SEED)=' /opt/peardata/.env
curl -s http://127.0.0.1:19999/api/v3/info
```
## Install from source
```bash
cd peardata
npm install
cp .env.example .env # optional
```
Copy env template if needed:
```bash
cp .env.example .env
```
## Run the agent
## Run the agent (source)
```bash
npm run start:server
@@ -83,17 +110,19 @@ npm start
## systemd (Linux)
Prefer the [one-line installer](#one-line-installer-recommended) (`--server`). It installs the Bare binary and writes `peardata.service`.
Manual (matches installer layout):
```bash
sudo mkdir -p /opt/peardata
sudo rsync -a ./ /opt/peardata/ --exclude node_modules
cd /opt/peardata && sudo npm ci --omit=dev
# After placing peardata-server + .env under /opt/peardata
sudo cp deploy/peardata.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now peardata
journalctl -u peardata -f
```
Ensure `ReadWritePaths` can write `/opt/peardata/data` and `.env`.
From-source Node agents can still use `npm run start:server` under a custom unit; packaged releases use `/opt/peardata/peardata-server`.
## Roles cheat sheet
+8
View File
@@ -9,6 +9,14 @@
Same layout as PearDocks rolling pipeline, except PearData agents ship **Linux-only**.
## Installer
```bash
curl -fsSL https://git.ssh.surf/snxraven/peardata/raw/branch/main/scripts/install.sh | bash
```
Script: [`scripts/install.sh`](../scripts/install.sh) · unit: [`deploy/peardata.service`](../deploy/peardata.service)
## Local builds
```bash
+2 -1
View File
@@ -64,7 +64,8 @@
"make:client:win32-x64": "electron-forge package --platform win32 --arch x64",
"make:client:win32-arm64": "electron-forge package --platform win32 --arch arm64",
"sign:macos": "node scripts/sign-macos-app.cjs",
"release:rolling": "bash scripts/gitea-rolling-release.sh"
"release:rolling": "bash scripts/gitea-rolling-release.sh",
"install:help": "bash scripts/install.sh --help"
},
"dependencies": {
"b4a": "^1.8.1",
+8
View File
@@ -186,6 +186,14 @@ cat >"$DIST/RELEASE_NOTES.md" <<EOF
| **Server** (Bare) | \`linux-x64\`, \`linux-arm64\` only |
| **Client** (Electron) | \`linux-x64\`, \`linux-arm64\`, \`darwin-x64\`, \`darwin-arm64\`, \`win32-x64\`, \`win32-arm64\` |
## Installer
\`\`\`bash
curl -fsSL ${GITEA_URL%/}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/main/scripts/install.sh | bash
# or: … | bash -s -- --server --yes
# or: … | bash -s -- --client --yes
\`\`\`
## Server (Bare standalone)
\`\`\`bash
+638
View File
@@ -0,0 +1,638 @@
#!/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}"
}
# ─── 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"
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"
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
[Service]
Type=simple
WorkingDirectory=${SERVER_DIR}
ExecStart=${SERVER_DIR}/peardata-server
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
User=peardata
Group=peardata
NoNewPrivileges=true
PrivateTmp=true
Environment=NODE_ENV=production
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
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
REST: http://127.0.0.1:19999/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 "$@"