diff --git a/scripts/setup/machine-setup/lib/dev.sh b/scripts/setup/machine-setup/lib/dev.sh index 5aa790d8..e649c44e 100644 --- a/scripts/setup/machine-setup/lib/dev.sh +++ b/scripts/setup/machine-setup/lib/dev.sh @@ -347,3 +347,35 @@ install_opencode() { } install_pi() { npm install -g @mariozechner/pi-coding-agent >/dev/null 2>&1; } + +# ----------------------------------------------------------------------------- +# Default editor +# ----------------------------------------------------------------------------- +# +# One preference, two mechanisms, and both are needed: +# +# EDITOR / VISUAL what the account's own shell hands to git, crontab -e, +# systemctl edit and anything else that opens an editor +# update-alternatives the system-wide `editor` command, which is what root and +# `sudoedit` use — an account's shell config cannot reach +# those +# +# This is the setting core.editor was deliberately left out in favour of: set it +# here and git follows, along with everything else. + +editor_candidates() { + local e + for e in nvim vim nano; do command -v "$e" &>/dev/null && echo "$e"; done +} + +current_editor() { (cd / && sudo -H -u "$USERNAME" bash -lc 'echo "${EDITOR:-}"' 2>/dev/null); } + +set_system_editor() { + local editor="$1" path + path="$(command -v "$editor")" || return 1 + # Only where the alternatives system is in use. Absent on non-Debian systems, + # where there is nothing to set. + command -v update-alternatives &>/dev/null || return 0 + update-alternatives --install /usr/bin/editor editor "$path" 100 >/dev/null 2>&1 + update-alternatives --set editor "$path" >/dev/null 2>&1 +} diff --git a/scripts/setup/machine-setup/lib/files.sh b/scripts/setup/machine-setup/lib/files.sh index 31e085a3..805ff124 100644 --- a/scripts/setup/machine-setup/lib/files.sh +++ b/scripts/setup/machine-setup/lib/files.sh @@ -103,6 +103,12 @@ install_config() { # EOF # # Returns 0 if it wrote, 1 if the block was already there. +# +# One limitation, and it bites the author rather than the user: RENAMING a marker +# orphans the block that used the old name. append_once only recognises the name +# it is given, so the previous block stays in the file doing whatever it did. +# Changing a block's CONTENT has the same shape — the marker is found, so the new +# content is never written. Both need the old block removed by hand. append_once() { local file="$1" name="$2" local begin="# >>> machine-setup: ${name} >>>" diff --git a/scripts/setup/machine-setup/machine-setup.sh b/scripts/setup/machine-setup/machine-setup.sh index 6ac4674b..c816bdad 100755 --- a/scripts/setup/machine-setup/machine-setup.sh +++ b/scripts/setup/machine-setup/machine-setup.sh @@ -1754,107 +1754,7 @@ if ! skip; then 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 - -# ============================================================================= -# 24. Neovim +# 23. Neovim # ============================================================================= step "Neovim" @@ -1950,7 +1850,7 @@ if ! skip; then fi # ============================================================================= -# 25. JavaScript runtimes +# 24. JavaScript runtimes # ============================================================================= # # Not offered as a choice. Officer does not run without these, so asking would be @@ -2063,7 +1963,7 @@ if ! skip; then fi # ============================================================================= -# 26. Agent CLIs +# 25. Agent CLIs # ============================================================================= # # claude and opencode are what the chat sidecars spawn, so they are installed @@ -2133,26 +2033,6 @@ if ! skip; then fi fi - # ~/.local/bin has to be on PATH for the account's own shell. The sidecars find - # claude without it — claude-manager.ts checks that directory explicitly — but - # a user who cannot run `claude` in their own terminal reasonably concludes it - # was never installed. - if id "$USERNAME" &>/dev/null; then - ZSHRC="${USER_HOME}/.zshrc" - touch "$ZSHRC" - chown "$USERNAME:$USERNAME" "$ZSHRC" - # Both directories, because the two installers disagree about where they put - # things. The sidecars find their binaries without this — they check the - # exact paths — but a user who cannot run `claude` in their own terminal - # reasonably concludes it was never installed. - if append_once "$ZSHRC" agent-clis <<'EOF' -export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$PATH" -EOF - then - ok "~/.local/bin and ~/.opencode/bin added to ${USERNAME}'s PATH" - fi - fi - # ── PI, which nothing here uses ── echo "" if command -v pi &>/dev/null; then @@ -2176,6 +2056,173 @@ EOF step_ok fi +# ============================================================================= +# 26. 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 + + # Everything below writes .zshrc, and every block is marker-wrapped so a + # second run recognises its own work. The original appended all of it + # unguarded, so a re-run duplicated the lot. + ZSHRC="${USER_HOME}/.zshrc" + touch "$ZSHRC" + chown "$USERNAME:$USERNAME" "$ZSHRC" + + if command -v starship &>/dev/null; then + if append_once "$ZSHRC" starship <<'EOF' +eval "$(starship init zsh)" +EOF + then + ok "starship added to .zshrc" + fi + fi + + # The agent CLIs install to two different directories, neither of which is on + # PATH by default. The sidecars find them regardless — they check the exact + # paths — but a user who cannot run `claude` in their own terminal reasonably + # concludes it was never installed. + if append_once "$ZSHRC" agent-clis <<'EOF' +export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$PATH" +EOF + then + ok "~/.local/bin and ~/.opencode/bin added to PATH" + fi + + if append_once "$ZSHRC" aliases <<'EOF' +alias sz="source ~/.zshrc" +EOF + then + ok "shell aliases added" + fi + + SUMMARY+=("Shell: prompt and dotfiles in place") + fi + + # ── the default editor ── + echo "" + info "Default editor — what opens when anything needs you to type something" + echo " git commit, crontab -e, systemctl edit, sudoedit. One preference," + echo " which is why git's own core.editor is deliberately not set: git" + echo " falls back to \$EDITOR, so setting it here covers everything." + echo "" + mapfile -t EDITORS < <(editor_candidates) + if ((${#EDITORS[@]} == 0)); then + warn "no editor found to offer — skipping" + else + EDITOR_NOW="$(current_editor)" + echo " currently: ${EDITOR_NOW:-not set}" + for i in "${!EDITORS[@]}"; do + printf ' [%d] %s\n' "$((i + 1))" "${EDITORS[$i]}" + done + echo "" + + EDITOR_PICK="" + while [[ -z "$EDITOR_PICK" ]]; do + if ! read -rp " Which one? (1-${#EDITORS[@]}) [1]: " EDITOR_CHOICE; then + echo "" + fail "No answer." + fi + EDITOR_CHOICE="${EDITOR_CHOICE:-1}" + if [[ "$EDITOR_CHOICE" =~ ^[0-9]+$ ]] && ((EDITOR_CHOICE >= 1 && EDITOR_CHOICE <= ${#EDITORS[@]})); then + EDITOR_PICK="${EDITORS[$((EDITOR_CHOICE - 1))]}" + else + warn "Pick a number from the list." + fi + done + + # Written to the account's shell, and set as the system `editor` alternative + # so root and sudoedit agree with it. + if append_once "$ZSHRC" editor <