setup: Ubuntu GNOME-on-Xorg desktop + setup.sh hardening
setup-desktop.sh now installs ubuntu-desktop + gdm3 + x11vnc and forces the Xorg session (WaylandEnable=false) with auto-login — x11vnc can only mirror an Xorg :0, not Wayland. vnc-manager.ts resolves the X authority from the GDM per-session path (/run/user/<uid>/gdm/Xauthority) with a ~/.Xauthority fallback. setup.sh fixes: - desktop step gates on `dpkg -s ubuntu-desktop` (was the decommissioned officer-vnc service, which never matched so setup-desktop re-ran every time) - remove Pi (install, --list-models validation, verification check) - export GOPATH before the cliamp build so `go install` lands where it's checked even when Go was already present this run - write PUBLIC_BUILD_ENV=production and quote all .env values - guard the interactive .env block behind a TTY check so non-interactive runs skip cleanly instead of aborting on read EOF under set -e - restart systemd-logind only when a key actually changed - sed prefix-strip instead of `tr -d` (which deletes characters, not a prefix) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,205 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# One-time migration: provision Linux users for existing database members.
|
|
||||||
# Reads users from PostgreSQL, skips Super Admins (they use the service account),
|
|
||||||
# and creates Linux users + seeds shell configs for everyone else.
|
|
||||||
#
|
|
||||||
# Usage: bash scripts/provision-existing-users.sh
|
|
||||||
# Requires: sudoers entry from setup.sh, POSTGRES_URL in .env
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
||||||
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
|
||||||
fail() { echo -e " ${RED}✗${NC} $1"; }
|
|
||||||
skip() { echo -e " - $1 (skipped)"; }
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
||||||
ENV_FILE="$PROJECT_DIR/.env"
|
|
||||||
TEMPLATE_DIR="$PROJECT_DIR/src/servers/api/terminal/templates"
|
|
||||||
|
|
||||||
# Load .env
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
|
||||||
fail ".env not found at $ENV_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
source <(grep -E '^[A-Z_]+=.*' "$ENV_FILE" | sed 's/^/export /')
|
|
||||||
|
|
||||||
# Resolve DATA_PATH
|
|
||||||
DATA_PATH="${DATA_PATH:-$PROJECT_DIR/data}"
|
|
||||||
|
|
||||||
# Parse POSTGRES_URL for psql
|
|
||||||
if [ -z "${POSTGRES_URL:-}" ]; then
|
|
||||||
fail "POSTGRES_URL not set in .env"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract components from postgresql://user:pass@host:port/dbname
|
|
||||||
PG_USER=$(echo "$POSTGRES_URL" | sed -n 's|.*://\([^:]*\):.*|\1|p')
|
|
||||||
PG_PASS=$(echo "$POSTGRES_URL" | sed -n 's|.*://[^:]*:\([^@]*\)@.*|\1|p')
|
|
||||||
PG_HOST=$(echo "$POSTGRES_URL" | sed -n 's|.*@\([^:]*\):.*|\1|p')
|
|
||||||
PG_PORT=$(echo "$POSTGRES_URL" | sed -n 's|.*:\([0-9]*\)/.*|\1|p')
|
|
||||||
PG_DB=$(echo "$POSTGRES_URL" | sed -n 's|.*/\([^?]*\).*|\1|p')
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "═══════════════════════════════════════════"
|
|
||||||
echo " Provision existing users"
|
|
||||||
echo "═══════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
echo "DATA_PATH: $DATA_PATH"
|
|
||||||
echo "Database: $PG_DB @ $PG_HOST:$PG_PORT"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Query non-Super Admin active users
|
|
||||||
USERS=$(PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -F '|' \
|
|
||||||
-c "SELECT email, COALESCE(username, '') FROM users WHERE role != 'Super Admin' AND status = 'Active';" 2>&1)
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
fail "Failed to query database: $USERS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$USERS" ]; then
|
|
||||||
echo "No non-admin active users found. Nothing to do."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "── Found users ──"
|
|
||||||
echo "$USERS" | while IFS='|' read -r email username; do
|
|
||||||
echo " $email (username: ${username:-<from email>})"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "── Provisioning ──"
|
|
||||||
while IFS='|' read -r email username; do
|
|
||||||
# Derive shell username (same logic as toShellUsername in data-path.ts)
|
|
||||||
if [ -n "$username" ]; then
|
|
||||||
shell_user=$(echo "$username" | sed 's/@.*$//' | sed 's/[^a-zA-Z0-9._-]/_/g' | tr '[:upper:]' '[:lower:]' | cut -c1-32)
|
|
||||||
else
|
|
||||||
shell_user=$(echo "$email" | sed 's/@.*$//' | sed 's/[^a-zA-Z0-9._-]/_/g' | tr '[:upper:]' '[:lower:]' | cut -c1-32)
|
|
||||||
fi
|
|
||||||
|
|
||||||
USER_ROOT="$DATA_PATH/$email"
|
|
||||||
HOME_DIR="$USER_ROOT/home"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo " [$email → $shell_user]"
|
|
||||||
|
|
||||||
# Ensure data dirs exist (sudo in case dir is owned by a previous provisioning run)
|
|
||||||
sudo mkdir -p "$USER_ROOT" "$HOME_DIR"
|
|
||||||
|
|
||||||
# Create Linux user if needed
|
|
||||||
if id "$shell_user" &>/dev/null; then
|
|
||||||
skip "Linux user $shell_user already exists"
|
|
||||||
else
|
|
||||||
if sudo useradd -d "$HOME_DIR" -s /bin/zsh -M "$shell_user"; then
|
|
||||||
ok "Created Linux user $shell_user"
|
|
||||||
else
|
|
||||||
fail "Failed to create Linux user $shell_user"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Seed shell configs (only if not already present)
|
|
||||||
if [ ! -f "$HOME_DIR/.zshenv" ] && [ -f "$TEMPLATE_DIR/.zshenv" ]; then
|
|
||||||
sudo cp "$TEMPLATE_DIR/.zshenv" "$HOME_DIR/.zshenv"
|
|
||||||
ok "Seeded .zshenv"
|
|
||||||
else
|
|
||||||
skip ".zshenv"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$HOME_DIR/.zshrc" ] && [ -f "$TEMPLATE_DIR/.zshrc" ]; then
|
|
||||||
sudo cp "$TEMPLATE_DIR/.zshrc" "$HOME_DIR/.zshrc"
|
|
||||||
ok "Seeded .zshrc"
|
|
||||||
else
|
|
||||||
skip ".zshrc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$HOME_DIR/.tmux.conf" ] && [ -f "$TEMPLATE_DIR/.tmux.conf" ]; then
|
|
||||||
sudo cp "$TEMPLATE_DIR/.tmux.conf" "$HOME_DIR/.tmux.conf"
|
|
||||||
ok "Seeded .tmux.conf"
|
|
||||||
else
|
|
||||||
skip ".tmux.conf"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo mkdir -p "$HOME_DIR/.config"
|
|
||||||
if [ ! -f "$HOME_DIR/.config/starship-officer.toml" ] && [ -f "$TEMPLATE_DIR/starship-officer.toml" ]; then
|
|
||||||
sudo cp "$TEMPLATE_DIR/starship-officer.toml" "$HOME_DIR/.config/starship-officer.toml"
|
|
||||||
ok "Seeded starship config"
|
|
||||||
else
|
|
||||||
skip "starship config"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Oh My Zsh
|
|
||||||
if [ ! -d "$HOME_DIR/.oh-my-zsh" ]; then
|
|
||||||
if [ -d "$HOME/.oh-my-zsh" ]; then
|
|
||||||
sudo cp -r "$HOME/.oh-my-zsh" "$HOME_DIR/.oh-my-zsh"
|
|
||||||
ok "Copied oh-my-zsh from host"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
skip "oh-my-zsh"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# LazyVim
|
|
||||||
sudo mkdir -p "$HOME_DIR/.config"
|
|
||||||
if [ ! -d "$HOME_DIR/.config/nvim" ]; then
|
|
||||||
if [ -d "$HOME/.config/nvim" ]; then
|
|
||||||
sudo cp -r "$HOME/.config/nvim" "$HOME_DIR/.config/nvim"
|
|
||||||
ok "Copied nvim config from host"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
skip "nvim config"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure dirs
|
|
||||||
sudo mkdir -p "$HOME_DIR/.local/bin"
|
|
||||||
sudo mkdir -p "$HOME_DIR/.pi/agent/sessions"
|
|
||||||
|
|
||||||
# VNC environment
|
|
||||||
VNC_DIR="$HOME_DIR/.vnc"
|
|
||||||
sudo mkdir -p "$VNC_DIR"
|
|
||||||
if [ ! -f "$VNC_DIR/passwd" ]; then
|
|
||||||
VNC_PASS=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 8)
|
|
||||||
echo -n "$VNC_PASS" | sudo tee "$VNC_DIR/password" > /dev/null
|
|
||||||
echo -n "$VNC_PASS" | vncpasswd -f | sudo tee "$VNC_DIR/passwd" > /dev/null
|
|
||||||
sudo tee "$VNC_DIR/xstartup" > /dev/null << 'XSTARTUP'
|
|
||||||
#!/bin/sh
|
|
||||||
unset SESSION_MANAGER
|
|
||||||
unset DBUS_SESSION_BUS_ADDRESS
|
|
||||||
eval $(dbus-launch --sh-syntax)
|
|
||||||
export DBUS_SESSION_BUS_ADDRESS
|
|
||||||
exec startxfce4
|
|
||||||
XSTARTUP
|
|
||||||
sudo chmod +x "$VNC_DIR/xstartup"
|
|
||||||
sudo chmod 600 "$VNC_DIR/passwd"
|
|
||||||
sudo chmod 600 "$VNC_DIR/password"
|
|
||||||
ok "Provisioned VNC environment"
|
|
||||||
else
|
|
||||||
skip "VNC environment"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set ownership and permissions last
|
|
||||||
# chmod 770 so only owner and group can access (service user is added to group above)
|
|
||||||
sudo chown -R "$shell_user:$shell_user" "$USER_ROOT"
|
|
||||||
sudo chmod -R 770 "$USER_ROOT"
|
|
||||||
|
|
||||||
# Add service user to this user's group so server jobs can access user data
|
|
||||||
SERVICE_USER="${SUDO_USER:-$(whoami)}"
|
|
||||||
if [ "$SERVICE_USER" != "$shell_user" ]; then
|
|
||||||
sudo usermod -aG "$shell_user" "$SERVICE_USER"
|
|
||||||
ok "Added $SERVICE_USER to group $shell_user"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ok "Provisioning complete"
|
|
||||||
done <<< "$USERS"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "═══════════════════════════════════════════"
|
|
||||||
echo " Done!"
|
|
||||||
echo "═══════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
+54
-18
@@ -1,24 +1,63 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Officer Remote Desktop Setup — System packages only.
|
# Officer Remote Desktop Setup — Ubuntu GNOME desktop on Xorg, mirrored over VNC.
|
||||||
# Per-user VNC config is handled by user provisioning (provision.ts).
|
#
|
||||||
|
# The platform mirrors the single physical display :0 with x11vnc (see vnc-manager.ts). x11vnc can only
|
||||||
|
# capture an Xorg server, NOT a Wayland compositor — so we install the full GNOME desktop but force GDM
|
||||||
|
# onto the Xorg session (WaylandEnable=false). Auto-login is enabled so a user session owns :0 for the
|
||||||
|
# mirror to attach to. Switching the display manager takes effect on the next reboot.
|
||||||
# Usage: ./scripts/setup-desktop.sh
|
# Usage: ./scripts/setup-desktop.sh
|
||||||
|
|
||||||
echo "=== Officer Remote Desktop Setup ==="
|
echo "=== Officer Remote Desktop Setup (Ubuntu GNOME on Xorg) ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# --- Step 1: Install system packages ---
|
DESKTOP_USER="$(whoami)"
|
||||||
echo "[1/4] Installing system packages..."
|
|
||||||
|
# --- Step 1: Install GNOME desktop + GDM + x11vnc ---
|
||||||
|
echo "[1/5] Installing ubuntu-desktop, GDM, x11vnc..."
|
||||||
sudo apt update -qq
|
sudo apt update -qq
|
||||||
sudo DEBIAN_FRONTEND=noninteractive apt install -y -qq \
|
sudo DEBIAN_FRONTEND=noninteractive apt install -y -qq \
|
||||||
xfce4 xfce4-goodies \
|
ubuntu-desktop \
|
||||||
tigervnc-standalone-server tigervnc-common \
|
gdm3 \
|
||||||
|
x11vnc \
|
||||||
dbus-x11
|
dbus-x11
|
||||||
echo " Done."
|
echo " Done."
|
||||||
|
|
||||||
# --- Step 2: Install Brave browser (native .deb, not snap) ---
|
# --- Step 2: Force GDM onto Xorg + enable auto-login (x11vnc cannot mirror Wayland) ---
|
||||||
echo "[2/4] Installing Brave browser..."
|
echo "[2/5] Forcing Xorg session and auto-login in GDM..."
|
||||||
|
GDM_CONF=/etc/gdm3/custom.conf
|
||||||
|
sudo mkdir -p /etc/gdm3
|
||||||
|
[ -f "$GDM_CONF" ] || echo "[daemon]" | sudo tee "$GDM_CONF" > /dev/null
|
||||||
|
# Ensure a [daemon] section exists to anchor the keys under.
|
||||||
|
sudo grep -qE '^\[daemon\]' "$GDM_CONF" || echo "[daemon]" | sudo tee -a "$GDM_CONF" > /dev/null
|
||||||
|
|
||||||
|
# Set key=value under [daemon]: rewrite an existing (possibly commented) line, else insert after [daemon].
|
||||||
|
set_gdm_key() {
|
||||||
|
local key="$1" val="$2"
|
||||||
|
if sudo grep -qE "^[[:space:]]*#?[[:space:]]*${key}=" "$GDM_CONF"; then
|
||||||
|
sudo sed -i "s|^[[:space:]]*#\?[[:space:]]*${key}=.*|${key}=${val}|" "$GDM_CONF"
|
||||||
|
else
|
||||||
|
sudo sed -i "/^\[daemon\]/a ${key}=${val}" "$GDM_CONF"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
set_gdm_key WaylandEnable false
|
||||||
|
set_gdm_key AutomaticLoginEnable true
|
||||||
|
set_gdm_key AutomaticLogin "$DESKTOP_USER"
|
||||||
|
echo " Xorg forced (WaylandEnable=false), auto-login as $DESKTOP_USER."
|
||||||
|
|
||||||
|
# --- Step 3: Make GDM the default display manager ---
|
||||||
|
echo "[3/5] Setting GDM as the default display manager..."
|
||||||
|
echo "/usr/sbin/gdm3" | sudo tee /etc/X11/default-display-manager > /dev/null
|
||||||
|
sudo systemctl enable gdm3 >/dev/null 2>&1 || sudo systemctl enable gdm >/dev/null 2>&1 || true
|
||||||
|
sudo systemctl set-default graphical.target >/dev/null 2>&1 || true
|
||||||
|
# Disable any prior display manager (e.g. lightdm from an XFCE setup) so it doesn't fight GDM.
|
||||||
|
sudo systemctl disable lightdm >/dev/null 2>&1 || true
|
||||||
|
echo " Done (takes effect on next reboot)."
|
||||||
|
|
||||||
|
# --- Step 4: Install Brave browser (native .deb, not snap) ---
|
||||||
|
echo "[4/5] Installing Brave browser..."
|
||||||
if ! command -v brave-browser-stable > /dev/null 2>&1; then
|
if ! command -v brave-browser-stable > /dev/null 2>&1; then
|
||||||
sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg \
|
sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg \
|
||||||
https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg
|
https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg
|
||||||
@@ -32,24 +71,20 @@ if [ -f /opt/brave.com/brave/brave-browser ]; then
|
|||||||
sudo rm -f /usr/bin/brave-browser-stable
|
sudo rm -f /usr/bin/brave-browser-stable
|
||||||
sudo ln -s /opt/brave.com/brave/brave-browser /usr/bin/brave-browser-stable
|
sudo ln -s /opt/brave.com/brave/brave-browser /usr/bin/brave-browser-stable
|
||||||
fi
|
fi
|
||||||
# Tell Brave to use basic password store (no GNOME Keyring prompts)
|
# Tell Brave to use basic password store (no keyring prompts)
|
||||||
sudo mkdir -p /etc/brave
|
sudo mkdir -p /etc/brave
|
||||||
echo '--password-store=basic' | sudo tee /etc/brave/brave-flags.conf > /dev/null
|
echo '--password-store=basic' | sudo tee /etc/brave/brave-flags.conf > /dev/null
|
||||||
echo " Done."
|
echo " Done."
|
||||||
|
|
||||||
# --- Step 3: Remove GNOME Keyring (prevents password prompts on login) ---
|
# --- Step 5: Remove GNOME Keyring + set default browser (prevents password prompts on login) ---
|
||||||
echo "[3/4] Removing GNOME Keyring..."
|
echo "[5/5] Removing GNOME Keyring and setting default browser..."
|
||||||
sudo apt remove -y --purge gnome-keyring > /dev/null 2>&1 || true
|
sudo apt remove -y --purge gnome-keyring > /dev/null 2>&1 || true
|
||||||
rm -rf ~/.local/share/keyrings
|
rm -rf ~/.local/share/keyrings
|
||||||
echo " Done."
|
|
||||||
|
|
||||||
# --- Step 4: Set default browser ---
|
|
||||||
echo "[4/4] Setting default browser..."
|
|
||||||
if command -v brave-browser-stable > /dev/null 2>&1; then
|
if command -v brave-browser-stable > /dev/null 2>&1; then
|
||||||
sudo update-alternatives --set x-www-browser /opt/brave.com/brave/brave 2>/dev/null || true
|
sudo update-alternatives --set x-www-browser /opt/brave.com/brave/brave 2>/dev/null || true
|
||||||
echo " Brave set as default."
|
echo " Brave set as default."
|
||||||
else
|
else
|
||||||
echo " No supported browser found, skipping."
|
echo " No supported browser found, skipping default."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Cleanup old systemd service if it exists ---
|
# --- Cleanup old systemd service if it exists ---
|
||||||
@@ -64,4 +99,5 @@ if systemctl list-unit-files officer-vnc.service &>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Setup complete. Per-user VNC sessions are managed by the VNC sidecar."
|
echo "Setup complete. The screen mirror (x11vnc on :0) is managed by the VNC sidecar."
|
||||||
|
echo "REBOOT to switch into the GNOME-on-Xorg session with auto-login."
|
||||||
|
|||||||
+39
-34
@@ -299,8 +299,13 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure logind to ignore idle — patch individual keys, don't overwrite the file
|
# Configure logind to ignore idle — patch individual keys, don't overwrite the file
|
||||||
|
LOGIND_CHANGED=0
|
||||||
set_logind_key() {
|
set_logind_key() {
|
||||||
local key="$1" val="$2" file="/etc/systemd/logind.conf"
|
local key="$1" val="$2" file="/etc/systemd/logind.conf"
|
||||||
|
# Already set (uncommented) to the desired value → nothing to do.
|
||||||
|
if grep -qE "^${key}=${val}$" "$file" 2>/dev/null; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||||||
sudo sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
sudo sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
||||||
elif grep -qE "^#${key}=" "$file" 2>/dev/null; then
|
elif grep -qE "^#${key}=" "$file" 2>/dev/null; then
|
||||||
@@ -308,6 +313,7 @@ set_logind_key() {
|
|||||||
else
|
else
|
||||||
echo "${key}=${val}" | sudo tee -a "$file" > /dev/null
|
echo "${key}=${val}" | sudo tee -a "$file" > /dev/null
|
||||||
fi
|
fi
|
||||||
|
LOGIND_CHANGED=1
|
||||||
}
|
}
|
||||||
|
|
||||||
set_logind_key HandleLidSwitch ignore
|
set_logind_key HandleLidSwitch ignore
|
||||||
@@ -316,8 +322,13 @@ set_logind_key HandlePowerKey ignore
|
|||||||
set_logind_key IdleAction none
|
set_logind_key IdleAction none
|
||||||
set_logind_key RuntimeDirectorySize 10%
|
set_logind_key RuntimeDirectorySize 10%
|
||||||
|
|
||||||
|
# Only restart logind when something actually changed — a needless restart can disrupt live sessions.
|
||||||
|
if [ "$LOGIND_CHANGED" = "1" ]; then
|
||||||
sudo systemctl restart systemd-logind
|
sudo systemctl restart systemd-logind
|
||||||
ok "Configured logind to disable auto-suspend"
|
ok "Configured logind to disable auto-suspend"
|
||||||
|
else
|
||||||
|
skip "logind auto-suspend settings"
|
||||||
|
fi
|
||||||
|
|
||||||
# ─── 5. Node.js 22 (system-wide) ─────────────────────────────────────────────
|
# ─── 5. Node.js 22 (system-wide) ─────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
@@ -337,7 +348,7 @@ case $PM in
|
|||||||
apt)
|
apt)
|
||||||
NEED_INSTALL=0
|
NEED_INSTALL=0
|
||||||
if [ -f "$SYSTEM_NODE" ]; then
|
if [ -f "$SYSTEM_NODE" ]; then
|
||||||
SYS_MAJOR=$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | tr -d 'v')
|
SYS_MAJOR=$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')
|
||||||
if [ "$SYS_MAJOR" = "22" ]; then
|
if [ "$SYS_MAJOR" = "22" ]; then
|
||||||
skip "node v$("$SYSTEM_NODE" -v) (system-wide at $SYSTEM_NODE)"
|
skip "node v$("$SYSTEM_NODE" -v) (system-wide at $SYSTEM_NODE)"
|
||||||
else
|
else
|
||||||
@@ -354,7 +365,7 @@ case $PM in
|
|||||||
fi
|
fi
|
||||||
if [ "$NEED_INSTALL" = "1" ]; then
|
if [ "$NEED_INSTALL" = "1" ]; then
|
||||||
install_node22_apt
|
install_node22_apt
|
||||||
if [ -f "$SYSTEM_NODE" ] && [ "$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then
|
if [ -f "$SYSTEM_NODE" ] && [ "$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then
|
||||||
ok "node v$("$SYSTEM_NODE" -v) installed at $SYSTEM_NODE"
|
ok "node v$("$SYSTEM_NODE" -v) installed at $SYSTEM_NODE"
|
||||||
if has node && [ "$(command -v node)" != "$SYSTEM_NODE" ]; then
|
if has node && [ "$(command -v node)" != "$SYSTEM_NODE" ]; then
|
||||||
warn "Shell resolves 'node' to $(command -v node) — system node is at $SYSTEM_NODE"
|
warn "Shell resolves 'node' to $(command -v node) — system node is at $SYSTEM_NODE"
|
||||||
@@ -367,7 +378,7 @@ case $PM in
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
pacman)
|
pacman)
|
||||||
if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then
|
if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then
|
||||||
skip "node v$(node -v)"
|
skip "node v$(node -v)"
|
||||||
else
|
else
|
||||||
install_pkg nodejs npm
|
install_pkg nodejs npm
|
||||||
@@ -375,7 +386,7 @@ case $PM in
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
brew)
|
brew)
|
||||||
if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then
|
if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then
|
||||||
skip "node v$(node -v)"
|
skip "node v$(node -v)"
|
||||||
else
|
else
|
||||||
install_pkg node
|
install_pkg node
|
||||||
@@ -412,7 +423,7 @@ echo ""
|
|||||||
echo "── Go ──"
|
echo "── Go ──"
|
||||||
|
|
||||||
echo " Fetching latest Go version..."
|
echo " Fetching latest Go version..."
|
||||||
GOLANG_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | jq -r '.[0].version' | tr -d 'go')
|
GOLANG_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | jq -r '.[0].version' | sed 's/^go//')
|
||||||
if [ -z "$GOLANG_VERSION" ]; then
|
if [ -z "$GOLANG_VERSION" ]; then
|
||||||
warn "Could not fetch latest Go version — falling back to 1.23.6"
|
warn "Could not fetch latest Go version — falling back to 1.23.6"
|
||||||
GOLANG_VERSION=1.23.6
|
GOLANG_VERSION=1.23.6
|
||||||
@@ -445,7 +456,7 @@ install_go() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if has go; then
|
if has go; then
|
||||||
INSTALLED_GO=$(go version 2>/dev/null | awk '{print $3}' | tr -d 'go')
|
INSTALLED_GO=$(go version 2>/dev/null | awk '{print $3}' | sed 's/^go//')
|
||||||
if [ "$INSTALLED_GO" = "$GOLANG_VERSION" ]; then
|
if [ "$INSTALLED_GO" = "$GOLANG_VERSION" ]; then
|
||||||
skip "go $INSTALLED_GO"
|
skip "go $INSTALLED_GO"
|
||||||
else
|
else
|
||||||
@@ -528,7 +539,11 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "── cliamp ──"
|
echo "── cliamp ──"
|
||||||
|
|
||||||
GOPATH_BIN="${GOPATH:-$HOME/.local/go-path}/bin"
|
# Export GOPATH (not just PATH) so `go install` lands in GOPATH_BIN — even when Go was already present
|
||||||
|
# this run and install_go (which sets GOPATH) never ran. Otherwise go uses its default ~/go/bin and the
|
||||||
|
# check below wrongly reports a build failure.
|
||||||
|
export GOPATH="${GOPATH:-$HOME/.local/go-path}"
|
||||||
|
GOPATH_BIN="$GOPATH/bin"
|
||||||
export PATH="$GOPATH_BIN:$PATH"
|
export PATH="$GOPATH_BIN:$PATH"
|
||||||
|
|
||||||
if has cliamp; then
|
if has cliamp; then
|
||||||
@@ -619,7 +634,7 @@ else
|
|||||||
case $PM in
|
case $PM in
|
||||||
apt)
|
apt)
|
||||||
echo " Fetching latest eza version..."
|
echo " Fetching latest eza version..."
|
||||||
EZA_VERSION=$(curl -fsSL "https://api.github.com/repos/eza-community/eza/releases/latest" | jq -r '.tag_name' | tr -d 'v')
|
EZA_VERSION=$(curl -fsSL "https://api.github.com/repos/eza-community/eza/releases/latest" | jq -r '.tag_name' | sed 's/^v//')
|
||||||
if [ -z "$EZA_VERSION" ]; then warn "Could not fetch eza version — skipping"; else
|
if [ -z "$EZA_VERSION" ]; then warn "Could not fetch eza version — skipping"; else
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
case $ARCH in
|
case $ARCH in
|
||||||
@@ -647,7 +662,7 @@ else
|
|||||||
case $PM in
|
case $PM in
|
||||||
apt)
|
apt)
|
||||||
echo " Fetching latest lazygit version..."
|
echo " Fetching latest lazygit version..."
|
||||||
LAZYGIT_VERSION=$(curl -fsSL "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | jq -r '.tag_name' | tr -d 'v')
|
LAZYGIT_VERSION=$(curl -fsSL "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | jq -r '.tag_name' | sed 's/^v//')
|
||||||
if [ -z "$LAZYGIT_VERSION" ]; then warn "Could not fetch lazygit version — skipping"; else
|
if [ -z "$LAZYGIT_VERSION" ]; then warn "Could not fetch lazygit version — skipping"; else
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
case $ARCH in
|
case $ARCH in
|
||||||
@@ -710,24 +725,6 @@ else
|
|||||||
npm config set prefix "$HOME/.local"
|
npm config set prefix "$HOME/.local"
|
||||||
ok "npm prefix set to $HOME/.local"
|
ok "npm prefix set to $HOME/.local"
|
||||||
|
|
||||||
# Pi (coding agent)
|
|
||||||
if has pi; then
|
|
||||||
skip "pi (@mariozechner/pi-coding-agent)"
|
|
||||||
else
|
|
||||||
echo " Installing pi..."
|
|
||||||
npm install -g @mariozechner/pi-coding-agent
|
|
||||||
if has pi; then ok "pi installed"; else warn "pi install failed"; fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate Pi works
|
|
||||||
if has pi; then
|
|
||||||
if pi --list-models > /dev/null 2>&1; then
|
|
||||||
ok "pi validated (--list-models works)"
|
|
||||||
else
|
|
||||||
warn "pi installed but --list-models failed — check API keys in ~/.pi/agent/auth.json"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Claude Code (uses Anthropic's own installer for auto-update support)
|
# Claude Code (uses Anthropic's own installer for auto-update support)
|
||||||
if has claude; then
|
if has claude; then
|
||||||
skip "claude (claude-code)"
|
skip "claude (claude-code)"
|
||||||
@@ -765,7 +762,15 @@ echo "── Environment (.env) ──"
|
|||||||
|
|
||||||
GENERATE_ENV=true
|
GENERATE_ENV=true
|
||||||
|
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
# Non-interactive shell: the prompts below would hit EOF and abort the whole script under `set -e`.
|
||||||
|
GENERATE_ENV=false
|
||||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||||
|
skip ".env (kept existing — non-interactive shell)"
|
||||||
|
else
|
||||||
|
warn "No .env and not a terminal — re-run setup.sh interactively to generate it"
|
||||||
|
fi
|
||||||
|
elif [ -f "$PROJECT_DIR/.env" ]; then
|
||||||
echo -n " .env already exists. Regenerate? (y/n) [n]: "
|
echo -n " .env already exists. Regenerate? (y/n) [n]: "
|
||||||
read -r REGEN
|
read -r REGEN
|
||||||
if [[ "$REGEN" != "y" && "$REGEN" != "Y" ]]; then
|
if [[ "$REGEN" != "y" && "$REGEN" != "Y" ]]; then
|
||||||
@@ -820,12 +825,13 @@ if [ "$GENERATE_ENV" = true ]; then
|
|||||||
|
|
||||||
# Write .env
|
# Write .env
|
||||||
cat > "$PROJECT_DIR/.env" <<ENVFILE
|
cat > "$PROJECT_DIR/.env" <<ENVFILE
|
||||||
PORT=$ENV_PORT
|
PORT="$ENV_PORT"
|
||||||
JWT_SECRET="$JWT_SECRET"
|
JWT_SECRET="$JWT_SECRET"
|
||||||
MAIL_TRANSPORT="$ENV_MAIL_TRANSPORT"
|
MAIL_TRANSPORT="$ENV_MAIL_TRANSPORT"
|
||||||
PUBLIC_URL=$ENV_PUBLIC_URL
|
PUBLIC_URL="$ENV_PUBLIC_URL"
|
||||||
DATA_PATH=$ENV_DATA_PATH
|
PUBLIC_BUILD_ENV="production"
|
||||||
HOME_DIR=$HOME
|
DATA_PATH="$ENV_DATA_PATH"
|
||||||
|
HOME_DIR="$HOME"
|
||||||
POSTGRES_URL="$POSTGRES_URL"
|
POSTGRES_URL="$POSTGRES_URL"
|
||||||
DISCORD_BUG_REPORT_WEBHOOK="$ENV_DISCORD_WEBHOOK"
|
DISCORD_BUG_REPORT_WEBHOOK="$ENV_DISCORD_WEBHOOK"
|
||||||
ENVFILE
|
ENVFILE
|
||||||
@@ -837,8 +843,8 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "── Remote Desktop (XFCE + VNC) ──"
|
echo "── Remote Desktop (XFCE + VNC) ──"
|
||||||
|
|
||||||
if systemctl is-active --quiet officer-vnc 2>/dev/null; then
|
if dpkg -s ubuntu-desktop &>/dev/null 2>&1; then
|
||||||
skip "remote desktop (officer-vnc service already running)"
|
skip "remote desktop (ubuntu-desktop already installed)"
|
||||||
else
|
else
|
||||||
case $PM in
|
case $PM in
|
||||||
apt)
|
apt)
|
||||||
@@ -901,7 +907,6 @@ check cliamp
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "AI agents:"
|
echo "AI agents:"
|
||||||
check pi
|
|
||||||
check claude
|
check claude
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -11,9 +11,19 @@ const MIRROR_DISPLAY_NUM = 0;
|
|||||||
const MIRROR_PORT = 5900;
|
const MIRROR_PORT = 5900;
|
||||||
const READY_TIMEOUT_MS = 5000;
|
const READY_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
// x11vnc reads :0's cookie from the logged-in user's own .Xauthority, so no root is needed.
|
// x11vnc reads :0's cookie from the logged-in user's X authority, so no root is needed. Where that
|
||||||
// While the greeter owns :0 the cookie lives in lightdm's file instead and mirroring fails.
|
// cookie lives depends on the display manager: GDM (Ubuntu GNOME on Xorg) keeps it in the per-session
|
||||||
const XAUTHORITY = join(process.env.HOME ?? '', '.Xauthority');
|
// dir /run/user/<uid>/gdm/Xauthority; LightDM (or a manual startx) uses the classic ~/.Xauthority. Try
|
||||||
|
// the GDM path first, then fall back. While the greeter owns :0 the user's cookie doesn't exist yet and
|
||||||
|
// mirroring fails until someone is logged in.
|
||||||
|
function resolveXauthority(): string | null {
|
||||||
|
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
||||||
|
const candidates = [
|
||||||
|
uid != null ? `/run/user/${uid}/gdm/Xauthority` : null,
|
||||||
|
join(process.env.HOME ?? '', '.Xauthority'),
|
||||||
|
].filter((p): p is string => Boolean(p));
|
||||||
|
return candidates.find((p) => existsSync(p)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
type MirrorSession = {
|
type MirrorSession = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -99,8 +109,9 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb
|
|||||||
const homeDir = getHomeDirForRole(params.email, params.role);
|
const homeDir = getHomeDirForRole(params.email, params.role);
|
||||||
const passwdFile = await ensureVncPassword(homeDir);
|
const passwdFile = await ensureVncPassword(homeDir);
|
||||||
|
|
||||||
if (!existsSync(XAUTHORITY)) {
|
const xauthority = resolveXauthority();
|
||||||
throw new Error(`No X authority at ${XAUTHORITY} — nobody is logged in on ${MIRROR_DISPLAY}`);
|
if (!xauthority) {
|
||||||
|
throw new Error(`No X authority found (GDM or ~/.Xauthority) — nobody is logged in on ${MIRROR_DISPLAY}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
@@ -109,7 +120,7 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb
|
|||||||
'-display',
|
'-display',
|
||||||
MIRROR_DISPLAY,
|
MIRROR_DISPLAY,
|
||||||
'-auth',
|
'-auth',
|
||||||
XAUTHORITY,
|
xauthority,
|
||||||
'-rfbport',
|
'-rfbport',
|
||||||
String(MIRROR_PORT),
|
String(MIRROR_PORT),
|
||||||
'-rfbauth',
|
'-rfbauth',
|
||||||
|
|||||||
Reference in New Issue
Block a user