Add Claude Code LXC deployer script for Proxmox
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+982
@@ -0,0 +1,982 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Claude Code LXC Deployer for Proxmox — angepasste Version
|
||||
# Creates a fully provisioned Ubuntu 26.04 LXC container ready for Claude Code
|
||||
#
|
||||
# Änderungen gegenüber dem Original (serversathome):
|
||||
# - FIX: Template-Erkennung nahm mit awk '{print $NF}' die letzte Spalte
|
||||
# (Architektur) statt des Template-Namens -> jetzt $2
|
||||
# - FIX: grep-Pipeline unter `set -e` abgesichert (|| true), sonst bricht
|
||||
# das Script kommentarlos ab, wenn kein Template gefunden wird
|
||||
# - Storage-Default: data (statt truenas-lvm)
|
||||
# - Zeitzone: Europe/Berlin (statt America/New_York) — auch Cron & CLAUDE.md
|
||||
# - Zusätzlich de_DE.UTF-8 Locale generiert
|
||||
# - pnpm global installiert (zusätzlich zu npm)
|
||||
# - Optionale Git-Identität (user.name/user.email) wird abgefragt und gesetzt
|
||||
#
|
||||
# Run on your Proxmox host: bash agentic.sh
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors & Helpers ────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
|
||||
header() {
|
||||
echo ""
|
||||
echo -e "${BOLD}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}║ Claude Code LXC Deployer (Proxmox) ║${NC}"
|
||||
echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Pre-flight checks ──────────────────────────────────────────────────────
|
||||
preflight() {
|
||||
[[ $(id -u) -eq 0 ]] || error "This script must be run as root on the Proxmox host."
|
||||
command -v pct &>/dev/null || error "pct not found. Are you running this on a Proxmox host?"
|
||||
command -v pveam &>/dev/null || error "pveam not found. Are you running this on a Proxmox host?"
|
||||
}
|
||||
|
||||
# ── Template Resolution ─────────────────────────────────────────────────────
|
||||
resolve_template() {
|
||||
info "Resolving latest Ubuntu 26.04 LXC template from catalog..."
|
||||
pveam update >/dev/null 2>&1 || true
|
||||
local found
|
||||
# FIX: $2 statt $NF (pveam available liefert: Sektion, Name, Arch) und
|
||||
# || true, damit ein leerer grep-Treffer unter `set -e` nicht das ganze
|
||||
# Script beendet, bevor der Fallback greifen kann.
|
||||
found=$(pveam available --section system 2>/dev/null \
|
||||
| awk '{print $2}' \
|
||||
| grep -E '^ubuntu-26\.04-standard' \
|
||||
| sort -V | tail -n1 || true)
|
||||
if [[ -n "$found" ]]; then
|
||||
TEMPLATE="$found"
|
||||
success "Using template: $TEMPLATE"
|
||||
else
|
||||
TEMPLATE="ubuntu-26.04-standard_26.04-1_amd64.tar.zst"
|
||||
warn "No 26.04 template found in catalog; using fallback name: $TEMPLATE"
|
||||
warn "Verify with: pveam available --section system | grep ubuntu-26.04"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Configuration ───────────────────────────────────────────────────────────
|
||||
get_config() {
|
||||
# Find next available CT ID
|
||||
local next_id
|
||||
next_id=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
|
||||
|
||||
# Resolve newest Ubuntu 26.04 template (sets $TEMPLATE)
|
||||
resolve_template
|
||||
|
||||
echo -e "${BOLD}Container Configuration${NC}"
|
||||
echo "─────────────────────────────────────────────────"
|
||||
|
||||
read -rp "Container ID [$next_id]: " CT_ID
|
||||
CT_ID="${CT_ID:-$next_id}"
|
||||
[[ "$CT_ID" =~ ^[0-9]+$ ]] || error "Container ID must be a number."
|
||||
pct status "$CT_ID" &>/dev/null && error "Container ID $CT_ID already exists."
|
||||
|
||||
read -rp "Hostname [claude-code]: " CT_HOSTNAME
|
||||
CT_HOSTNAME="${CT_HOSTNAME:-claude-code}"
|
||||
|
||||
read -rsp "Root password: " CT_PASSWORD
|
||||
echo ""
|
||||
[[ -n "$CT_PASSWORD" ]] || error "Password cannot be empty."
|
||||
|
||||
read -rp "CPU cores [4]: " CT_CORES
|
||||
CT_CORES="${CT_CORES:-4}"
|
||||
|
||||
read -rp "RAM in MB [10240]: " CT_RAM
|
||||
CT_RAM="${CT_RAM:-10240}"
|
||||
|
||||
read -rp "Swap in MB [2048]: " CT_SWAP
|
||||
CT_SWAP="${CT_SWAP:-2048}"
|
||||
|
||||
read -rp "Disk size in GB [30]: " CT_DISK
|
||||
CT_DISK="${CT_DISK:-30}"
|
||||
|
||||
read -rp "Storage [data]: " CT_STORAGE
|
||||
CT_STORAGE="${CT_STORAGE:-data}"
|
||||
|
||||
# Network - default DHCP
|
||||
read -rp "IP address (DHCP or x.x.x.x/xx) [dhcp]: " CT_IP
|
||||
CT_IP="${CT_IP:-dhcp}"
|
||||
if [[ "$CT_IP" != "dhcp" ]]; then
|
||||
read -rp "Gateway: " CT_GW
|
||||
[[ -n "$CT_GW" ]] || error "Gateway is required for static IP."
|
||||
fi
|
||||
|
||||
read -rp "DNS server [1.1.1.1]: " CT_DNS
|
||||
CT_DNS="${CT_DNS:-1.1.1.1}"
|
||||
|
||||
# SSH key (optional)
|
||||
read -rp "Path to SSH public key (optional, press Enter to skip): " CT_SSH_KEY
|
||||
|
||||
# Git identity (optional) — wird im Container als git config --global gesetzt
|
||||
read -rp "Git user.name (optional, press Enter to skip): " GIT_NAME
|
||||
read -rp "Git user.email (optional, press Enter to skip): " GIT_EMAIL
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Summary${NC}"
|
||||
echo "─────────────────────────────────────────────────"
|
||||
echo " CT ID: $CT_ID"
|
||||
echo " Hostname: $CT_HOSTNAME"
|
||||
echo " Template: $TEMPLATE"
|
||||
echo " CPU: $CT_CORES cores"
|
||||
echo " RAM: $CT_RAM MB ($(( CT_RAM / 1024 )) GB)"
|
||||
echo " Swap: $CT_SWAP MB"
|
||||
echo " Disk: ${CT_DISK}G on $CT_STORAGE"
|
||||
echo " Network: $CT_IP"
|
||||
echo " DNS: $CT_DNS"
|
||||
[[ -n "${GIT_NAME:-}" ]] && echo " Git: $GIT_NAME <${GIT_EMAIL:-}>"
|
||||
echo "─────────────────────────────────────────────────"
|
||||
echo ""
|
||||
read -rp "Proceed? (y/N): " confirm
|
||||
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
||||
}
|
||||
|
||||
# ── Download Ubuntu 26.04 Template ─────────────────────────────────────────
|
||||
get_template() {
|
||||
info "Checking for template: $TEMPLATE"
|
||||
if ! pveam list local 2>/dev/null | grep -q "$TEMPLATE"; then
|
||||
info "Downloading $TEMPLATE ..."
|
||||
pveam download local "$TEMPLATE" || error "Failed to download template. Run 'pveam update' and try again."
|
||||
else
|
||||
success "Template already downloaded: $TEMPLATE"
|
||||
fi
|
||||
TEMPLATE_PATH="local:vztmpl/$TEMPLATE"
|
||||
}
|
||||
|
||||
# ── Create Container ───────────────────────────────────────────────────────
|
||||
create_container() {
|
||||
info "Creating LXC container $CT_ID..."
|
||||
|
||||
# Build network string
|
||||
local net_str="name=eth0,bridge=vmbr0"
|
||||
if [[ "$CT_IP" == "dhcp" ]]; then
|
||||
net_str+=",ip=dhcp"
|
||||
else
|
||||
net_str+=",ip=$CT_IP,gw=$CT_GW"
|
||||
fi
|
||||
|
||||
# Build pct create command
|
||||
local cmd=(
|
||||
pct create "$CT_ID" "$TEMPLATE_PATH"
|
||||
--hostname "$CT_HOSTNAME"
|
||||
--password "$CT_PASSWORD"
|
||||
--cores "$CT_CORES"
|
||||
--memory "$CT_RAM"
|
||||
--swap "$CT_SWAP"
|
||||
--rootfs "$CT_STORAGE:$CT_DISK"
|
||||
--net0 "$net_str"
|
||||
--nameserver "$CT_DNS"
|
||||
--ostype ubuntu
|
||||
--unprivileged 0
|
||||
--features nesting=1,keyctl=1
|
||||
--onboot 1
|
||||
--start 0
|
||||
)
|
||||
|
||||
# Add SSH key if provided
|
||||
if [[ -n "${CT_SSH_KEY:-}" && -f "$CT_SSH_KEY" ]]; then
|
||||
cmd+=(--ssh-public-keys "$CT_SSH_KEY")
|
||||
fi
|
||||
|
||||
"${cmd[@]}"
|
||||
success "Container $CT_ID created."
|
||||
|
||||
# Disable AppArmor for Docker-in-LXC compatibility
|
||||
info "Setting AppArmor profile to unconfined (required for Docker)..."
|
||||
echo "lxc.apparmor.profile: unconfined" >> "/etc/pve/lxc/${CT_ID}.conf"
|
||||
}
|
||||
|
||||
# ── Start & Wait for Network ──────────────────────────────────────────────
|
||||
start_container() {
|
||||
info "Starting container $CT_ID..."
|
||||
pct start "$CT_ID"
|
||||
sleep 3
|
||||
|
||||
info "Waiting for network..."
|
||||
local attempts=0
|
||||
while ! pct exec "$CT_ID" -- ping -c1 -W2 1.1.1.1 &>/dev/null; do
|
||||
((attempts++))
|
||||
[[ $attempts -lt 30 ]] || error "Container failed to get network after 60s."
|
||||
sleep 2
|
||||
done
|
||||
success "Container is online."
|
||||
}
|
||||
|
||||
# ── Provision Container ───────────────────────────────────────────────────
|
||||
provision_container() {
|
||||
info "Provisioning container (this takes a few minutes)..."
|
||||
|
||||
# Write provision script to host, then push into container
|
||||
cat > /tmp/provision-${CT_ID}.sh << 'PROVISION_EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo ">>> Setting timezone to Europe/Berlin..."
|
||||
ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime
|
||||
echo "Europe/Berlin" > /etc/timezone
|
||||
dpkg-reconfigure -f noninteractive tzdata
|
||||
|
||||
echo ">>> Generating locales..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq locales
|
||||
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen
|
||||
sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen
|
||||
locale-gen en_US.UTF-8 de_DE.UTF-8 > /dev/null 2>&1
|
||||
update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
echo ">>> Updating system..."
|
||||
apt-get upgrade -y -qq
|
||||
|
||||
echo ">>> Installing core packages..."
|
||||
apt-get install -y -qq \
|
||||
git curl wget unzip zip \
|
||||
ca-certificates gnupg lsb-release apt-transport-https software-properties-common \
|
||||
bash-completion locales \
|
||||
htop nano vim tmux screen \
|
||||
jq yq tree \
|
||||
net-tools iproute2 iputils-ping dnsutils \
|
||||
openssh-server \
|
||||
cron logrotate
|
||||
|
||||
echo ">>> Installing build tools & dev libraries..."
|
||||
apt-get install -y -qq \
|
||||
build-essential make cmake pkg-config autoconf automake libtool \
|
||||
python3 python3-pip python3-venv python3-dev \
|
||||
libssl-dev libffi-dev libsqlite3-dev zlib1g-dev \
|
||||
libreadline-dev libbz2-dev libncurses-dev liblzma-dev libxml2-dev libxslt-dev
|
||||
|
||||
echo ">>> Installing search & productivity tools..."
|
||||
apt-get install -y -qq \
|
||||
ripgrep fd-find fzf bat \
|
||||
rsync \
|
||||
sqlite3
|
||||
|
||||
echo ">>> Installing database clients..."
|
||||
apt-get install -y -qq \
|
||||
postgresql-client redis-tools
|
||||
|
||||
echo ">>> Installing Node.js (latest LTS via NodeSource)..."
|
||||
# setup_lts.x tracks the current LTS line, so fresh deploys get the newest
|
||||
# *safe* Node automatically (Node 24 today; it rolls to the next LTS on its own
|
||||
# when one lands — no script edit needed). We deliberately track LTS, not the
|
||||
# odd/Current line (short-lived, not recommended here). apt upgrades keep it
|
||||
# patched within the line; a major-version jump stays a deliberate rebuild
|
||||
# rather than an unattended 4 a.m. surprise.
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
apt-get install -y -qq nodejs
|
||||
# Quiet flags reused for every provisioning npm install: drop the funding/audit
|
||||
# lines and the deprecation warnings that come from these packages' upstream
|
||||
# transitive deps (not fixable from here). Scoped to provisioning only — your
|
||||
# own installs under /project behave normally.
|
||||
NPM_QUIET=(--no-fund --no-audit --loglevel=error)
|
||||
# Take npm to its own latest release — the bundled npm lags behind and otherwise
|
||||
# prints an "update available" notice on every install.
|
||||
npm install -g "${NPM_QUIET[@]}" npm@latest || echo " [WARN] npm self-update failed; using bundled npm"
|
||||
echo " Node.js $(node --version) / npm $(npm --version)"
|
||||
# npm 12+ blocks dependencies' install scripts behind an allowlist (separate from
|
||||
# ignore-scripts), so native modules like CloudCLI's better-sqlite3/node-pty/
|
||||
# bcrypt never compile and the service crash-loops with no bind on :3001. We just
|
||||
# took npm to latest (>=12), so set the allowlist NOW, before any native install
|
||||
# below (CloudCLI, Playwright, the language servers). It persists to /root/.npmrc
|
||||
# under npm 12+; harmless if npm is still <12.
|
||||
# HINWEIS: gilt damit global (auch für eigene Installs unter /project) —
|
||||
# Install-Scripts fremder Pakete laufen dann ungefragt.
|
||||
npm config set dangerously-allow-all-scripts true 2>/dev/null || true
|
||||
|
||||
echo ">>> Installing global npm packages..."
|
||||
npm install -g "${NPM_QUIET[@]}" typescript ts-node eslint prettier
|
||||
|
||||
echo ">>> Installing pnpm..."
|
||||
# Für Projekte mit pnpm-Workflow. Zusätzlich corepack aktivieren, damit
|
||||
# Projekte mit "packageManager"-Feld in der package.json ihre gepinnte
|
||||
# pnpm/yarn-Version automatisch bekommen.
|
||||
npm install -g "${NPM_QUIET[@]}" pnpm || echo " [WARN] pnpm install failed"
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
corepack enable 2>/dev/null || true
|
||||
echo " pnpm $(pnpm --version 2>/dev/null || echo 'n/a')"
|
||||
|
||||
echo ">>> Installing Go..."
|
||||
GO_VERSION=$(curl -fsSL "https://go.dev/VERSION?m=text" | head -1)
|
||||
curl -fsSL "https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz
|
||||
rm -rf /usr/local/go
|
||||
tar -C /usr/local -xzf /tmp/go.tar.gz
|
||||
rm /tmp/go.tar.gz
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile.d/go.sh
|
||||
echo " Go $(/usr/local/go/bin/go version | awk '{print $3}')"
|
||||
|
||||
echo ">>> Installing Rust..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source "$HOME/.cargo/env"
|
||||
echo " Rust $(rustc --version | awk '{print $2}')"
|
||||
|
||||
echo ">>> Installing Docker..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
echo " Docker $(docker --version | awk '{print $3}' | tr -d ',')"
|
||||
|
||||
echo ">>> Installing Docker Compose plugin..."
|
||||
apt-get install -y -qq docker-compose-plugin 2>/dev/null || true
|
||||
echo " Compose $(docker compose version --short 2>/dev/null || echo 'included with Docker')"
|
||||
|
||||
echo ">>> Installing Claude Code (native installer)..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
# Ensure claude is on PATH for all sessions
|
||||
if [[ -f "$HOME/.local/bin/claude" ]]; then
|
||||
ln -sf "$HOME/.local/bin/claude" /usr/local/bin/claude 2>/dev/null || true
|
||||
elif [[ -f "$HOME/.claude/bin/claude" ]]; then
|
||||
ln -sf "$HOME/.claude/bin/claude" /usr/local/bin/claude 2>/dev/null || true
|
||||
fi
|
||||
CLAUDE_BIN="$(command -v claude || echo /usr/local/bin/claude)"
|
||||
echo " Claude Code installed: $("$CLAUDE_BIN" --version 2>/dev/null || echo 'version unknown')"
|
||||
|
||||
echo ">>> Configuring Claude Code settings (permissions + env)..."
|
||||
# NOTE on enabledPlugins: we DO declare it here. Earlier belief was that
|
||||
# enabledPlugins is ignored in non-interactive/container contexts — that's
|
||||
# wrong. `claude plugin install X@mkt` installs the plugin but leaves it
|
||||
# Status: disabled; the enabled-state lives in enabledPlugins in THIS file
|
||||
# (user settings, ~/.claude/settings.json), which IS honored. Without this
|
||||
# block every plugin below (including the LSP servers) stays disabled and
|
||||
# CloudCLI's /project chat shows only the 1 local skill. Enabled skills show
|
||||
# up live after the cloudcli restart — no fresh session needed.
|
||||
#
|
||||
# Verified keys (checked against code.claude.com/docs, 2026-07): the env vars
|
||||
# below are all valid current settings. CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
|
||||
# must live inside "env" (it's an environment variable, not a top-level key).
|
||||
# Removed: CLAUDE_CODE_ENABLE_AUTO_MODE (dead no-op since Claude Code v2.1.207 —
|
||||
# auto mode works from permissions.defaultMode alone) and alwaysThinkingEnabled
|
||||
# (current models use adaptive thinking; MAX_THINKING_TOKENS still caps the
|
||||
# budget). The old "enableRemoteControl" key was never real — Remote Control is
|
||||
# enabled per-session with /rc or for all sessions via /config (Pro/Max).
|
||||
mkdir -p /root/.claude
|
||||
# Permissions model: auto mode instead of a blanket allow-list.
|
||||
# defaultMode "auto" auto-approves actions but routes them through a
|
||||
# background safety classifier (still blocks rm -rf / , rm -rf ~ , etc.).
|
||||
# Unlike bypassPermissions, auto mode is NOT refused when running as root,
|
||||
# and defaultMode is honored from user settings (~/.claude/settings.json).
|
||||
# The "deny" list applies in EVERY mode (including auto) — it's the hard
|
||||
# floor protecting credentials/secrets and a few destructive commands.
|
||||
# Env extras: DISABLE_AUTOUPDATER stops Claude's background auto-updater (we
|
||||
# self-manage the version via agentic-update, so the background check just
|
||||
# risks version drift/races); CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is the
|
||||
# umbrella that silences telemetry/error-reporting/feedback/surveys for a quiet
|
||||
# headless box (trade-off: also disables error reporting).
|
||||
cat > /root/.claude/settings.json << 'SETTINGS'
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"permissions": {
|
||||
"defaultMode": "auto",
|
||||
"deny": [
|
||||
"Read(**/.env)",
|
||||
"Read(**/.env.*)",
|
||||
"Read(**/.credentials.json)",
|
||||
"Read(/root/.claude/.credentials.json)",
|
||||
"Read(**/secrets/**)",
|
||||
"Read(**/*.pem)",
|
||||
"Read(**/id_rsa)",
|
||||
"Read(**/id_ed25519)",
|
||||
"Bash(dd:*)",
|
||||
"Bash(mkfs:*)",
|
||||
"Bash(rm -rf /:*)",
|
||||
"Bash(rm -rf ~:*)"
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
|
||||
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000",
|
||||
"MAX_THINKING_TOKENS": "31999",
|
||||
"DISABLE_AUTOUPDATER": "1",
|
||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"code-review@claude-plugins-official": true,
|
||||
"commit-commands@claude-plugins-official": true,
|
||||
"context7@claude-plugins-official": true,
|
||||
"frontend-design@claude-plugins-official": true,
|
||||
"security-guidance@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"pyright-lsp@claude-plugins-official": true,
|
||||
"gopls-lsp@claude-plugins-official": true,
|
||||
"rust-analyzer-lsp@claude-plugins-official": true,
|
||||
"superpowers@superpowers-marketplace": true
|
||||
}
|
||||
}
|
||||
SETTINGS
|
||||
|
||||
echo ">>> Adding plugin marketplaces..."
|
||||
# The official marketplace is auto-registered on first INTERACTIVE launch only,
|
||||
# so a non-interactive provisioning run must add it explicitly. We use ONLY the
|
||||
# first-party marketplace now — the old 'anthropics/claude-code' demo repo
|
||||
# (registers as 'claude-code-plugins') ships same-named example copies that
|
||||
# shadow the official ones, and boxes were silently landing on those.
|
||||
"$CLAUDE_BIN" plugin marketplace add anthropics/claude-plugins-official \
|
||||
|| echo " [WARN] could not add claude-plugins-official"
|
||||
# Refresh the index BEFORE installing — a stale cache makes name@official
|
||||
# silently fall through, which is how boxes ended up on demo copies.
|
||||
"$CLAUDE_BIN" plugin marketplace update claude-plugins-official \
|
||||
|| echo " [WARN] could not update claude-plugins-official index"
|
||||
# Third-party marketplace for Superpowers.
|
||||
"$CLAUDE_BIN" plugin marketplace add obra/superpowers-marketplace \
|
||||
|| echo " [WARN] could not add superpowers-marketplace"
|
||||
|
||||
echo ">>> Installing Claude Code plugins (official CLI)..."
|
||||
# Install pinned to the official marketplace only (no demo fallthrough).
|
||||
# Non-fatal — a single failed plugin won't abort provisioning. Enabled-state is
|
||||
# declared in settings.json above (enabledPlugins); install just stages files.
|
||||
install_plugin() {
|
||||
local name="$1"
|
||||
if "$CLAUDE_BIN" plugin install "${name}@claude-plugins-official" 2>/dev/null; then
|
||||
echo " installed ${name}@claude-plugins-official"
|
||||
else
|
||||
echo " [WARN] could not install plugin: ${name}@claude-plugins-official"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
install_plugin frontend-design
|
||||
install_plugin code-review
|
||||
install_plugin commit-commands
|
||||
install_plugin security-guidance
|
||||
install_plugin context7
|
||||
# LSP plugins — real-time diagnostics + navigation. Thin wrappers around the
|
||||
# language servers installed below; they need the server binary on PATH.
|
||||
install_plugin typescript-lsp
|
||||
install_plugin pyright-lsp
|
||||
install_plugin gopls-lsp
|
||||
install_plugin rust-analyzer-lsp
|
||||
|
||||
# Superpowers lives in its own marketplace.
|
||||
"$CLAUDE_BIN" plugin install superpowers@superpowers-marketplace 2>/dev/null \
|
||||
&& echo " installed superpowers@superpowers-marketplace" \
|
||||
|| echo " [WARN] could not install superpowers"
|
||||
|
||||
# Sanity check — list what actually landed.
|
||||
echo ">>> Installed plugins:"
|
||||
"$CLAUDE_BIN" plugin list 2>/dev/null || echo " (plugin list unavailable)"
|
||||
|
||||
echo ">>> Installing language servers (backends for the *-lsp plugins)..."
|
||||
# The *-lsp plugins are thin wrappers that shell out to a language server on
|
||||
# PATH — they do NOT self-install the binary. Toolchains (Node/Go/Rust) are
|
||||
# already installed above, so this just adds the servers. All non-fatal.
|
||||
command -v typescript-language-server >/dev/null 2>&1 \
|
||||
|| npm install -g "${NPM_QUIET[@]}" typescript-language-server typescript \
|
||||
|| echo " [WARN] typescript-language-server install failed"
|
||||
command -v pyright-langserver >/dev/null 2>&1 \
|
||||
|| npm install -g "${NPM_QUIET[@]}" pyright \
|
||||
|| echo " [WARN] pyright install failed"
|
||||
if [ ! -x /usr/local/bin/gopls ] && [ -x /usr/local/go/bin/go ]; then
|
||||
GOBIN=/usr/local/bin /usr/local/go/bin/go install golang.org/x/tools/gopls@latest \
|
||||
|| echo " [WARN] gopls install failed"
|
||||
fi
|
||||
if ! command -v rust-analyzer >/dev/null 2>&1 && [ -x "$HOME/.cargo/bin/rustup" ]; then
|
||||
"$HOME/.cargo/bin/rustup" component add rust-analyzer >/dev/null 2>&1 \
|
||||
&& ln -sf "$("$HOME/.cargo/bin/rustup" which rust-analyzer 2>/dev/null)" /usr/local/bin/rust-analyzer \
|
||||
|| echo " [WARN] rust-analyzer install failed"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FALLBACK if the above installs don't persist in your Claude Code version:
|
||||
# Headless plugin install has been historically finicky. The container-intended
|
||||
# mechanism is the (currently undocumented) CLAUDE_CODE_PLUGIN_SEED_DIR env var,
|
||||
# which seeds pre-staged plugins on first launch. If `claude plugin list` above
|
||||
# is empty after provisioning, stage the plugin repos into a seed dir and export
|
||||
# CLAUDE_CODE_PLUGIN_SEED_DIR=<dir> in /root/.bashrc, or simply run the
|
||||
# `claude plugin install` commands above once inside an interactive session.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo ">>> Installing webapp-testing skill (from anthropics/skills)..."
|
||||
# Installed as a plain user skill (NOT a marketplace plugin). A SKILL.md placed
|
||||
# under ~/.claude/skills/ is picked up automatically with no plugin entry needed.
|
||||
git clone --depth 1 --filter=blob:none --sparse https://github.com/anthropics/skills.git /tmp/anthropic-skills
|
||||
cd /tmp/anthropic-skills && git sparse-checkout set skills/webapp-testing
|
||||
mkdir -p /root/.claude/skills/
|
||||
cp -r /tmp/anthropic-skills/skills/webapp-testing /root/.claude/skills/webapp-testing
|
||||
rm -rf /tmp/anthropic-skills
|
||||
cd /root
|
||||
|
||||
echo ">>> Installing Playwright (Python) for the webapp-testing skill..."
|
||||
# The webapp-testing skill imports the PYTHON playwright package
|
||||
# ("from playwright.sync_api import ..."), so the Node playwright we install for
|
||||
# CloudCLI's Browser feature is NOT enough — the pip package must be present too,
|
||||
# or the skill dies at import. Both share the same browser cache
|
||||
# (~/.cache/ms-playwright), so Chromium is downloaded once for both.
|
||||
# Playwright doesn't support Ubuntu 26.04 yet (microsoft/playwright#40117) and
|
||||
# hard-errors instead of falling back, so force the ubuntu24.04 build, which runs
|
||||
# fine on 26.04's newer glibc. This whole block is non-fatal: a browser-install
|
||||
# failure must NEVER abort provisioning (it previously killed everything after
|
||||
# it — Docker services, SSH, cron — because the script runs under `set -e`).
|
||||
set +e
|
||||
# The override MUST include the arch suffix — Playwright's build keys are
|
||||
# "ubuntu24.04-x64", not "ubuntu24.04". Without "-x64" it can't find a build.
|
||||
export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE="ubuntu24.04-x64"
|
||||
# Make the override durable for interactive sessions too (systemd services read
|
||||
# their own Environment=, but 'claude' launched over SSH reads /etc/environment),
|
||||
# so the skill can re-install browsers on 26.04 later without hitting the error.
|
||||
grep -q PLAYWRIGHT_HOST_PLATFORM_OVERRIDE /etc/environment 2>/dev/null \
|
||||
|| echo 'PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64' >> /etc/environment
|
||||
pip install --break-system-packages -q playwright \
|
||||
&& echo " python playwright package installed" \
|
||||
|| echo " [WARN] pip install playwright failed (webapp-testing skill will error on import)"
|
||||
if python3 -m playwright install --with-deps chromium; then
|
||||
echo " Playwright chromium + OS deps installed (ubuntu24.04-x64 build, running on 26.04)"
|
||||
elif python3 -m playwright install chromium; then
|
||||
echo " Playwright chromium installed (browser only; some OS deps may be missing)"
|
||||
else
|
||||
echo " [WARN] Playwright browser install failed."
|
||||
echo " Ubuntu 26.04 isn't supported by Playwright yet (microsoft/playwright#40117)."
|
||||
echo " Re-run this once support lands, or to retry the 24.04-build workaround:"
|
||||
echo " PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 python3 -m playwright install --with-deps chromium"
|
||||
fi
|
||||
unset PLAYWRIGHT_HOST_PLATFORM_OVERRIDE
|
||||
set -e
|
||||
|
||||
echo ">>> Setting up /project directory..."
|
||||
mkdir -p /project
|
||||
cat > /project/CLAUDE.md << 'CLAUDEMD'
|
||||
# Claude Code Workspace
|
||||
|
||||
## Environment
|
||||
- **OS**: Ubuntu 26.04 LXC container on Proxmox
|
||||
- **Working directory**: /project
|
||||
- **Timezone**: Europe/Berlin
|
||||
- **User**: root
|
||||
|
||||
## Available Tools
|
||||
- **Languages**: Node.js (latest LTS), Python 3 (system default), Go (latest), Rust (latest)
|
||||
- **Package managers**: npm, pnpm (+ corepack), pip (use --break-system-packages), cargo, go install
|
||||
- **Docker**: Docker Engine + Compose plugin, running and ready
|
||||
- **Web UI**: CloudCLI UI (claudecodeui) on port 3001 — chat, file explorer/editor, git, shell
|
||||
- **Search tools**: ripgrep (rg), fd-find (fdfind), fzf
|
||||
- **Databases**: PostgreSQL client (psql), Redis client (redis-cli), SQLite3
|
||||
|
||||
## Permissions
|
||||
Permission mode is "auto" (permissions.defaultMode). Actions are auto-approved
|
||||
but pass through a background safety classifier that still blocks catastrophic
|
||||
commands (rm -rf /, rm -rf ~). A deny floor in ~/.claude/settings.json applies in
|
||||
every mode and blocks reading credentials/secrets (.env, *.pem, id_rsa,
|
||||
.credentials.json) — do not try to work around it.
|
||||
|
||||
## Agent Teams
|
||||
Agent teams are enabled. You can spawn parallel teammates for complex tasks:
|
||||
- Use agent teams for work that benefits from parallel exploration
|
||||
- Use subagents (Task tool) for quick focused work that reports back
|
||||
- tmux is installed for split-pane team visualization
|
||||
|
||||
## Remote Control
|
||||
Remote Control lets you steer a live local session from the Claude mobile app or
|
||||
web. It is NOT enabled via settings.json — turn it on per session with `/rc`
|
||||
(or `claude remote-control`), or for all sessions via `/config` →
|
||||
"Enable Remote Control for all sessions". Requires a Pro/Max login (research
|
||||
preview), so it only applies once someone signs in interactively.
|
||||
|
||||
## Docker Usage
|
||||
Docker compose files should go in /docker/<service-name>/docker-compose.yml.
|
||||
There is no always-on Watchtower daemon. Container images are refreshed one-shot
|
||||
by the weekly `agentic-update` run (or on demand: `agentic-update`), so give
|
||||
containers you want updated the usual `restart: unless-stopped`.
|
||||
All Docker containers in this LXC need `security_opt: [apparmor=unconfined]`.
|
||||
|
||||
## Conventions
|
||||
- Prefer creating files over printing long code blocks
|
||||
- Use git for version control on all projects in /project/src/
|
||||
- When installing Python packages, use: pip install --break-system-packages <package>
|
||||
- Thinking is adaptive — the model decides when to think; lean into it for complex work
|
||||
|
||||
## Installed Plugins / Skills
|
||||
Plugins are staged via the `claude plugin` CLI at provision time and enabled via
|
||||
the `enabledPlugins` block in ~/.claude/settings.json. Run `claude plugin list`
|
||||
to confirm what's active.
|
||||
- **frontend-design**: Production-grade UI with distinctive aesthetics (auto-activates on frontend tasks)
|
||||
- **code-review**: Multi-agent PR review with confidence scoring
|
||||
- **commit-commands**: Git commit, push, and PR workflows (/commit, /push, /pr)
|
||||
- **security-guidance**: Security warnings when editing sensitive files
|
||||
- **context7**: Live, version-specific library docs lookup (reduces API hallucinations)
|
||||
- **typescript-lsp / pyright-lsp / gopls-lsp / rust-analyzer-lsp**: real-time
|
||||
diagnostics + navigation (backed by the language servers on PATH)
|
||||
- **superpowers**: Development workflow framework — brainstorm → plan → implement with TDD
|
||||
- /superpowers:brainstorm — Refine ideas before coding
|
||||
- /superpowers:write-plan — Create implementation plans
|
||||
- /superpowers:execute-plan — Execute plans in batches via subagents
|
||||
- Auto-activating skills: test-driven-development, systematic-debugging, verification-before-completion
|
||||
- **webapp-testing** (local skill, not a marketplace plugin): Playwright-based
|
||||
browser testing for UI verification and debugging
|
||||
CLAUDEMD
|
||||
|
||||
echo ">>> Configuring SSH..."
|
||||
sed -i "s/^#*PermitRootLogin.*/PermitRootLogin yes/" /etc/ssh/sshd_config
|
||||
sed -i "s/^#*PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config
|
||||
systemctl enable ssh
|
||||
systemctl restart ssh
|
||||
|
||||
echo ">>> Setting up shell environment..."
|
||||
cat >> /root/.bashrc << 'BASHRC'
|
||||
|
||||
# ── Claude Code Container ──────────────────────────────────
|
||||
export EDITOR=nano
|
||||
export LANG=en_US.UTF-8
|
||||
export TZ=Europe/Berlin
|
||||
export PATH="$HOME/.local/bin:$HOME/.claude/bin:$HOME/.cargo/bin:/usr/local/go/bin:$PATH"
|
||||
|
||||
# Aliases
|
||||
alias ll="ls -lah --color=auto"
|
||||
alias cls="clear"
|
||||
alias ..="cd .."
|
||||
alias ...="cd ../.."
|
||||
alias gs="git status"
|
||||
alias gl="git log --oneline -20"
|
||||
alias dc="docker compose"
|
||||
alias dps="docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
|
||||
|
||||
# Always start in /project
|
||||
cd /project 2>/dev/null || true
|
||||
BASHRC
|
||||
|
||||
echo ">>> Setting up Git defaults..."
|
||||
git config --global init.defaultBranch main
|
||||
git config --global core.editor nano
|
||||
git config --global pull.rebase false
|
||||
|
||||
echo ">>> Docker ready (no always-on service containers)..."
|
||||
# code-server was removed — CloudCLI UI (below) already provides a file
|
||||
# explorer/editor, so it was redundant (and dropped its :8443 + hardcoded
|
||||
# 'admin' password surface). Watchtower is no longer a scheduled daemon; image
|
||||
# updates for any containers YOU create now run one-shot from the coordinated
|
||||
# 'agentic-update' script (see below).
|
||||
|
||||
echo ">>> Installing CloudCLI UI (claudecodeui web front end)..."
|
||||
# Web front end for Claude Code: chat UI, file explorer/editor, git panel, and a
|
||||
# built-in shell, served on :3001. It drives the same `claude` CLI and reads
|
||||
# /root/.claude, so it inherits this container's login and auto-mode settings.
|
||||
# It ships with its own login/auth (first-run account setup on the login page).
|
||||
# Installed from the published npm package (latest, unpinned); the package
|
||||
# includes a prebuilt server, so there is no client/server build step here.
|
||||
npm install -g "${NPM_QUIET[@]}" @cloudcli-ai/cloudcli \
|
||||
|| echo " [WARN] CloudCLI UI install failed (needs Node 22+ and build tools for node-pty)"
|
||||
# Self-heal in case the native build was still skipped (better-sqlite3 binding
|
||||
# missing) — rebuild in place so the service can bind :3001. Gated by the
|
||||
# allowlist set right after the npm self-update above.
|
||||
CCUI_PKG="$(npm root -g 2>/dev/null)/@cloudcli-ai/cloudcli"
|
||||
if [ -d "$CCUI_PKG/node_modules/better-sqlite3" ] && ! ls "$CCUI_PKG/node_modules/better-sqlite3/build/Release/"*.node >/dev/null 2>&1; then
|
||||
echo " rebuilding CloudCLI native modules (better-sqlite3/node-pty/bcrypt)"
|
||||
( cd "$CCUI_PKG" && npm rebuild >/dev/null 2>&1 ) || echo " [WARN] CloudCLI native rebuild failed"
|
||||
fi
|
||||
CCUI_BIN="$(command -v cloudcli || echo /usr/bin/cloudcli)"
|
||||
mkdir -p /root/.cloudcli
|
||||
echo " CloudCLI UI installed: $("$CCUI_BIN" version 2>/dev/null || echo 'version unknown')"
|
||||
|
||||
# Browser feature runtime. CloudCLI detects Playwright via require('playwright')
|
||||
# from its GLOBAL package dir, but its in-app "Install Runtime" button installs
|
||||
# into the server's cwd (/project) — which require() can't resolve, so the button
|
||||
# silently never takes effect. Fix: install Playwright GLOBALLY (resolvable) plus
|
||||
# its Chromium using the ubuntu24.04-x64 fallback build (26.04 isn't officially
|
||||
# supported by Playwright yet), so the Browser tab is ready with no button click.
|
||||
echo ">>> Installing Playwright runtime for CloudCLI Browser feature..."
|
||||
export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64
|
||||
npm install -g "${NPM_QUIET[@]}" playwright \
|
||||
|| echo " [WARN] global Playwright install failed (CloudCLI Browser feature won't work)"
|
||||
playwright install chromium \
|
||||
|| echo " [WARN] Chromium download failed; retry later with: PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 playwright install chromium"
|
||||
unset PLAYWRIGHT_HOST_PLATFORM_OVERRIDE
|
||||
|
||||
# systemd unit so the UI survives reboots and restarts on failure.
|
||||
cat > /etc/systemd/system/cloudcli.service << EOF
|
||||
[Unit]
|
||||
Description=CloudCLI UI (claudecodeui) - web front end for Claude Code
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Environment=NODE_ENV=production
|
||||
# NODE_ENV=production makes child 'npm install' omit devDependencies, which
|
||||
# breaks CloudCLI's in-app plugin installs (their 'npm run build' is 'tsc' and
|
||||
# needs typescript/@types/node). Force npm to include dev deps so plugin builds
|
||||
# succeed.
|
||||
Environment=NPM_CONFIG_INCLUDE=dev
|
||||
# CloudCLI's "Install Runtime" (Playwright/Chromium for the Browser feature)
|
||||
# hard-fails on Ubuntu 26.04, which Playwright doesn't officially support yet
|
||||
# (microsoft/playwright#40117). Same override we use for the webapp-testing
|
||||
# skill: force the ubuntu24.04-x64 fallback build, which runs on 26.04's glibc.
|
||||
Environment=PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64
|
||||
Environment=HOME=/root
|
||||
Environment=HOST=0.0.0.0
|
||||
Environment=SERVER_PORT=3001
|
||||
Environment=CLAUDE_CLI_PATH=/usr/local/bin/claude
|
||||
Environment=DATABASE_PATH=/root/.cloudcli/auth.db
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/bin
|
||||
WorkingDirectory=/project
|
||||
ExecStart=${CCUI_BIN} start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable cloudcli >/dev/null 2>&1 || true
|
||||
systemctl start cloudcli \
|
||||
|| echo " [WARN] cloudcli service failed to start; check 'journalctl -u cloudcli'"
|
||||
|
||||
echo ">>> Installing coordinated update tooling (agentic-update / agentic-doctor)..."
|
||||
# Single coordinated update pass, modeled on homelabhero's hh-update + hh doctor:
|
||||
# OS packages -> Claude Code -> CloudCLI UI -> one-shot Watchtower for any Docker
|
||||
# containers -> restart UI -> health check -> log. Re-runnable on demand
|
||||
# ('agentic-update'), no container recreation required. Everything tracks latest
|
||||
# (unpinned); the post-update health check is the safety net, and results land
|
||||
# in /var/log/agentic-update.log.
|
||||
|
||||
cat > /usr/local/bin/agentic-doctor << 'DOCTOR'
|
||||
#!/usr/bin/env bash
|
||||
# Health check for the Claude Code container. Exit 0 = healthy, non-zero = problems.
|
||||
LOG_TAG="[doctor]"
|
||||
rc=0
|
||||
check() { if eval "$2" >/dev/null 2>&1; then echo " OK $1"; else echo " FAIL $1"; rc=1; fi; }
|
||||
# note() is informational only — it never fails the health check. Used for
|
||||
# user-actionable states (like "not logged in yet") that aren't container faults.
|
||||
note() { if eval "$2" >/dev/null 2>&1; then echo " OK $1"; else echo " WARN $1"; fi; }
|
||||
|
||||
echo "$LOG_TAG $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
check "claude binary present" "command -v claude"
|
||||
check "claude reports a version" "claude --version"
|
||||
note "claude logged in (creds on disk; run 'claude' /login if WARN)" "test -s /root/.claude/.credentials.json"
|
||||
check "cloudcli service active" "systemctl is-active --quiet cloudcli"
|
||||
check "UI listening on :3001" "ss -ltn | grep -q ':3001'"
|
||||
check "docker daemon running" "systemctl is-active --quiet docker"
|
||||
check "disk under 90% on /" "test $(df --output=pcent / | tail -1 | tr -dc 0-9) -lt 90"
|
||||
|
||||
if [ $rc -eq 0 ]; then echo "$LOG_TAG all checks passed"; else echo "$LOG_TAG one or more checks FAILED"; fi
|
||||
exit $rc
|
||||
DOCTOR
|
||||
chmod +x /usr/local/bin/agentic-doctor
|
||||
|
||||
cat > /usr/local/bin/agentic-update << 'UPDATE'
|
||||
#!/usr/bin/env bash
|
||||
# Coordinated update pass for the Claude Code container.
|
||||
# Run on demand: agentic-update
|
||||
# Runs weekly via /etc/cron.d/agentic-update. Log: /var/log/agentic-update.log
|
||||
# Deliberately NOT 'set -e': one component failing must not abort the rest.
|
||||
set -uo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
LOG=/var/log/agentic-update.log
|
||||
exec >>"$LOG" 2>&1
|
||||
echo "==================================================================="
|
||||
echo ">>> agentic-update starting: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
|
||||
echo ">>> [1/6] OS packages (includes Node.js patches within its LTS line)..."
|
||||
apt-get update -qq && apt-get upgrade -y -qq && apt-get autoremove -y -qq && apt-get clean -qq
|
||||
|
||||
echo ">>> [2/6] npm (latest)..."
|
||||
npm install -g --no-fund --no-audit --loglevel=error npm@latest || echo " [WARN] npm self-update failed"
|
||||
# npm 12+ blocks dependencies' install scripts behind an allowlist (separate
|
||||
# from ignore-scripts), so CloudCLI's native deps (better-sqlite3/node-pty/
|
||||
# bcrypt) never compile -> cloudcli crash-loops and nothing binds :3001. Boxes
|
||||
# deploy on npm 10 (builds fine) but this weekly run upgrades npm above, so the
|
||||
# config MUST be set here -- AFTER npm@latest lands (it only persists to
|
||||
# /root/.npmrc under npm 12+) and BEFORE the cloudcli reinstall/rebuild below.
|
||||
npm config set dangerously-allow-all-scripts true 2>/dev/null || true
|
||||
|
||||
echo ">>> [3/6] Claude Code..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash || echo " [WARN] Claude Code update failed"
|
||||
|
||||
echo ">>> [4/6] CloudCLI UI (claudecodeui)..."
|
||||
npm install -g --no-fund --no-audit --loglevel=error @cloudcli-ai/cloudcli@latest || echo " [WARN] CloudCLI UI update failed"
|
||||
# Self-heal: if the native build was skipped (better-sqlite3 binding missing),
|
||||
# rebuild in place before restart -- else cloudcli loops on "Could not locate
|
||||
# the bindings file". npm rebuild is itself gated by the config set above.
|
||||
CCUI_PKG="$(npm root -g 2>/dev/null)/@cloudcli-ai/cloudcli"
|
||||
if [ -d "$CCUI_PKG/node_modules/better-sqlite3" ] && ! ls "$CCUI_PKG/node_modules/better-sqlite3/build/Release/"*.node >/dev/null 2>&1; then
|
||||
echo " rebuilding CloudCLI native modules (better-sqlite3/node-pty/bcrypt)"
|
||||
( cd "$CCUI_PKG" && npm rebuild >/dev/null 2>&1 ) || echo " [WARN] CloudCLI native rebuild failed"
|
||||
fi
|
||||
systemctl restart cloudcli || echo " [WARN] cloudcli restart failed"
|
||||
|
||||
echo ">>> [5/6] Docker images (one-shot Watchtower)..."
|
||||
if command -v docker >/dev/null 2>&1 && [ -n "$(docker ps -q 2>/dev/null)" ]; then
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
containrrr/watchtower --run-once --cleanup \
|
||||
|| echo " [WARN] Watchtower one-shot run failed"
|
||||
else
|
||||
echo " (no running containers; skipped)"
|
||||
fi
|
||||
|
||||
echo ">>> [6/6] Health check..."
|
||||
if agentic-doctor; then
|
||||
echo ">>> agentic-update finished OK: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
else
|
||||
echo ">>> agentic-update finished WITH HEALTH-CHECK FAILURES: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
fi
|
||||
echo ""
|
||||
UPDATE
|
||||
chmod +x /usr/local/bin/agentic-update
|
||||
|
||||
cat > /etc/cron.d/agentic-update << 'CRON'
|
||||
# Weekly coordinated update - Sunday 4:00 AM (Europe/Berlin, container local time)
|
||||
# (OS + Claude Code + CloudCLI UI + container images, then a health check)
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||
0 4 * * 0 root /usr/local/bin/agentic-update
|
||||
CRON
|
||||
chmod 0644 /etc/cron.d/agentic-update
|
||||
|
||||
cat > /etc/logrotate.d/agentic-update << 'LOGROTATE'
|
||||
/var/log/agentic-update.log {
|
||||
monthly
|
||||
rotate 3
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
}
|
||||
LOGROTATE
|
||||
|
||||
echo ">>> Cleaning up..."
|
||||
apt-get autoremove -y -qq
|
||||
apt-get clean -qq
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════╗"
|
||||
echo "║ Provisioning Complete! ║"
|
||||
echo "╚══════════════════════════════════════════════════╝"
|
||||
PROVISION_EOF
|
||||
|
||||
chmod +x /tmp/provision-${CT_ID}.sh
|
||||
pct push "$CT_ID" /tmp/provision-${CT_ID}.sh /tmp/provision.sh
|
||||
pct exec "$CT_ID" -- chmod +x /tmp/provision.sh
|
||||
pct exec "$CT_ID" -- /tmp/provision.sh
|
||||
rm -f /tmp/provision-${CT_ID}.sh
|
||||
}
|
||||
|
||||
# ── Configure Git Identity (optional) ──────────────────────────────────────
|
||||
configure_git_identity() {
|
||||
if [[ -n "${GIT_NAME:-}" ]]; then
|
||||
info "Setting git user.name..."
|
||||
pct exec "$CT_ID" -- git config --global user.name "$GIT_NAME" \
|
||||
|| warn "Could not set git user.name"
|
||||
fi
|
||||
if [[ -n "${GIT_EMAIL:-}" ]]; then
|
||||
info "Setting git user.email..."
|
||||
pct exec "$CT_ID" -- git config --global user.email "$GIT_EMAIL" \
|
||||
|| warn "Could not set git user.email"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Write Proxmox Notes ─────────────────────────────────────────────────────
|
||||
# Drops a Markdown "card" into the container's Notes panel in the Proxmox UI
|
||||
# (rendered as Markdown on PVE 7+). Uses placeholders so the heredoc can stay
|
||||
# single-quoted (no accidental expansion of backticks/$ in the Markdown).
|
||||
write_notes() {
|
||||
local ct_ip
|
||||
ct_ip=$(pct exec "$CT_ID" -- hostname -I 2>/dev/null | awk '{print $1}')
|
||||
ct_ip="${ct_ip:-<container-ip>}"
|
||||
|
||||
local notes
|
||||
notes=$(cat <<'EOF'
|
||||
# 🤖 Claude Code Container
|
||||
|
||||
**Web UI (CloudCLI UI):** http://__IP__:3001 — _create a login on first visit_
|
||||
**SSH:** `ssh root@__IP__` | **Console:** `pct enter __CTID__`
|
||||
|
||||
## Start Claude Code
|
||||
Log in, then run `claude` (the shell auto-cd's to `/project`).
|
||||
|
||||
## Update & health
|
||||
- `agentic-update` — one coordinated pass: OS → Claude Code → Web UI → container images → health check
|
||||
- `agentic-doctor` — run the health check on its own
|
||||
- Auto-runs weekly (Sunday 4 AM, Europe/Berlin). Log: `/var/log/agentic-update.log`
|
||||
|
||||
## Service & config
|
||||
- `systemctl status cloudcli` — the Web UI service (`journalctl -u cloudcli` for logs)
|
||||
- Permissions: **auto mode** + secret deny-floor — `/root/.claude/settings.json`
|
||||
|
||||
---
|
||||
_IP above is the address at deploy time; on DHCP it may change (check with `pct exec __CTID__ -- hostname -I`)._
|
||||
EOF
|
||||
)
|
||||
notes=${notes//__IP__/$ct_ip}
|
||||
notes=${notes//__CTID__/$CT_ID}
|
||||
|
||||
if pct set "$CT_ID" --description "$notes" >/dev/null 2>&1; then
|
||||
success "Wrote container notes to the Proxmox UI."
|
||||
else
|
||||
warn "Could not set container notes (non-fatal)."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Print Summary ─────────────────────────────────────────────────────────
|
||||
print_summary() {
|
||||
local ct_ip
|
||||
ct_ip=$(pct exec "$CT_ID" -- hostname -I 2>/dev/null | awk '{print $1}')
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}${BOLD}║ Claude Code LXC Ready! ║${NC}"
|
||||
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Container:${NC} $CT_ID ($CT_HOSTNAME)"
|
||||
echo -e " ${BOLD}IP:${NC} ${ct_ip:-pending (DHCP)}"
|
||||
echo -e " ${BOLD}Resources:${NC} ${CT_CORES} CPU / $(( CT_RAM / 1024 )) GB RAM / ${CT_DISK} GB disk"
|
||||
echo -e " ${BOLD}Storage:${NC} $CT_STORAGE"
|
||||
echo -e " ${BOLD}Timezone:${NC} Europe/Berlin"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Connect:${NC}"
|
||||
echo -e " Console: ${CYAN}pct enter $CT_ID${NC}"
|
||||
[[ -n "${ct_ip:-}" ]] && echo -e " SSH: ${CYAN}ssh root@${ct_ip}${NC}"
|
||||
[[ -n "${ct_ip:-}" ]] && echo -e " Web UI: ${CYAN}http://${ct_ip}:3001${NC} (CloudCLI UI — create a login on first visit)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Start Claude Code:${NC}"
|
||||
echo -e " ${CYAN}claude${NC} (shell auto-cd's to /project on login)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Verify plugins:${NC} ${CYAN}claude plugin list${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Installed:${NC}"
|
||||
echo " • Claude Code (native) • Node.js (latest LTS) + npm/pnpm"
|
||||
echo " • Python 3 + pip + venv • Go (latest)"
|
||||
echo " • Rust (via rustup) • Docker + Compose"
|
||||
echo " • Git, ripgrep, fzf, fd • Build essentials"
|
||||
echo " • PostgreSQL & Redis CLI • CloudCLI UI web front end (port 3001)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Permissions:${NC} Auto mode (classifier-guarded) + deny floor for secrets"
|
||||
echo -e " ${BOLD}Config:${NC} ~/.claude/settings.json"
|
||||
echo -e " ${BOLD}Features:${NC} Agent teams (experimental), adaptive thinking, 64k output tokens"
|
||||
echo -e " ${BOLD}Remote Control:${NC} enable per-session with ${CYAN}/rc${NC} or all sessions via ${CYAN}/config${NC} (Pro/Max)"
|
||||
echo -e " ${BOLD}Plugins:${NC} frontend-design, code-review, commit-commands, security-guidance,"
|
||||
echo -e " context7, superpowers, + LSP (typescript/pyright/gopls/rust-analyzer)"
|
||||
echo -e " ${BOLD}Skills:${NC} webapp-testing (local, Playwright)"
|
||||
echo -e " ${BOLD}Updates:${NC} Sonntags 4 Uhr (Europe/Berlin) — coordinated (OS + Claude + UI + containers)"
|
||||
echo -e " then a health check. Run anytime: ${CYAN}agentic-update${NC} / ${CYAN}agentic-doctor${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
header
|
||||
preflight
|
||||
get_config
|
||||
get_template
|
||||
create_container
|
||||
start_container
|
||||
provision_container
|
||||
configure_git_identity
|
||||
write_notes
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user