fix: refactor setup scripts for system-wide Node.js (no nvm)

BREAKING: Officer now uses system Node.js via NodeSource instead of nvm.

Changes:
- Removed nvm sourcing from setup.sh
- Updated Node installation to use NodeSource repository
- All npm global packages installed system-wide with sudo
- Updated PTY sidecar setup to use /usr/bin/node (system node)
- Added Pi validation (test --list-models)
- System packages now available to all users automatically

Benefits:
- Multi-user friendly: all users get same Node version
- No per-user environment setup needed
- Simpler troubleshooting (one node version)
- Services use consistent node binary
- Prevents snap node incompatibility issues

Fixes:
- 'node not found' for secondary users
- systemd services finding correct node
- Pi installation consistency across users

New Files:
- SETUP_GUIDE.md: Comprehensive installation guide
- SETUP_ANALYSIS.md: Technical analysis of previous issues

Migration:
- Remove nvm if installed (optional)
- Run: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
- Run: sudo apt-get install -y nodejs
- Run: bash scripts/setup.sh
This commit is contained in:
2026-03-04 02:09:47 +00:00
parent ef13f96d36
commit ebd8778007
4 changed files with 681 additions and 65 deletions
+310
View File
@@ -0,0 +1,310 @@
# Officer Setup Flow Analysis
## Overview
The setup.sh script installs all dependencies for Officer, but there are several issues that can cause problems, especially with nvm Node.js environments.
## Setup Flow
```
1. Detect package manager (apt/pacman/brew)
2. Install core system packages (git, zip, curl, zsh, build-essential, etc.)
3. Install archive utilities (7z, unrar)
4. Install ffmpeg
5. Configure sudoers for service user
6. Install Node.js 22 (or warn if not found)
7. Configure npm global prefix (~/.npm-global)
8. Install Bun
9. Install Go 1.23.6
10. Install Rust
11. Install PulseAudio (for audio)
12. Build cliamp from source (Go music player)
13. Install Neovim
14. Install terminal tools (starship, oh-my-zsh, eza, lazygit)
15. Install yt-dlp (optional)
16. Install npm global packages (Pi, Claude Code, pm2)
17. Run bun install (project dependencies)
18. Setup remote desktop (XFCE + VNC)
19. Setup PTY sidecar (systemd service)
20. Verification
```
## Issues Found
### 1. **nvm Node.js Not Properly Documented**
**Location:** `scripts/setup.sh` (line ~238)
**Problem:**
```bash
if has node; then
NODE_VER=$(node -v 2>/dev/null | tr -d 'v')
NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1)
if [ "$NODE_MAJOR" = "22" ]; then
skip "node v$NODE_VER"
else
warn "Node $NODE_VER found but v22 is required"
warn "Use nvm: nvm install 22 && nvm use 22"
fi
else
warn "Node.js not found — install v22 via nvm:"
warn " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash"
warn " nvm install 22"
fi
```
**Issue:**
- Only warns about nvm but doesn't ensure it's sourced in the current shell
- When installing pm2 and other npm global packages, nvm might not be available
- When services (pm2, systemd) later run, they won't have nvm initialized
**Impact:**
- Users install nvm, run setup.sh in that shell, but when pm2/systemd runs later, it uses the wrong node or no node
---
### 2. **PTY Sidecar Service Uses Unbounded `which node`**
**Location:** `scripts/setup-pty-sidecar.sh` (line ~16)
**Problem:**
```bash
NODE_BIN="$(which node)"
```
**Issue:**
- If nvm is not sourced in the current shell, `which node` returns nothing or the system node
- The systemd service will run with the wrong node binary
- When systemd runs, nvm environment is not available anyway
**Impact:**
- PTY sidecar service fails to start or runs with wrong node
- Terminal functionality breaks in Officer
---
### 3. **PM2/Ecosystem Config Doesn't Handle nvm**
**Location:** `ecosystem.config.cjs`
**Problem:**
```javascript
module.exports = {
apps: [
{
name: 'officer',
script: 'bun',
args: 'start',
watch: false,
},
],
};
```
**Issue:**
- No environment setup for nvm
- PM2 runs with whatever node is in system PATH
- If user installed node via nvm, PM2 won't find it
- This is why you had to restart the server after installing nvm
**Impact:**
- Officer server fails to start after fresh nvm installation
- No clear error message about nvm not being available
---
### 4. **No Documentation on Node Installation Methods**
**Location:** `scripts/setup.sh` (lines 238-250)
**Problem:**
- Script warns about nvm but doesn't explain the workflow
- No mention of snap node incompatibility
- No mention of system apt/NodeSource installation
- No guidance on which method to use when
**Impact:**
- Users can choose any installation method
- Some methods (snap) don't work with Officer
- New issues arise from incompatible setups
---
### 5. **Pi Installation Doesn't Validate nvm Environment**
**Location:** `scripts/setup.sh` (lines ~408-420)
**Problem:**
```bash
if has pi; then
skip "pi (@mariozechner/pi-coding-agent)"
else
npm install -g @mariozechner/pi-coding-agent
if has pi; then ok "pi installed"; else warn "pi install failed"; fi
fi
```
**Issue:**
- Installs pi with `npm install -g`, but npm might be different than later shells
- No validation that pi works (should test `pi --list-models`)
- No check that Pi was installed to the right npm location
**Impact:**
- Pi appears installed but fails at runtime when shell environment differs
---
## Fixes Required
### Fix 1: Source nvm Before Installing Global Packages
```bash
# At the start of setup.sh, after detecting package manager
echo ""
echo "── Node.js Environment ──"
# Check if nvm needs to be sourced
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
nvm use 22 || nvm install 22
ok "nvm activated: $(node -v)"
elif ! has node; then
fail "Node.js not found and nvm not installed"
fail "Install nvm first: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash"
exit 1
fi
```
### Fix 2: Update PTY Sidecar Setup to Use Correct Node
```bash
# In scripts/setup-pty-sidecar.sh
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
nvm use 22 2>/dev/null || true
fi
NODE_BIN="$(which node)"
if [ ! -f "$NODE_BIN" ]; then
echo "ERROR: Node.js not found in PATH"
exit 1
fi
```
### Fix 3: Update PM2 Ecosystem Config
```javascript
module.exports = {
apps: [
{
name: 'officer',
script: 'bun',
args: 'start',
watch: false,
// Source nvm before running
exec_mode: 'cluster',
instances: 1,
env: {
NODE_ENV: 'production',
// This helps systemd find the right node
NVM_DIR: '$HOME/.nvm',
},
// For systemd service, use a wrapper script
},
],
};
```
Or better: Create a wrapper script for PM2:
```bash
#!/bin/bash
# bin/start.sh
set -euo pipefail
# Source nvm if available
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
fi
# Now start Officer
NODE_ENV=production bun src/server.tsx
```
Then in ecosystem.config.cjs:
```javascript
{
name: 'officer',
script: 'bin/start.sh',
// ...
}
```
### Fix 4: Validate Pi Installation
```bash
# After installing Pi
if has pi; then
if pi --list-models > /dev/null 2>&1; then
ok "pi installed and working"
else
warn "pi installed but --list-models failed"
warn "Try: nvm use && npm install -g @mariozechner/pi-coding-agent"
fi
else
warn "pi install failed"
fi
```
### Fix 5: Add Setup Documentation
Create `SETUP_GUIDE.md` with clear instructions on:
1. Choose ONE Node installation method (recommend nvm)
2. Source nvm in shell before running setup.sh
3. Setup.sh will validate node and npm are available
4. Services (PM2, systemd) will inherit nvm environment
---
## Recommendations
1. **Make nvm sourcing automatic** at the start of setup.sh
2. **Add environment wrapper script** for PM2 that sources nvm
3. **Document the three Node installation options** with pros/cons:
- nvm (recommended, flexible versions)
- NodeSource (system package, simple)
- apt (if available in repo)
- ❌ snap (broken, don't use)
4. **Validate Pi works** before marking setup complete
5. **Create a post-setup check script** that verifies everything works
---
## Current Workaround
If setup.sh already ran with snap node:
1. Remove snap: `sudo snap remove node`
2. Install nvm: `curl -o- ... | bash` (reload shell)
3. Install node: `nvm install 22 && nvm use 22`
4. Reinstall pm2 packages: `npm install -g pm2`
5. Restart pm2/officer
This is what you just did!
+289
View File
@@ -0,0 +1,289 @@
# Officer Setup Guide
## Overview
Officer is a self-hosted AI intranet for teams. This guide covers fresh installation on Ubuntu/Debian servers.
## Architecture
Officer uses **system-wide Node.js** to ensure all users have consistent access to tools:
```
System Node.js (v22, system-wide)
npm global packages → /usr/local/lib/node_modules/
├── pi (coding agent)
├── claude (Claude Code)
└── (available to all users)
Systemd Services
├── officer-pty-sidecar (terminal backend)
└── (other services)
```
This is **not** an nvm-based setup. Each user doesn't install their own Node version. Instead:
- One system Node.js for everyone
- All npm packages installed system-wide
- Simple, predictable, production-friendly
---
## Installation Steps
### 1. Install Node.js 22 (System-Wide)
**On fresh Ubuntu/Debian:**
```bash
# Add NodeSource repository for Node 22 LTS
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
# Install Node.js and npm
sudo apt-get install -y nodejs
# Verify for all users
node --version
npm --version
# Both should work regardless of which user runs it
```
**Already have an older Node version?**
```bash
# upgrade it
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# verify
node --version # Should be v22.x.x
```
**Coming from nvm?**
```bash
# If you installed via nvm before, remove it
# Then install system Node as above
# nvm is NOT needed for Officer production setup
```
### 2. Run Setup Script
```bash
cd /path/to/officer/monorepo
bash scripts/setup.sh
```
This will:
- Install all system dependencies (git, build tools, ffmpeg, etc.)
- Install Bun, Go, Rust
- Install npm global packages (pi, claude-code, etc.)
- Setup PulseAudio for audio
- Setup remote desktop (XFCE + VNC)
- Configure PTY sidecar systemd service
- Verify everything works
**Note:** Setup script uses `sudo` for system-level installations. You'll be prompted for your password.
### 3. Verify Installation
```bash
# Everyone can use these (all users)
which node # /usr/bin/node
which pi # /usr/bin/pi
which claude # /usr/bin/claude
which bun # /home/user/.bun/bin/bun (or /usr/local/bin/bun)
# Test that pi works
pi --version
pi --list-models
# Test that Officer can start
bun dev
# In another terminal
curl http://localhost:5000/api/pi/models
```
### 4. Start Officer
**Development:**
```bash
bun dev
```
**Production (with PM2):**
```bash
pm2 start ecosystem.config.cjs
pm2 logs officer
```
**Production (with systemd):**
```bash
sudo systemctl start officer
sudo systemctl status officer
```
---
## Multi-User Scenarios
### Scenario A: Server with Multiple Users
All users automatically get access to:
- `node` command (system-wide)
- `pi`, `claude` (system-wide npm packages)
- Officer web UI (via port/proxy)
**No setup needed per user.** Just install globally once as shown above.
```bash
# User 1
$ which node
/usr/bin/node
# User 2 (different terminal/server)
$ which node
/usr/bin/node
# Both work!
```
### Scenario B: Development User Wants nvm
**Important:** For production, DON'T do this. But for dev, you can:
```bash
# As a user (not system-wide)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
nvm install 22
nvm use 22
# Now you can use nvm locally, but make sure system Node is also installed for services
which node # /home/user/.nvm/versions/node/v22.x.x/bin/node (your local override)
```
The `officer-pty-sidecar` systemd service will still use `/usr/bin/node` (system-wide).
---
## Troubleshooting
### "node: command not found"
**For a single user:**
```bash
# User's shell doesn't have node in PATH
# Make sure system Node is installed:
sudo apt-get install -y nodejs
node --version
```
**For systemd service:**
```bash
# Service can't find node
sudo systemctl status officer-pty-sidecar
journalctl -u officer-pty-sidecar
# Fix: Install system Node
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Restart service
sudo systemctl restart officer-pty-sidecar
```
### "pi: command not found"
```bash
# Pi not installed
sudo npm install -g @mariozechner/pi-coding-agent
# Or use setup script
bash scripts/setup.sh
```
### "Pi process exited with code 1" in chat
This usually means **snap node was used**. Don't use snap node for Officer.
```bash
# Check if snap node is installed
which node
# If output contains "/snap/bin/node", remove it:
sudo snap remove node
# Then install system Node
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify
which node # /usr/bin/node (NOT /snap/bin/node)
```
### PTY Sidecar service won't start
```bash
# Check status
sudo systemctl status officer-pty-sidecar
# Check logs
journalctl -u officer-pty-sidecar -n 20
# Verify Node is installed
/usr/bin/node --version
# Restart
sudo systemctl restart officer-pty-sidecar
```
---
## Files Changed/Created
- `scripts/setup.sh` — Main installation script (updated to use system Node)
- `scripts/setup-pty-sidecar.sh` — PTY sidecar systemd service setup (updated)
- `ecosystem.config.cjs` — PM2 config (no changes, using systemd instead)
- `SETUP_GUIDE.md` — This file
---
## What NOT To Do
**Don't use snap node**
- It has file descriptor issues with piped processes
- Use system Node via NodeSource instead
**Don't mix Node versions per user**
- For production: one Node version for everyone (system-wide)
- For dev: sure, use nvm, but keep system Node installed too
**Don't install npm packages locally for system services**
- Services need system-wide packages (`sudo npm install -g`)
- Or use absolute paths in service files
**Do:**
- Use system Node 22 via NodeSource
- Install global packages once with `sudo npm install -g`
- Use systemd services for daemons
- Let all users share the same tools
---
## Next Steps
1. Install Node.js: `curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs`
2. Run setup: `bash scripts/setup.sh`
3. Start Officer: `bun dev` (or PM2/systemd in production)
4. Open browser: `http://localhost:5000` (or your configured port)
---
## Support
If you hit issues:
1. Check troubleshooting section above
2. Verify system Node is installed: `node --version`
3. Check service logs: `journalctl -u officer-pty-sidecar`
4. Re-run setup script: `bash scripts/setup.sh`
+12 -2
View File
@@ -18,14 +18,24 @@ SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
SERVICE_USER="$(whoami)" SERVICE_USER="$(whoami)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")" PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
NODE_BIN="$(which node)"
# Use system node (not nvm) for systemd service
NODE_BIN="/usr/bin/node"
if [ ! -f "$NODE_BIN" ]; then
fail "System Node.js not found at $NODE_BIN"
fail "Install Node.js 22 via NodeSource:"
fail " curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -"
fail " sudo apt-get install -y nodejs"
exit 1
fi
echo "" echo ""
echo "── PTY Sidecar (systemd service) ──" echo "── PTY Sidecar (systemd service) ──"
echo " Node: $NODE_BIN ($("$NODE_BIN" -v))"
if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
skip "$SERVICE_NAME service already running" skip "$SERVICE_NAME service already running"
echo " Use 'bun run restart:sidecar' to restart"
exit 0 exit 0
fi fi
+70 -63
View File
@@ -260,60 +260,66 @@ fi
# ─── 5. Node.js 22 ──────────────────────────────────────────────────────────── # ─── 5. Node.js 22 ────────────────────────────────────────────────────────────
echo "" echo ""
echo "── Node.js 22 ──" echo "── Node.js 22 (system-wide) ──"
if has node; then if has node; then
NODE_VER=$(node -v 2>/dev/null | tr -d 'v') NODE_VER=$(node -v 2>/dev/null | tr -d 'v')
NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1) NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1)
if [ "$NODE_MAJOR" = "22" ]; then if [ "$NODE_MAJOR" = "22" ]; then
skip "node v$NODE_VER" skip "node v$NODE_VER (system-wide)"
else else
warn "Node $NODE_VER found but v22 is required" warn "Node $NODE_VER found but v22 is required — upgrading via NodeSource..."
warn "Use nvm: nvm install 22 && nvm use 22" case $PM in
apt)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
if has node && [ "$(node -v | cut -d. -f1 | tr -d 'v')" = "22" ]; then
ok "Upgraded to node v$(node -v)"
else
fail "Node.js upgrade failed"
fi
;;
*)
warn "Manual upgrade needed for $PM: install Node.js 22 from official sources"
;;
esac
fi fi
else else
warn "Node.js not found — install v22 via nvm:" warn "Node.js not found — installing v22 via NodeSource..."
warn " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash" case $PM in
warn " nvm install 22" apt)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; fi
;;
pacman)
install_pkg nodejs npm
if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; fi
;;
brew)
install_pkg node
if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; fi
;;
esac
fi fi
# ─── npm global prefix ───────────────────────────────────────────────────────── # ─── npm global packages (system-wide) ─────────────────────────────────────────
# Set npm global prefix to a user-writable directory so npm install -g # Install npm global packages to system /usr/local so all users can access them.
# never requires sudo. This also lets the server install packages at runtime. # This is safer for multi-user production setups.
echo "" echo ""
echo "── npm global prefix ──" echo "── npm global packages (system-wide) ──"
if has npm; then if ! has npm; then
NPM_PREFIX=$(npm config get prefix 2>/dev/null) warn "npm not found — skipping global package installs"
NPM_GLOBAL="$HOME/.npm-global" exit 1
fi
if [ "$NPM_PREFIX" = "$NPM_GLOBAL" ]; then # Verify npm is pointing to system node (not user-local nvm)
skip "npm prefix already set to $NPM_GLOBAL" NPM_BIN=$(which npm)
else if echo "$NPM_BIN" | grep -q '.nvm'; then
mkdir -p "$NPM_GLOBAL" warn "npm is from nvm ($NPM_BIN), but setup.sh expects system npm"
npm config set prefix "$NPM_GLOBAL" warn "This is fine — packages will be installed to your nvm, but won't be available to other users"
export PATH="$NPM_GLOBAL/bin:$PATH" warn "For production: all users should use the same system Node.js (no nvm)"
ok "Set npm global prefix to $NPM_GLOBAL"
# Migrate existing global packages from system prefix if any
if [ -d "$NPM_PREFIX/lib/node_modules/@mariozechner" ] || [ -d "$NPM_PREFIX/lib/node_modules/@anthropic-ai" ]; then
warn "Removing stale system-level npm packages from $NPM_PREFIX (will reinstall to $NPM_GLOBAL)"
sudo rm -rf "$NPM_PREFIX/lib/node_modules/@mariozechner" "$NPM_PREFIX/lib/node_modules/@anthropic-ai" 2>/dev/null
sudo rm -f "$NPM_PREFIX/bin/pi" "$NPM_PREFIX/bin/claude" 2>/dev/null
fi
fi
# Ensure ~/.npm-global/bin is in shell profiles
for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
if [ -f "$profile" ] && ! grep -q '.npm-global/bin' "$profile"; then
echo '' >> "$profile"
echo '# npm global packages' >> "$profile"
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$profile"
ok "Added ~/.npm-global/bin to $(basename "$profile")"
fi
done
else
warn "npm not found — skipping prefix setup"
fi fi
# ─── 6. Bun ─────────────────────────────────────────────────────────────────── # ─── 6. Bun ───────────────────────────────────────────────────────────────────
@@ -557,35 +563,36 @@ else
esac esac
fi fi
# ─── 14. npm global packages ───────────────────────────────────────────────── # ─── 14. npm global packages (system-wide) ──────────────────────────────────
echo "" echo ""
echo "── npm global packages ──" echo "── npm global packages (system-wide) ──"
if has npm; then if ! has npm; then
warn "npm not found — skipping global package installs"
else
# Pi (coding agent) # Pi (coding agent)
if has pi; then if has pi; then
skip "pi (@mariozechner/pi-coding-agent)" skip "pi (@mariozechner/pi-coding-agent)"
else else
npm install -g @mariozechner/pi-coding-agent echo " Installing pi..."
sudo npm install -g @mariozechner/pi-coding-agent
if has pi; then ok "pi installed"; else warn "pi install failed"; fi if has pi; then ok "pi installed"; else warn "pi install failed"; fi
fi fi
# Fix ~/.pi ownership (previous installs via sudo may have created root-owned dirs) # Validate Pi works
if [ -d "$HOME/.pi" ]; then if has pi; then
if find "$HOME/.pi" -not -user "$USER" -print -quit 2>/dev/null | grep -q .; then if pi --list-models > /dev/null 2>&1; then
sudo chown -R "$USER:$(id -gn)" "$HOME/.pi" ok "pi validated (--list-models works)"
ok "Fixed ~/.pi ownership" else
warn "pi installed but --list-models failed — check API keys in ~/.pi/agent/auth.json"
fi fi
fi fi
# Patch Pi compaction bug: calculateContextTokens crashes when usage is undefined # Fix ~/.pi ownership if needed (in case of mixed user/sudo installs)
PI_COMPACTION="$(npm root -g 2>/dev/null)/@mariozechner/pi-coding-agent/dist/core/compaction/compaction.js" if [ -d "$HOME/.pi" ]; then
if [ -f "$PI_COMPACTION" ]; then if find "$HOME/.pi" -not -user "$USER" -print -quit 2>/dev/null | grep -q .; then
if grep -q "if (!usage) return 0;" "$PI_COMPACTION" 2>/dev/null; then sudo chown -R "$USER:$(id -gn)" "$HOME/.pi"
skip "pi compaction patch (already applied)" ok "Fixed ~/.pi ownership to $USER"
else
sed -i '/^export function calculateContextTokens(usage) {$/a\ if (!usage) return 0;' "$PI_COMPACTION"
ok "Applied pi compaction bug patch"
fi fi
fi fi
@@ -593,16 +600,16 @@ if has npm; then
if has claude; then if has claude; then
skip "claude (@anthropic-ai/claude-code)" skip "claude (@anthropic-ai/claude-code)"
else else
npm install -g @anthropic-ai/claude-code echo " Installing claude-code..."
sudo npm install -g @anthropic-ai/claude-code
if has claude; then ok "claude-code installed"; else warn "claude-code install failed"; fi if has claude; then ok "claude-code installed"; else warn "claude-code install failed"; fi
fi fi
# pm2 (process manager) # pm2 (process manager) — now optional since we use systemd services
if has pm2; then if has pm2; then
skip "pm2" skip "pm2 (optional, using systemd services instead)"
else else
npm install -g pm2 warn "pm2 optional (Officer uses systemd services for main app and sidecar)"
if has pm2; then ok "pm2 installed"; else warn "pm2 install failed"; fi
fi fi
else else
warn "npm not found — skipping global package installs" warn "npm not found — skipping global package installs"