infra/scripts/workstation/skel/start-claude.sh
Viktor Barzin eecd78233b
All checks were successful
ci/woodpecker/push/default Pipeline was successful
workstation: standardize on the native claude install (drop npm-global + npx)
Question from Viktor: should claude run via the binary or npx? Answer: the native
install is the recommended runtime (self-contained, self-updating ~/.local/bin/claude;
installMethod=native) — and every existing user had already auto-migrated to it, leaving
the npm-global copy empty and the npx fallback dead. "Leave only the recommended setup":

- setup-devvm.sh: node is now installed ONLY for the t3 CLI; dropped the machine-wide
  `npm install -g @anthropic-ai/claude-code` (npm/npx is not the recommended runtime and
  just shadowed the per-user native installs).
- t3-provision-users.sh: new per-user `install_user_claude_native` (runs the official
  https://claude.ai/install.sh AS the user, idempotent/skip-if-present) — provisions native
  claude for BOTH the terminal launcher and each t3-serve instance, replacing the npm bootstrap.
- skel/start-claude.sh: launcher runs the native `claude` only; if missing it bootstraps via
  the native installer (was an `npx @anthropic-ai/claude-code` fallback).
- docs/architecture/multi-tenancy.md: documented the native-only runtime model.

node stays (the pinned t3 CLI is npm-global). Verified: native installer reachable +
produces ~/.local/bin/claude 2.1.177; all three scripts pass bash -n + shellcheck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:12:05 +00:00

101 lines
4.8 KiB
Bash
Executable file

#!/bin/bash
# Per-user Claude Code Workstation launcher (devvm). Lands the user in their OWN
# ~/code clone (NOT a hardcoded /home/wizard/code) and names the Claude session
# after the tmux session so /resume, the prompt box, and the terminal title line
# up. Deployed via /etc/skel by setup-devvm.sh, so new accounts get it on
# `useradd -m`. Existing users are repointed to this during their migration.
echo ""
echo " Welcome, $(id -un)! 🚀"
echo ""
echo " Starting Claude Code in $HOME/code ..."
echo " (Right-click for tmux menu, or Ctrl+B then | or - to split)"
echo ""
name_args=()
if [ -n "${TMUX:-}" ]; then
sess="$(tmux display-message -p '#{session_name}' 2>/dev/null)"
[ -n "$sess" ] && name_args=(--name "$sess")
fi
cd "$HOME/code" 2>/dev/null || cd "$HOME"
# Freshen the user's clone(s) at session start so they begin on current upstream
# state (the hourly t3-provision-users reconcile does the same in the background).
# Single layout freshens ~/code itself; workspace layout freshens each repo under
# ~/code. Fast-forward only, and only when safe (on master + clean tree); hard
# 10s fetch cap per repo so an offline remote never stalls the launch.
freshen_repo() {
GIT_TERMINAL_PROMPT=0 timeout 10 git -C "$1" fetch --all --prune --quiet 2>/dev/null || true
# ff whatever branch is checked out (master, main, ...) when that is provably
# safe: on a branch, clean tree, upstream configured. Never rebases/merges.
if [ -n "$(git -C "$1" symbolic-ref --short -q HEAD)" ] \
&& [ -z "$(git -C "$1" status --porcelain 2>/dev/null)" ] \
&& git -C "$1" rev-parse --verify -q '@{upstream}' >/dev/null 2>&1; then
git -C "$1" merge --ff-only '@{upstream}' >/dev/null 2>&1 || true
fi
}
if [ -d "$HOME/code/.git" ]; then
freshen_repo "$HOME/code"
else
for repo_git in "$HOME"/code/*/.git; do
[ -d "$repo_git" ] && freshen_repo "${repo_git%/.git}"
done
fi
# 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
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
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
# isn't destroyed-and-recreated in a ttyd auto-reconnect loop.
# No --model flag: inherit the org-wide default from /etc/claude-code/managed-settings.json
# (an explicit --model would override that managed default for every launched session).
launch --dangerously-skip-permissions "${name_args[@]}"
code=$?
[ "$code" -eq 0 ] && exit 0
echo ""
echo " claude exited abnormally (status $code). Dropping to a shell — your tmux session is preserved."
echo " Re-launch any time with: ~/start-claude.sh"
echo ""
exec "${SHELL:-/bin/bash}" -l