port the shell section, and make one starship config serve both audiences

os-user-shell.ts:33 calls scripts/setup/starship.toml "the prompt config the
owner's own install uses — one file, both audiences". It was not: the original
machine script wrote a DIFFERENT config inline, so the owner got a prompt that
only disabled language modules while every member got the repo file with its
custom format. Two prompts, one comment claiming otherwise.

This deploys the same file the platform does, which makes the comment true.
Verified with cmp against a fresh account: byte-identical to what a member gets.

Nothing overwrites any more:

  .config/starship.toml and .tmux.conf go through install_config, so they are
  written when absent, skipped when identical, and KEPT when they differ — with
  the cp printed, so taking ours stays the reader's decision. On this host that
  is what happens: the existing config differs and is left alone.

  The starship line in .zshrc is marker-wrapped by append_once. Verified over
  three consecutive runs: one block, not three. The original appended it
  unguarded every time.

The login shell is now its own question. Having zsh on the machine and being
handed it at every login are different decisions, and `chsh` made the second one
silently. It also adds the shell to /etc/shells first, which chsh requires.

.tmux.conf lives here now, with the rest of the dotfiles, rather than in user
creation where the original put it only because that is where $USER_HOME first
exists.

One bug found by running it: install_config returns 2 for "kept yours", which is
an outcome rather than a failure — but still non-zero, so calling it as a plain
command under `set -e` ended the run before `case $?` could read it. Captured with
&& / || at both call sites, and the contract is documented where the function is
defined so the next caller does not repeat it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 20:45:25 +00:00
co-authored by Claude Opus 5
parent 03cc01a135
commit 2b9e16c11e
3 changed files with 144 additions and 1 deletions
+37
View File
@@ -62,3 +62,40 @@ ask_required() {
done
printf -v "$__var" '%s' "$answer"
}
# -----------------------------------------------------------------------------
# Shell
# -----------------------------------------------------------------------------
#
# ── One starship config, not two ──
#
# The platform deploys scripts/setup/starship.toml into every member's home
# (os-user-shell.ts), and the comment there calls it "the prompt config the
# owner's own install uses — one file, both audiences". That was not true: the
# original machine script wrote a DIFFERENT config inline, so the owner got one
# prompt and every member got another. This deploys the same file the platform
# does, which makes the comment true rather than aspirational.
#
# It lives one directory up because it is shared with the platform, not owned by
# this script.
STARSHIP_SRC="${STARSHIP_SRC:-$SCRIPT_DIR/../starship.toml}"
user_login_shell() { getent passwd "$USERNAME" | cut -d: -f7; }
oh_my_zsh_installed() { [[ -d "${USER_HOME}/.oh-my-zsh" ]]; }
install_oh_my_zsh() {
# The installer refuses to run unattended over an existing install, so this is
# only ever called when there is none.
sudo -H -u "$USERNAME" sh -c \
"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended >/dev/null 2>&1
}
# `chsh` is what actually changes the login shell. Asked separately from
# installing zsh, because having a shell available and being handed it at every
# login are different decisions.
set_login_shell() {
local shell="$1"
grep -qxF "$shell" /etc/shells || echo "$shell" >>/etc/shells
chsh -s "$shell" "$USERNAME"
}
+6
View File
@@ -30,6 +30,12 @@ MACHINE_SETUP_FILES_LOADED=1
# Converging when there is nothing to lose and keeping what the user wrote when
# there is. The third case prints how to take ours, so the choice stays with
# them.
#
# NOTE for callers: 1 and 2 are outcomes, not failures — but they are still
# non-zero, so calling this as a plain command under `set -e` ends the script
# before the result can be read. Always capture it:
#
# install_config "$src" "$dest" "$user" && rc=0 || rc=$?
install_config() {
local src="$1" dest="$2" owner="$3"
+101 -1
View File
@@ -1753,13 +1753,113 @@ if ! skip; then
step_ok
fi
# =============================================================================
# 23. Shell
# =============================================================================
#
# zsh, oh-my-zsh, the starship prompt, and the dotfiles that go with them. The
# .tmux.conf lives here rather than in user creation, where the original put it
# only because that is where $USER_HOME first exists.
step "Shell"
if ! skip; then
SHELL_NOW="$(user_login_shell)"
echo ""
info "Shell — what ${USERNAME} gets at every login"
echo " login shell: ${SHELL_NOW}"
echo " zsh: $(command -v zsh &>/dev/null && echo 'installed' || echo 'not installed')"
echo " oh-my-zsh: $(oh_my_zsh_installed && echo 'installed' || echo 'not installed')"
echo " starship: $(command -v starship &>/dev/null && echo 'installed' || echo 'not installed')"
# ── zsh and oh-my-zsh ──
if ! command -v zsh &>/dev/null || ! oh_my_zsh_installed; then
echo ""
echo " oh-my-zsh is a configuration framework for zsh: completions, a"
echo " plugin system, and sensible history behaviour out of the box."
if confirm "Install zsh and oh-my-zsh?"; then
command -v zsh &>/dev/null || pkg_install_now zsh
if ! oh_my_zsh_installed; then
install_oh_my_zsh
oh_my_zsh_installed && ok "oh-my-zsh installed" || warn "oh-my-zsh did not install"
fi
SUMMARY+=("Shell: zsh and oh-my-zsh installed")
else
warn "skipped by request"
SUMMARY+=("Shell: SKIPPED by request")
fi
fi
# ── the login shell, asked separately ──
#
# Having zsh on the machine and being handed it at every login are different
# decisions, and the original made the second one silently.
if command -v zsh &>/dev/null && [[ "$SHELL_NOW" != *zsh ]]; then
echo ""
echo " ${USERNAME}'s login shell is ${SHELL_NOW}. Changing it to zsh takes"
echo " effect at the next login, and does not affect this session."
if confirm "Make zsh the login shell?"; then
set_login_shell "$(command -v zsh)"
ok "login shell is now $(user_login_shell)"
SUMMARY+=("Shell: login shell set to zsh")
else
warn "left as ${SHELL_NOW}"
SUMMARY+=("Shell: login shell left as ${SHELL_NOW}")
fi
fi
# ── dotfiles, none of which overwrite ──
if id "$USERNAME" &>/dev/null; then
echo ""
info " shell configuration"
# The prompt config the platform also deploys to every member. install_config
# keeps whatever is already there if it differs.
if [[ -r "$STARSHIP_SRC" ]]; then
# && / || rather than a bare call: 2 means "kept yours", which is an
# outcome and not a failure, but is still non-zero and would end the run.
install_config "$STARSHIP_SRC" "${USER_HOME}/.config/starship.toml" "$USERNAME" && RC=0 || RC=$?
case $RC in
0) ok "starship config installed — the same one members get" ;;
1) echo " starship config already matches" ;;
esac
fi
if [[ -r "$SCRIPT_DIR/.tmux.conf" ]]; then
install_config "$SCRIPT_DIR/.tmux.conf" "${USER_HOME}/.tmux.conf" "$USERNAME" && RC=0 || RC=$?
case $RC in
0) ok "tmux config installed" ;;
1) echo " tmux config already matches" ;;
esac
fi
# Marker-wrapped, so a second run recognises its own work instead of adding
# it again. The original appended this unguarded on every pass.
if command -v starship &>/dev/null; then
ZSHRC="${USER_HOME}/.zshrc"
touch "$ZSHRC"
chown "$USERNAME:$USERNAME" "$ZSHRC"
if append_once "$ZSHRC" starship <<'EOF'
eval "$(starship init zsh)"
EOF
then
ok "starship added to .zshrc"
else
echo " starship already in .zshrc"
fi
fi
SUMMARY+=("Shell: prompt and dotfiles in place")
fi
step_ok
fi
# =============================================================================
# NOT PORTED YET
# =============================================================================
#
# Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order:
#
# zsh + prompt (incl. .tmux.conf) · neovim · js runtimes ·
# neovim · js runtimes ·
# dev tools · ufw · zshrc
#
# And one that is new rather than ported, to come last of all: