Merge remote-tracking branch 'forgejo/master' into emo/fan-control-ha-actuator
All checks were successful
ci/woodpecker/push/default Pipeline was successful

This commit is contained in:
Emil Barzin 2026-06-16 08:08:27 +00:00
commit 5bc3d27d1b
42 changed files with 3072 additions and 387 deletions

View file

@ -270,6 +270,43 @@ install_user_claude_token() {
log "shared Claude token -> $user (t3-serve env; restart needed to take effect)"
}
# Re-deploy the managed per-user Claude launcher to ~/start-claude.sh. /etc/skel only
# seeds it at account creation (setup-devvm.sh), so without this a launcher edit never
# reaches EXISTING users — they keep running a stale copy. Copy-if-changed from the repo's
# skel/, owned by the user, 0755. (We deliberately do NOT re-copy .tmux.conf: terminal-lobby
# appends a managed persistence section to each user's ~/.tmux.conf that a re-copy would clobber.)
deploy_user_launcher() {
local user="$1" home src dst
src="$WORKSTATION_DIR/skel/start-claude.sh"
home="$(getent passwd "$user" | cut -d: -f6)"
[[ -n "$home" && -d "$home" && -f "$src" ]] || return 0
dst="$home/start-claude.sh"
cmp -s "$src" "$dst" 2>/dev/null && return 0 # already current -> no churn
if [[ "$DRY_RUN" == 1 ]]; then echo "[dry-run] deploy launcher -> $dst"; return 0; fi
install -m 0755 "$src" "$dst"
chown "$user:$user" "$dst"
log "deployed start-claude.sh -> $user"
}
# Ensure the per-user NATIVE claude install (the recommended runtime: ~user/.local/bin/claude,
# self-updating) — used by BOTH the terminal launcher AND the user's t3-serve instance. We do
# NOT npm-install claude system-wide (npm/npx isn't the recommended runtime); each user gets
# their own native install. Idempotent: skip if already present. Runs the official native
# installer AS the user (into their ~/.local). Best-effort: a failure WARNs and retries next
# reconcile (start-claude.sh also self-bootstraps the terminal path).
install_user_claude_native() {
local user="$1" home
home="$(getent passwd "$user" | cut -d: -f6)"
[[ -n "$home" && -d "$home" ]] || return 0
[[ -x "$home/.local/bin/claude" ]] && return 0 # already native -> done
if [[ "$DRY_RUN" == 1 ]]; then echo "[dry-run] native claude install -> $user"; return 0; fi
if runuser -u "$user" -- bash -lc 'curl -fsSL https://claude.ai/install.sh | bash' >/dev/null 2>&1; then
log "installed native claude -> $user"
else
log "WARN: native claude install failed for $user (retries next reconcile)"
fi
}
[[ $EUID -eq 0 ]] || { echo "t3-provision-users: must run as root" >&2; exit 1; }
for bin in python3 jq; do command -v "$bin" >/dev/null || { echo "missing $bin" >&2; exit 1; }; done
[[ -f "$ROSTER" && -f "$ENGINE" ]] || { echo "roster/engine not under $WORKSTATION_DIR" >&2; exit 1; }
@ -346,8 +383,10 @@ while IFS=$'\t' read -r os_user tier shell groups_csv code_layout repos_csv; do
fi
install_user_kubeconfig "$os_user"
install_user_claude_token "$os_user"
deploy_user_launcher "$os_user" # keep ~/start-claude.sh current (skel only seeds new accounts)
fi
refresh_codex_mirror "$os_user" # all tiers — mirror of the managed claudeMd
install_user_claude_native "$os_user" # all tiers — per-user native claude (terminal + t3); no npm/npx
done < <(jq -r '.accounts[] | [.os_user, .tier, .shell, (if (.groups|length)==0 then "-" else (.groups|join(",")) end), .code_layout, (if (.repos|length)==0 then "-" else (.repos|join(",")) end)] | @tsv' "$desired_file")
# 5) per-user .env (sticky port) + enable t3-serve@

View file

@ -21,7 +21,13 @@ export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y "${PKGS[@]}" >/dev/null
# 2) node >= 18 + claude-code (claude-code requires node >= 18)
# 2) node >= 18 — needed for the t3 CLI (npm-global, below). NOT for claude-code:
# claude-code is the per-user NATIVE install (the recommended, self-updating
# ~/.local/bin/claude), provisioned per user by t3-provision-users
# (install_user_claude_native) and self-bootstrapped by start-claude.sh on first launch.
# We deliberately do NOT `npm install -g @anthropic-ai/claude-code` — npm/npx is not the
# recommended runtime, and a system-wide npm copy just shadows/duplicates the per-user
# native installs everyone auto-migrates to anyway.
need_node=1
if command -v node >/dev/null; then
[[ "$(node -v | sed 's/^v\([0-9]*\).*/\1/')" -ge 18 ]] && need_node=0
@ -31,14 +37,23 @@ if [[ $need_node -eq 1 ]]; then
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null
apt-get install -y nodejs >/dev/null
fi
# Detect the GLOBAL npm package, NOT whatever `claude` resolves to on PATH: the admin's
# personal ~/.local/bin/claude shadows it, so `command -v claude` silently skipped the
# system-wide install — leaving /usr/lib/node_modules/@anthropic-ai empty and fresh
# non-admins with no claude (they only worked because the admin's install was on PATH).
if ! npm ls -g --depth=0 @anthropic-ai/claude-code >/dev/null 2>&1; then
log "npm: installing @anthropic-ai/claude-code (system-wide)"
npm install -g @anthropic-ai/claude-code >/dev/null
fi
# 2a) ~/.local/bin on PATH for all LOGIN shells (machine-wide). The native claude install
# lives at ~/.local/bin; this guarantees login shells (SSH, etc.) find it regardless of
# whether the per-user native-installer rc edit ran. (The terminal launcher sets PATH
# itself, and t3-serve@.service hard-sets PATH in the unit.)
install -d -m 0755 /etc/profile.d
cat > /etc/profile.d/10-local-bin.sh <<'PROFILE_EOF'
# Native per-user installs (e.g. claude-code) live in ~/.local/bin — put it on PATH.
# Guarded so it never duplicates. Sourced by login shells (bash via /etc/profile; zsh
# login via /etc/zsh/zprofile -> /etc/profile).
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
PROFILE_EOF
chmod 0644 /etc/profile.d/10-local-bin.sh
log "/etc/profile.d/10-local-bin.sh (~/.local/bin on PATH for login shells)"
# 2b) t3 (the per-user coding surface) — PINNED, never nightly/latest. t3 is pre-1.0 and
# ships breaking auth-schema + bootstrap-API changes our t3-dispatch can't follow blind

View file

@ -11,6 +11,14 @@ echo " Starting Claude Code in $HOME/code ..."
echo " (Right-click for tmux menu, or Ctrl+B then | or - to split)"
echo ""
# The native claude install lives in ~/.local/bin. This launcher runs in tmux's non-login
# env, which does NOT source the user's shell rc (where the native installer added it to
# PATH) — so `claude` would appear missing here. Put it on PATH ourselves; guarded/idempotent.
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
name_args=()
if [ -n "${TMUX:-}" ]; then
sess="$(tmux display-message -p '#{session_name}' 2>/dev/null)"
@ -42,14 +50,48 @@ else
done
fi
# Prefer the system-wide `claude` (installed by setup-devvm.sh); fall back to npx.
# Run the NATIVE `claude` (the recommended install: ~/.local/bin/claude, self-updating).
# No npm/npx. If the native binary is missing (a fresh account before the hourly reconcile
# has provisioned it), bootstrap it with the official native installer, then run it.
launch() {
if command -v claude >/dev/null 2>&1; then
claude "$@"
if ! command -v claude >/dev/null 2>&1; then
echo " Installing Claude Code (native) for $(id -un)"
curl -fsSL https://claude.ai/install.sh | bash || return 127
export PATH="$HOME/.local/bin:$PATH"
fi
claude "$@"
}
# Re-assert Claude Code's first-run onboarding flag before launch. ~/.claude.json is a
# SINGLE file that ALL of a user's concurrent claude processes (this terminal, their
# t3-serve instance, agent/SDK sessions) read-modify-write; a stale writer periodically
# drops top-level keys — including hasCompletedOnboarding — which throws the next
# interactive session back to the "Choose the text style" wizard even though the user is
# fully logged in (credentials live in the SEPARATE ~/.claude/.credentials.json, which is
# never affected). Idempotent, runs as the user right before launch, never clobbers other
# keys. Best-effort: no-op if jq is missing or the file is empty/corrupt (claude self-heals).
ensure_onboarding() {
command -v jq >/dev/null 2>&1 || return 0
local cfg="$HOME/.claude.json" ver tmp
ver="$(claude --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
if [ -s "$cfg" ]; then
jq -e . "$cfg" >/dev/null 2>&1 || return 0 # corrupt -> leave for claude
[ "$(jq -r '.hasCompletedOnboarding // false' "$cfg")" = "true" ] && return 0 # already set -> no write
elif [ -e "$cfg" ]; then
return 0 # empty (mid-write?) -> leave it
fi
tmp="$(mktemp "${cfg}.XXXXXX")" || return 0
if [ -f "$cfg" ]; then
jq --arg v "$ver" '.hasCompletedOnboarding = true
| (if $v != "" then .lastOnboardingVersion = $v else . end)' "$cfg" > "$tmp" 2>/dev/null \
&& chmod 600 "$tmp" && mv "$tmp" "$cfg" || rm -f "$tmp"
else
npx @anthropic-ai/claude-code "$@"
jq -n --arg v "$ver" '{hasCompletedOnboarding: true}
+ (if $v != "" then {lastOnboardingVersion: $v} else {} end)' > "$tmp" 2>/dev/null \
&& chmod 600 "$tmp" && mv "$tmp" "$cfg" || rm -f "$tmp"
fi
}
ensure_onboarding
# Deliberately not `exec` so we can branch on the exit code: clean quit ends the
# pane (ttyd closes the terminal); a crash drops to a shell so the tmux session