#!/usr/bin/env bash # install.sh — install livesync-cli as a systemd service # # Usage: # install.sh [--user] [--system] [--vault ] [--interval ] # # Defaults: user install, prompts for vault path if not supplied. set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "$SCRIPT_DIR/../../../.." && pwd)" CLI_DIR="$REPO_ROOT/src/apps/cli" SERVICE_TEMPLATE="$SCRIPT_DIR/livesync-cli.service" # ── Argument parsing ──────────────────────────────────────────────────────── INSTALL_MODE="user" VAULT_PATH="" INTERVAL="" FORCE=0 while [[ $# -gt 0 ]]; do case "$1" in --user) INSTALL_MODE="user" shift ;; --system) INSTALL_MODE="system" shift ;; --vault) if [[ -z "${2:-}" ]]; then echo "Error: --vault requires a path argument" >&2 exit 1 fi VAULT_PATH="$2" shift 2 ;; --interval) if [[ -z "${2:-}" ]]; then echo "Error: --interval requires a numeric argument" >&2 exit 1 fi INTERVAL="$2" if ! [[ "$INTERVAL" =~ ^[1-9][0-9]*$ ]]; then echo "Error: --interval requires a positive integer, got '$INTERVAL'" >&2 exit 1 fi shift 2 ;; --force|-f) FORCE=1 shift ;; --help|-h) cat <] [--interval ] [--force] --user Install as a user systemd service (default, ~/.config/systemd/user/) --system Install as a system systemd service (/etc/systemd/system/) --vault Path to the vault directory (prompted if omitted) --interval Poll CouchDB every N seconds instead of using the _changes feed --force Overwrite existing service unit without prompting EOF exit 0 ;; *) echo "Error: Unknown argument: $1" >&2 exit 1 ;; esac done # ── Vault path ────────────────────────────────────────────────────────────── if [[ -z "$VAULT_PATH" ]]; then if [ ! -t 0 ]; then echo "Error: --vault is required in non-interactive mode" >&2 exit 1 fi printf 'Vault path: ' read -r VAULT_PATH fi _orig_vault="$VAULT_PATH" if ! VAULT_PATH="$(cd -- "$VAULT_PATH" 2>/dev/null && pwd)"; then echo "Error: vault directory does not exist: $_orig_vault" >&2 exit 1 fi echo "[INFO] Vault: $VAULT_PATH" echo "[INFO] Install mode: $INSTALL_MODE" # ── Build ──────────────────────────────────────────────────────────────────── echo "[INFO] Building CLI from $REPO_ROOT..." (cd "$REPO_ROOT" && npm install --silent) (cd "$CLI_DIR" && npm run build) BUILT_CJS="$CLI_DIR/dist/index.cjs" if [[ ! -f "$BUILT_CJS" ]]; then echo "Error: build output not found: $BUILT_CJS" >&2 exit 1 fi # ── Install binary ─────────────────────────────────────────────────────────── if [[ "$INSTALL_MODE" == "user" ]]; then BIN_DIR="$HOME/.local/bin" LIB_DIR="$HOME/.local/lib/livesync-cli" UNIT_DIR="$HOME/.config/systemd/user" SYSTEMCTL_FLAGS="--user" else BIN_DIR="/usr/local/bin" LIB_DIR="/usr/local/lib/livesync-cli" UNIT_DIR="/etc/systemd/system" SYSTEMCTL_FLAGS="" fi LIB_PARENT="$(dirname -- "$LIB_DIR")" mkdir -p "$BIN_DIR" "$LIB_PARENT" LIVESYNC_BIN="$BIN_DIR/livesync-cli" LIVESYNC_JS="$LIB_DIR/dist/index.cjs" # Build a complete runtime payload before replacing any previous installation. # The Vite output contains hashed sibling chunks, while some Node dependencies # deliberately remain external and must be installed next to the bundle. PAYLOAD_STAGING="$(mktemp -d "$LIB_PARENT/.livesync-cli.install.XXXXXX")" cleanup_payload() { if [[ -n "$PAYLOAD_STAGING" ]] && [[ -e "$PAYLOAD_STAGING" ]]; then rm -rf -- "$PAYLOAD_STAGING" fi } trap cleanup_payload EXIT cp "$CLI_DIR/package.json" "$PAYLOAD_STAGING/package.json" npm install --omit=dev --no-audit --no-fund --prefix "$PAYLOAD_STAGING" cp -R "$CLI_DIR/dist" "$PAYLOAD_STAGING/dist" if ! node "$PAYLOAD_STAGING/dist/index.cjs" --help >/dev/null; then echo "Error: installed CLI failed its start-up check" >&2 exit 1 fi PAYLOAD_BACKUP="" if [[ -e "$LIB_DIR" ]] || [[ -L "$LIB_DIR" ]]; then PAYLOAD_BACKUP="$(mktemp -d "$LIB_PARENT/.livesync-cli.backup.XXXXXX")" rmdir "$PAYLOAD_BACKUP" mv -- "$LIB_DIR" "$PAYLOAD_BACKUP" fi if ! mv -- "$PAYLOAD_STAGING" "$LIB_DIR"; then if [[ -n "$PAYLOAD_BACKUP" ]]; then mv -- "$PAYLOAD_BACKUP" "$LIB_DIR" fi echo "Error: failed to install the CLI files at $LIB_DIR" >&2 exit 1 fi PAYLOAD_STAGING="" if [[ -n "$PAYLOAD_BACKUP" ]]; then rm -rf -- "$PAYLOAD_BACKUP" fi trap - EXIT # Write a bash wrapper that invokes Node.js on the installed payload. cat > "$LIVESYNC_BIN" <&2 exit 1 fi printf 'Service unit already exists at %s. Overwrite? [y/N]: ' "$UNIT_PATH" read -r CONFIRM case "$CONFIRM" in [yY]|[yY][eE][sS]) : ;; *) echo "[INFO] Aborted. Existing unit left in place." exit 0 ;; esac fi # In awk gsub(), '&' in the replacement means "matched text"; escape any literal '&' # in path variables before passing them as awk replacement strings. AWK_BIN="${LIVESYNC_BIN//&/\\&}" AWK_VAULT="${VAULT_PATH//&/\\&}" awk -v bin="$AWK_BIN" -v vault="$AWK_VAULT" -v exec_start="ExecStart=$EXEC_START" \ '/^ExecStart=/ { print exec_start; next } {gsub("LIVESYNC_BIN", bin); gsub("LIVESYNC_VAULT_PATH", vault); print}' \ "$SERVICE_TEMPLATE" > "$UNIT_PATH" echo "[INFO] Installed unit: $UNIT_PATH" # ── Enable service ─────────────────────────────────────────────────────────── if ! command -v systemctl >/dev/null 2>&1; then echo "[WARN] systemctl not found — skipping service activation" echo "[INFO] To enable manually, copy $UNIT_PATH to the correct systemd directory and run:" echo " systemctl $SYSTEMCTL_FLAGS daemon-reload" echo " systemctl $SYSTEMCTL_FLAGS enable --now livesync-cli" exit 0 fi # shellcheck disable=SC2086 systemctl $SYSTEMCTL_FLAGS daemon-reload # shellcheck disable=SC2086 systemctl $SYSTEMCTL_FLAGS enable --now livesync-cli sleep 1 # shellcheck disable=SC2086 if ! systemctl $SYSTEMCTL_FLAGS is-active --quiet livesync-cli; then echo "Error: livesync-cli service did not remain active after startup." >&2 # shellcheck disable=SC2086 systemctl $SYSTEMCTL_FLAGS status livesync-cli --no-pager || true exit 1 fi echo "" echo "[Done] livesync-cli service installed and started." echo "" # shellcheck disable=SC2086 systemctl $SYSTEMCTL_FLAGS status livesync-cli --no-pager || true