579f49ba13
Unit Tests / test (push) Successful in 15m24s
install.sh now guides the user through the full identity setup before running make install: - Cell name prompt with format validation and pic.ngo availability check - Domain mode selection: pic.ngo / Cloudflare / DuckDNS / HTTP-01 / LAN - Cloudflare API token: collected and verified against CF tokens/verify API - DuckDNS: subdomain + token verified against duckdns.org/update - HTTP-01: domain name collected, port-80 warning shown - All collected values passed as env vars to make install - After two failed token attempts user can continue (re-verified at boot) - Final banner shows configured cell name and domain setup_cell.py: updated to handle all domain modes - Reads DOMAIN_MODE / CELL_DOMAIN_NAME / CLOUDFLARE_API_TOKEN / DUCKDNS_TOKEN / DUCKDNS_SUBDOMAIN from env - write_cell_config() now writes domain_mode + domain_name to _identity and builds the ddns section for each provider (not hardcoded to pic_ngo) - register_with_ddns() only called when DOMAIN_MODE == 'pic_ngo' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
562 lines
21 KiB
Bash
Executable File
562 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# Personal Internet Cell (PIC) — Bash Installer
|
||
# =============================================================================
|
||
#
|
||
# SECURITY NOTICE
|
||
# ---------------
|
||
# You are about to execute this script with elevated privileges.
|
||
# ALWAYS review a script before running it:
|
||
#
|
||
# curl -fsSL https://git.pic.ngo/roof/pic/raw/branch/main/install.sh | less
|
||
#
|
||
# SHA256 checksum (verify before running):
|
||
# PLACEHOLDER — updated when script is published at git.pic.ngo
|
||
#
|
||
# Verify with:
|
||
# sha256sum install.sh
|
||
# # or, via curl before piping:
|
||
# curl -fsSL https://git.pic.ngo/roof/pic/raw/branch/main/install.sh \
|
||
# | sha256sum
|
||
#
|
||
# =============================================================================
|
||
#
|
||
# Usage:
|
||
# sudo bash install.sh # Standard install
|
||
# sudo bash install.sh --force # Bypass idempotency check
|
||
# sudo PIC_DIR=/srv/pic bash install.sh # Custom install directory
|
||
#
|
||
# Supported OS: Debian/Ubuntu (apt), Fedora/RHEL (dnf), Alpine Linux (apk)
|
||
#
|
||
# =============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Configuration
|
||
# ---------------------------------------------------------------------------
|
||
PIC_DIR="${PIC_DIR:-/opt/pic}"
|
||
PIC_REPO="${PIC_REPO:-https://git.pic.ngo/roof/pic.git}"
|
||
PIC_USER="${PIC_USER:-pic}"
|
||
API_HEALTH_URL="http://127.0.0.1:3000/health"
|
||
API_HEALTH_TIMEOUT=60
|
||
WEBUI_PORT=8081
|
||
FORCE=0
|
||
|
||
# Parse flags
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--force) FORCE=1 ;;
|
||
*)
|
||
echo "Unknown argument: $arg" >&2
|
||
echo "Usage: $0 [--force]" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Color output
|
||
# ---------------------------------------------------------------------------
|
||
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && tput setaf 1 >/dev/null 2>&1; then
|
||
RED="$(tput setaf 1)"
|
||
GREEN="$(tput setaf 2)"
|
||
YELLOW="$(tput setaf 3)"
|
||
BOLD="$(tput bold)"
|
||
RESET="$(tput sgr0)"
|
||
else
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[0;33m'
|
||
BOLD='\033[1m'
|
||
RESET='\033[0m'
|
||
fi
|
||
|
||
log_step() { printf "\n${BOLD}[%s/%s] %s${RESET}\n" "$1" "$TOTAL_STEPS" "$2"; }
|
||
log_ok() { printf " ${GREEN}✓${RESET} %s\n" "$1"; }
|
||
log_warn() { printf " ${YELLOW}⚠${RESET} %s\n" "$1"; }
|
||
log_error() { printf "\n${RED}${BOLD}ERROR:${RESET}${RED} %s${RESET}\n" "$1" >&2; }
|
||
|
||
die() { log_error "$1"; exit 1; }
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Interactive prompt helpers (use /dev/tty so they work even with piped stdin)
|
||
# ---------------------------------------------------------------------------
|
||
prompt() {
|
||
# prompt <label> <default> <var>
|
||
local _label="$1" _default="$2" _var="$3" _inp=''
|
||
if [ -n "$_default" ]; then
|
||
printf " %s [%s]: " "$_label" "$_default" >/dev/tty
|
||
else
|
||
printf " %s: " "$_label" >/dev/tty
|
||
fi
|
||
read -r _inp </dev/tty || true
|
||
[ -z "$_inp" ] && _inp="$_default"
|
||
eval "${_var}=\${_inp}"
|
||
}
|
||
|
||
prompt_secret() {
|
||
# prompt_secret <label> <var>
|
||
local _label="$1" _var="$2" _inp=''
|
||
printf " %s: " "$_label" >/dev/tty
|
||
stty -echo </dev/tty 2>/dev/null || true
|
||
read -r _inp </dev/tty || true
|
||
stty echo </dev/tty 2>/dev/null || true
|
||
printf "\n" >/dev/tty
|
||
eval "${_var}=\${_inp}"
|
||
}
|
||
|
||
verify_cf_token() {
|
||
local _token="$1" _result=''
|
||
_result=$(curl -fsSm 10 \
|
||
-H "Authorization: Bearer ${_token}" \
|
||
"https://api.cloudflare.com/client/v4/user/tokens/verify" 2>/dev/null) || true
|
||
echo "$_result" | grep -q '"success":true'
|
||
}
|
||
|
||
verify_duckdns() {
|
||
local _sub="$1" _token="$2" _result=''
|
||
_result=$(curl -fsSm 10 \
|
||
"https://www.duckdns.org/update?domains=${_sub}&token=${_token}&ip=" 2>/dev/null) || true
|
||
[ "$_result" = "OK" ]
|
||
}
|
||
|
||
check_pic_ngo_available() {
|
||
local _name="$1" _result=''
|
||
_result=$(curl -fsSm 10 \
|
||
"https://ddns.pic.ngo/api/v1/check/${_name}" 2>/dev/null) || true
|
||
echo "$_result" | grep -q '"available":true'
|
||
}
|
||
|
||
TOTAL_STEPS=8
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Must run as root
|
||
# ---------------------------------------------------------------------------
|
||
if [ "$(id -u)" -ne 0 ]; then
|
||
die "This installer must be run as root (use sudo)."
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Idempotency guard
|
||
# ---------------------------------------------------------------------------
|
||
if [ -f "${PIC_DIR}/.installed" ] && [ "$FORCE" -eq 0 ]; then
|
||
printf "${YELLOW}Already installed.${RESET} Run ${BOLD}'make update'${RESET} to update.\n"
|
||
printf "To force a full reinstall, run: ${BOLD}$0 --force${RESET}\n"
|
||
exit 0
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 1 — Detect OS / package manager
|
||
# ---------------------------------------------------------------------------
|
||
log_step 1 "Detecting operating system..."
|
||
|
||
PKG_MANAGER=""
|
||
OS_ID=""
|
||
|
||
if [ -f /etc/os-release ]; then
|
||
# shellcheck source=/dev/null
|
||
. /etc/os-release
|
||
OS_ID="${ID:-unknown}"
|
||
fi
|
||
|
||
case "$OS_ID" in
|
||
ubuntu|debian|raspbian)
|
||
PKG_MANAGER="apt"
|
||
;;
|
||
fedora)
|
||
PKG_MANAGER="dnf"
|
||
;;
|
||
rhel|centos|almalinux|rocky)
|
||
PKG_MANAGER="dnf"
|
||
;;
|
||
alpine)
|
||
PKG_MANAGER="apk"
|
||
;;
|
||
*)
|
||
# Last-resort detection
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
PKG_MANAGER="apt"
|
||
elif command -v dnf >/dev/null 2>&1; then
|
||
PKG_MANAGER="dnf"
|
||
elif command -v apk >/dev/null 2>&1; then
|
||
PKG_MANAGER="apk"
|
||
else
|
||
die "Unsupported OS '${OS_ID}'. PIC requires Debian/Ubuntu, Fedora/RHEL, or Alpine Linux."
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
log_ok "Detected OS: ${OS_ID} (package manager: ${PKG_MANAGER})"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 2 — Install required packages
|
||
# ---------------------------------------------------------------------------
|
||
log_step 2 "Installing dependencies..."
|
||
|
||
case "$PKG_MANAGER" in
|
||
|
||
apt)
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -qq
|
||
apt-get install -y -qq git curl make docker.io docker-compose-plugin 2>&1 \
|
||
| grep -v "^$" | sed 's/^/ /' || true
|
||
|
||
# Verify docker compose plugin installed
|
||
if ! docker compose version >/dev/null 2>&1; then
|
||
log_warn "docker-compose-plugin not available; falling back to standalone docker-compose"
|
||
apt-get install -y -qq docker-compose 2>&1 | grep -v "^$" | sed 's/^/ /' || true
|
||
fi
|
||
;;
|
||
|
||
dnf)
|
||
dnf install -y -q git curl make docker 2>&1 | sed 's/^/ /' || true
|
||
|
||
# Enable and start Docker (dnf installs but doesn't enable it)
|
||
systemctl enable --now docker >/dev/null 2>&1 || true
|
||
|
||
# Docker Compose plugin comes bundled with the Docker CE package on Fedora/RHEL.
|
||
# If not present, install via the docker-compose-plugin package (Docker CE repo).
|
||
if ! docker compose version >/dev/null 2>&1; then
|
||
log_warn "docker compose plugin not found; installing docker-compose-plugin..."
|
||
dnf install -y -q docker-compose-plugin 2>&1 | sed 's/^/ /' || true
|
||
fi
|
||
;;
|
||
|
||
apk)
|
||
apk add --quiet git curl make docker docker-cli-compose 2>&1 | sed 's/^/ /' || true
|
||
|
||
# Enable Docker on Alpine (OpenRC)
|
||
rc-update add docker default >/dev/null 2>&1 || true
|
||
service docker start >/dev/null 2>&1 || true
|
||
;;
|
||
|
||
esac
|
||
|
||
# Final sanity checks
|
||
command -v git >/dev/null 2>&1 || die "git could not be installed. Aborting."
|
||
command -v curl >/dev/null 2>&1 || die "curl could not be installed. Aborting."
|
||
command -v make >/dev/null 2>&1 || die "make could not be installed. Aborting."
|
||
command -v docker >/dev/null 2>&1 || die "docker could not be installed. Aborting."
|
||
|
||
docker compose version >/dev/null 2>&1 || \
|
||
docker-compose version >/dev/null 2>&1 || \
|
||
die "Neither 'docker compose' (plugin) nor 'docker-compose' is available. Aborting."
|
||
|
||
log_ok "All dependencies satisfied"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 3 — Create system user
|
||
# ---------------------------------------------------------------------------
|
||
log_step 3 "Configuring system user..."
|
||
|
||
if ! id "$PIC_USER" >/dev/null 2>&1; then
|
||
case "$PKG_MANAGER" in
|
||
apk)
|
||
adduser -S -D -H -s /sbin/nologin "$PIC_USER"
|
||
;;
|
||
*)
|
||
useradd --system --no-create-home --shell /usr/sbin/nologin "$PIC_USER"
|
||
;;
|
||
esac
|
||
log_ok "Created system user: ${PIC_USER}"
|
||
else
|
||
log_ok "System user already exists: ${PIC_USER}"
|
||
fi
|
||
|
||
# Ensure docker group exists and user is in it
|
||
if ! getent group docker >/dev/null 2>&1; then
|
||
groupadd docker
|
||
log_ok "Created docker group"
|
||
fi
|
||
|
||
if ! id -nG "$PIC_USER" | grep -qw docker; then
|
||
usermod -aG docker "$PIC_USER"
|
||
log_ok "Added ${PIC_USER} to docker group"
|
||
else
|
||
log_ok "${PIC_USER} is already in docker group"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 4 — Clone or update repository
|
||
# ---------------------------------------------------------------------------
|
||
log_step 4 "Setting up repository at ${PIC_DIR}..."
|
||
|
||
if [ -d "${PIC_DIR}/.git" ]; then
|
||
log_warn "Repository already cloned — running git pull"
|
||
git -C "$PIC_DIR" pull --ff-only 2>&1 | sed 's/^/ /'
|
||
log_ok "Repository updated"
|
||
elif [ -d "$PIC_DIR" ] && [ "$(ls -A "$PIC_DIR" 2>/dev/null)" ]; then
|
||
die "${PIC_DIR} exists and is not empty and is not a git repo. Aborting to avoid data loss."
|
||
else
|
||
mkdir -p "$(dirname "$PIC_DIR")"
|
||
git clone "$PIC_REPO" "$PIC_DIR" 2>&1 | sed 's/^/ /'
|
||
log_ok "Repository cloned to ${PIC_DIR}"
|
||
fi
|
||
|
||
# Give the invoking user (or pic if run directly as root) ownership of the repo
|
||
# so they can run `make update` and other git commands without sudo.
|
||
REPO_OWNER="${SUDO_USER:-${PIC_USER}}"
|
||
chown -R "${REPO_OWNER}:${REPO_OWNER}" "$PIC_DIR"
|
||
# Allow all users to run git commands here regardless of who owns the files
|
||
git config --system --add safe.directory "$PIC_DIR" 2>/dev/null || true
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 5 — Configure cell identity
|
||
# ---------------------------------------------------------------------------
|
||
log_step 5 "Configuring cell identity..."
|
||
|
||
if [ ! -c /dev/tty ]; then
|
||
die "No interactive terminal available. Re-run with a real terminal (not piped)."
|
||
fi
|
||
|
||
printf "\n" >/dev/tty
|
||
|
||
# ── Cell name ──────────────────────────────────────────────────────────────
|
||
PIC_CELL_NAME=''
|
||
while true; do
|
||
prompt "Cell name (e.g. myhome, alice, lab)" "" PIC_CELL_NAME
|
||
if echo "$PIC_CELL_NAME" | grep -qE '^[a-z][a-z0-9-]{1,30}$'; then
|
||
break
|
||
fi
|
||
log_warn "Must start with a letter, use only lowercase letters/digits/hyphens, 2–31 chars."
|
||
done
|
||
|
||
# ── Domain / DDNS choice ───────────────────────────────────────────────────
|
||
printf "\n" >/dev/tty
|
||
printf " How will your cell be publicly reachable?\n" >/dev/tty
|
||
printf " 1) pic.ngo subdomain — free, %s.pic.ngo, fully automatic HTTPS\n" "$PIC_CELL_NAME" >/dev/tty
|
||
printf " 2) Cloudflare DNS-01 — your own domain (must use Cloudflare nameservers)\n" >/dev/tty
|
||
printf " 3) DuckDNS — free *.duckdns.org subdomain\n" >/dev/tty
|
||
printf " 4) HTTP-01 (any) — any domain, port 80 must be publicly reachable\n" >/dev/tty
|
||
printf " 5) Local only — no public domain, LAN/VPN access only\n" >/dev/tty
|
||
printf "\n" >/dev/tty
|
||
|
||
PIC_DOMAIN_MODE=''
|
||
_choice=''
|
||
while true; do
|
||
prompt "Choice" "1" _choice
|
||
case "$_choice" in
|
||
1) PIC_DOMAIN_MODE="pic_ngo"; break ;;
|
||
2) PIC_DOMAIN_MODE="cloudflare"; break ;;
|
||
3) PIC_DOMAIN_MODE="duckdns"; break ;;
|
||
4) PIC_DOMAIN_MODE="http01"; break ;;
|
||
5) PIC_DOMAIN_MODE="lan"; break ;;
|
||
*) log_warn "Enter a number from 1 to 5." ;;
|
||
esac
|
||
done
|
||
|
||
PIC_DOMAIN_NAME=''
|
||
PIC_CF_TOKEN=''
|
||
PIC_DDK_TOKEN=''
|
||
PIC_DDK_SUB=''
|
||
|
||
# ── pic.ngo ────────────────────────────────────────────────────────────────
|
||
if [ "$PIC_DOMAIN_MODE" = "pic_ngo" ]; then
|
||
PIC_DOMAIN_NAME="${PIC_CELL_NAME}.pic.ngo"
|
||
printf " Checking name availability at pic.ngo..." >/dev/tty
|
||
if check_pic_ngo_available "$PIC_CELL_NAME"; then
|
||
printf " available\n" >/dev/tty
|
||
log_ok "Will register: ${PIC_DOMAIN_NAME}"
|
||
else
|
||
printf "\n" >/dev/tty
|
||
log_warn "${PIC_DOMAIN_NAME} may already be taken or the server is unreachable."
|
||
log_warn "Registration will be retried at first boot."
|
||
fi
|
||
fi
|
||
|
||
# ── Cloudflare ─────────────────────────────────────────────────────────────
|
||
if [ "$PIC_DOMAIN_MODE" = "cloudflare" ]; then
|
||
printf "\n" >/dev/tty
|
||
while true; do
|
||
prompt "Your domain name (e.g. home.example.com)" "" PIC_DOMAIN_NAME
|
||
if echo "$PIC_DOMAIN_NAME" | grep -qiE '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z]{2,})+$'; then
|
||
break
|
||
fi
|
||
log_warn "Enter a valid fully-qualified domain name (e.g. home.example.com)."
|
||
done
|
||
printf "\n" >/dev/tty
|
||
printf " Create an API token at: Cloudflare Dashboard → My Profile → API Tokens\n" >/dev/tty
|
||
printf " Required permission: Zone / DNS / Edit (set to all zones or your specific zone)\n" >/dev/tty
|
||
printf "\n" >/dev/tty
|
||
_attempts=0
|
||
while true; do
|
||
prompt_secret "Cloudflare API token" PIC_CF_TOKEN
|
||
if [ -z "$PIC_CF_TOKEN" ]; then
|
||
log_warn "Token cannot be empty."
|
||
continue
|
||
fi
|
||
printf " Verifying token with Cloudflare..." >/dev/tty
|
||
if verify_cf_token "$PIC_CF_TOKEN"; then
|
||
printf " valid\n" >/dev/tty
|
||
log_ok "Cloudflare token verified"
|
||
break
|
||
fi
|
||
printf " invalid\n" >/dev/tty
|
||
_attempts=$((_attempts + 1))
|
||
log_warn "Verification failed — check the token has Zone / DNS / Edit permission."
|
||
if [ "$_attempts" -ge 2 ]; then
|
||
log_warn "Token failed twice. You can still continue — it will be tested again at first boot."
|
||
prompt "Press Enter to continue with this token, or Ctrl-C to abort and re-run" "" _dummy
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# ── DuckDNS ────────────────────────────────────────────────────────────────
|
||
if [ "$PIC_DOMAIN_MODE" = "duckdns" ]; then
|
||
printf "\n" >/dev/tty
|
||
printf " First create a subdomain at duckdns.org, then enter the details below.\n" >/dev/tty
|
||
printf "\n" >/dev/tty
|
||
while true; do
|
||
prompt "DuckDNS subdomain (e.g. myhome → myhome.duckdns.org)" "" PIC_DDK_SUB
|
||
if echo "$PIC_DDK_SUB" | grep -qE '^[a-z0-9][a-z0-9-]*$'; then
|
||
break
|
||
fi
|
||
log_warn "Subdomain must be lowercase letters, digits, and hyphens only."
|
||
done
|
||
PIC_DOMAIN_NAME="${PIC_DDK_SUB}.duckdns.org"
|
||
printf "\n" >/dev/tty
|
||
_attempts=0
|
||
while true; do
|
||
prompt_secret "DuckDNS token (from duckdns.org account page)" PIC_DDK_TOKEN
|
||
if [ -z "$PIC_DDK_TOKEN" ]; then
|
||
log_warn "Token cannot be empty."
|
||
continue
|
||
fi
|
||
printf " Verifying token with DuckDNS..." >/dev/tty
|
||
if verify_duckdns "$PIC_DDK_SUB" "$PIC_DDK_TOKEN"; then
|
||
printf " valid\n" >/dev/tty
|
||
log_ok "DuckDNS token verified (${PIC_DOMAIN_NAME})"
|
||
break
|
||
fi
|
||
printf " invalid\n" >/dev/tty
|
||
_attempts=$((_attempts + 1))
|
||
log_warn "Verification failed — make sure the subdomain exists at duckdns.org and the token is correct."
|
||
if [ "$_attempts" -ge 2 ]; then
|
||
log_warn "Token failed twice. You can still continue — it will be tested again at first boot."
|
||
prompt "Press Enter to continue with this token, or Ctrl-C to abort and re-run" "" _dummy
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# ── HTTP-01 ────────────────────────────────────────────────────────────────
|
||
if [ "$PIC_DOMAIN_MODE" = "http01" ]; then
|
||
printf "\n" >/dev/tty
|
||
while true; do
|
||
prompt "Your domain name (e.g. home.example.com)" "" PIC_DOMAIN_NAME
|
||
if echo "$PIC_DOMAIN_NAME" | grep -qiE '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z]{2,})+$'; then
|
||
break
|
||
fi
|
||
log_warn "Enter a valid fully-qualified domain name."
|
||
done
|
||
printf "\n" >/dev/tty
|
||
log_warn "HTTP-01 requires port 80 to be publicly reachable from the internet."
|
||
log_warn "Make sure your router forwards port 80 to this machine before completing setup."
|
||
fi
|
||
|
||
# ── Local only ─────────────────────────────────────────────────────────────
|
||
if [ "$PIC_DOMAIN_MODE" = "lan" ]; then
|
||
log_ok "Local-only mode — no public domain or DDNS will be configured."
|
||
fi
|
||
|
||
printf "\n" >/dev/tty
|
||
log_ok "Identity configured: cell=${PIC_CELL_NAME} mode=${PIC_DOMAIN_MODE}${PIC_DOMAIN_NAME:+ domain=${PIC_DOMAIN_NAME}}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 6 — Run make install
|
||
# ---------------------------------------------------------------------------
|
||
log_step 6 "Running 'make install'..."
|
||
|
||
cd "$PIC_DIR"
|
||
|
||
if ! CELL_NAME="$PIC_CELL_NAME" \
|
||
DOMAIN_MODE="$PIC_DOMAIN_MODE" \
|
||
CELL_DOMAIN_NAME="${PIC_DOMAIN_NAME:-}" \
|
||
CLOUDFLARE_API_TOKEN="${PIC_CF_TOKEN:-}" \
|
||
DUCKDNS_TOKEN="${PIC_DDK_TOKEN:-}" \
|
||
DUCKDNS_SUBDOMAIN="${PIC_DDK_SUB:-}" \
|
||
make install 2>&1 | sed 's/^/ /'; then
|
||
die "'make install' failed. Check the output above."
|
||
fi
|
||
|
||
# make install runs as root so config/ and data/ get created root-owned.
|
||
# Re-apply ownership to the invoking user so they can manage files without sudo.
|
||
chown -R "${REPO_OWNER}:${REPO_OWNER}" "$PIC_DIR"
|
||
|
||
log_ok "'make install' complete"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 7 — Start core services
|
||
# ---------------------------------------------------------------------------
|
||
log_step 7 "Starting core services..."
|
||
|
||
cd "$PIC_DIR"
|
||
|
||
if ! make start-core 2>&1 | sed 's/^/ /'; then
|
||
die "'make start-core' failed. Check the output above."
|
||
fi
|
||
|
||
log_ok "Core services started"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 8 — Health check + print wizard URL
|
||
# ---------------------------------------------------------------------------
|
||
log_step 8 "Waiting for API health check (up to ${API_HEALTH_TIMEOUT}s)..."
|
||
|
||
ELAPSED=0
|
||
HEALTHY=0
|
||
while [ "$ELAPSED" -lt "$API_HEALTH_TIMEOUT" ]; do
|
||
if curl -fsS "$API_HEALTH_URL" >/dev/null 2>&1; then
|
||
HEALTHY=1
|
||
break
|
||
fi
|
||
sleep 2
|
||
ELAPSED=$((ELAPSED + 2))
|
||
printf " Waiting... (%ds)\r" "$ELAPSED"
|
||
done
|
||
printf "\n"
|
||
|
||
if [ "$HEALTHY" -ne 1 ]; then
|
||
log_warn "API did not respond within ${API_HEALTH_TIMEOUT}s at ${API_HEALTH_URL}"
|
||
log_warn "The stack may still be starting up. Check with: make -C ${PIC_DIR} status"
|
||
log_warn "Or follow logs with: make -C ${PIC_DIR} logs"
|
||
else
|
||
log_ok "API is healthy"
|
||
fi
|
||
|
||
# Detect the host's primary outbound IP address
|
||
HOST_IP="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/{print $7; exit}' || true)"
|
||
if [ -z "$HOST_IP" ]; then
|
||
# Fallback: first non-loopback IPv4
|
||
HOST_IP="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
||
fi
|
||
HOST_IP="${HOST_IP:-<host-ip>}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Done
|
||
# ---------------------------------------------------------------------------
|
||
printf "\n${GREEN}${BOLD}============================================================${RESET}\n"
|
||
printf "${GREEN}${BOLD} PIC installed successfully!${RESET}\n"
|
||
printf "${GREEN}${BOLD}============================================================${RESET}\n"
|
||
printf "\n"
|
||
printf " Cell: ${BOLD}%s${RESET}\n" "$PIC_CELL_NAME"
|
||
if [ -n "$PIC_DOMAIN_NAME" ]; then
|
||
printf " Domain: ${BOLD}%s${RESET} (%s)\n" "$PIC_DOMAIN_NAME" "$PIC_DOMAIN_MODE"
|
||
else
|
||
printf " Domain: %s\n" "local only (LAN/VPN)"
|
||
fi
|
||
printf "\n"
|
||
printf " Open the setup wizard to set your admin password and choose services:\n"
|
||
printf "\n"
|
||
printf " ${BOLD}http://${HOST_IP}:${WEBUI_PORT}/setup${RESET}\n"
|
||
printf "\n"
|
||
printf " Useful commands:\n"
|
||
printf " make -C ${PIC_DIR} status — check service status\n"
|
||
printf " make -C ${PIC_DIR} logs — follow all service logs\n"
|
||
printf " make -C ${PIC_DIR} start — start all services\n"
|
||
printf " make -C ${PIC_DIR} stop — stop all services\n"
|
||
printf " make -C ${PIC_DIR} update — pull latest code and restart\n"
|
||
printf "\n"
|