Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ Three platforms are supported. All changes must account for all three:

When adding a new tool installation, provide install commands for all three platforms.

### Shell configuration

- When persisting `PATH` or env activation to a shell rc file, write to the rc file of the user's **actual login shell** (`$SHELL`) — never hardcode `~/.zshrc`. Bash → `~/.bashrc`, zsh → `~/.zshrc`. Fall back to the OS default shell when `$SHELL` is unset or unrecognized (macOS → zsh, Ubuntu/Arch → bash).
- Use the `login_shell_rc` helper in `lib/common.sh` to resolve the target rc path (it does the `$SHELL` + `$OS`-based selection). See `lib/mise_setup.sh`, `lib/packages_setup.sh`, or `lib/claude_code_setup.sh` for usage.
- On Ubuntu/Arch, login bash sources `~/.bashrc` via `~/.profile` / `~/.bash_profile`, so `~/.bashrc` is the correct target there.
- Writing only to `~/.zshrc` silently fails for bash users on Ubuntu/Omarchy. Resolve the rc via `login_shell_rc` rather than hardcoding `~/.zshrc`.
- Always make rc-file edits idempotent: guard the append with a `grep -qF` for a stable marker.

### Scope boundaries

This repo installs **tool managers, CLI tools, and infrastructure dependencies**:
Expand Down
3 changes: 3 additions & 0 deletions doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ source "$SCRIPT_DIR/lib/render_doctor.sh"
# shellcheck source=lib/registries_doctor.sh
source "$SCRIPT_DIR/lib/registries_doctor.sh"

# shellcheck source=lib/claude_code_doctor.sh
source "$SCRIPT_DIR/lib/claude_code_doctor.sh"

# shellcheck source=lib/migrate_doctor.sh
source "$SCRIPT_DIR/lib/migrate_doctor.sh"

Expand Down
23 changes: 23 additions & 0 deletions lib/build_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ case "$OS" in
echo " NOTE: A dialog may have opened. Complete the installation and re-run this script."
fi

# rust is required for building Ruby from source
if brew list rust > /dev/null 2>&1; then
fmt_ok "rust already installed"
else
fmt_install "rust"
brew install rust
fi

# libyaml is required for building Ruby from source
if brew list libyaml > /dev/null 2>&1; then
fmt_ok "libyaml already installed"
Expand All @@ -32,6 +40,14 @@ case "$OS" in
fmt_install "build-essential"
sudo apt-get install -y -qq build-essential libssl-dev libreadline-dev zlib1g-dev libyaml-dev
fi

# rust is required for building Ruby from source
if dpkg -s rustc > /dev/null 2>&1; then
fmt_ok "rust already installed"
else
fmt_install "rust"
sudo apt-get install -y -qq rustc cargo
fi
;;
arch)
if pacman -Qi base-devel > /dev/null 2>&1; then
Expand All @@ -41,5 +57,12 @@ case "$OS" in
sudo pacman -S --noconfirm --needed base-devel
fi

# rust is required for building Ruby from source
if pacman -Qi rust > /dev/null 2>&1; then
fmt_ok "rust already installed"
else
fmt_install "rust"
sudo pacman -S --noconfirm --needed rust
fi
;;
esac
43 changes: 25 additions & 18 deletions lib/circleci_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,33 @@ fi

fmt_header "CircleCI Authentication"

# Capture diagnostic output into a variable to avoid pipe + pipefail issues.
# With pipefail, a failing left-hand side poisons the whole pipeline even when
# grep succeeds, which causes false negatives.
circleci_diag="$(circleci diagnostic 2>&1 || true)"

if echo "$circleci_diag" | grep -q "OK, got a token"; then
fmt_ok "CircleCI CLI: already authenticated"
# Allow skipping authentication when SKIP_CIRCLECI_AUTH is exactly "1".
# Only the auth step is skipped; the CLI install above always runs and the
# rest of setup.sh continues normally.
if [ "${SKIP_CIRCLECI_AUTH:-}" = "1" ]; then
fmt_ok "CircleCI CLI: authentication skipped (SKIP_CIRCLECI_AUTH=1)"
else
echo " CircleCI CLI needs to be configured."
echo " You will need a personal API token from:"
echo " https://app.circleci.com/settings/user/tokens"
echo ""
circleci setup

# Capture diagnostic output into a variable to avoid pipe + pipefail issues.
# With pipefail, a failing left-hand side poisons the whole pipeline even when
# grep succeeds, which causes false negatives.
circleci_diag="$(circleci diagnostic 2>&1 || true)"
if ! echo "$circleci_diag" | grep -q "OK, got a token"; then

if echo "$circleci_diag" | grep -q "OK, got a token"; then
fmt_ok "CircleCI CLI: already authenticated"
else
echo " CircleCI CLI needs to be configured."
echo " You will need a personal API token from:"
echo " https://app.circleci.com/settings/user/tokens"
echo ""
echo "ERROR: CircleCI CLI authentication failed or was cancelled."
echo "Setup cannot continue without CircleCI access."
exit 1
circleci setup

circleci_diag="$(circleci diagnostic 2>&1 || true)"
if ! echo "$circleci_diag" | grep -q "OK, got a token"; then
echo ""
echo "ERROR: CircleCI CLI authentication failed or was cancelled."
echo "Setup cannot continue without CircleCI access."
exit 1
fi
fmt_ok "CircleCI CLI: authenticated"
fi
fmt_ok "CircleCI CLI: authenticated"
fi
27 changes: 27 additions & 0 deletions lib/claude_code_doctor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash
#
# Doctor check: Claude Code CLI (opt-in).
# Sourced by doctor.sh — do not execute directly.
# Requires: lib/common.sh, doctor helpers (check_pass, check_fail, check_cmd)
#
# This component is OPT-IN. It is only checked when the OPT_IN_CLAUDE_CODE
# environment variable is set to exactly "1". Otherwise it is skipped silently
# so machines that never opted in don't report a spurious failure.

# Opt-in gate. Keep this check here (not in doctor.sh) so the component owns
# its own enablement logic. Only the literal value "1" enables the check.
if [ "${OPT_IN_CLAUDE_CODE:-}" = "1" ]; then
fmt_header "Claude Code (opt-in)"

# The native installer installs to ~/.local/bin, which may not be on PATH in
# a non-interactive shell. Add it so the check below can find claude.
# Read-only — no files are modified.
export PATH="$HOME/.local/bin:$PATH"

check_cmd "claude" "claude"

if cmd_exists claude; then
version_output="$(claude --version 2>&1 | head -1)"
check_pass "claude reports version: $version_output"
fi
fi
55 changes: 55 additions & 0 deletions lib/claude_code_setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash
#
# Claude Code CLI setup (opt-in).
# Sourced by setup.sh — do not execute directly.
# Requires: lib/common.sh
#
# This component is OPT-IN. It is only installed when the OPT_IN_CLAUDE_CODE
# environment variable is set to exactly "1". Any other value (including
# unset, "0", "true", "yes", etc.) is treated as "do not install".

fmt_header "Claude Code (opt-in)"

# Opt-in gate. Keep this check here (not in setup.sh) so the component owns
# its own enablement logic. Only the literal value "1" enables installation.
if [ "${OPT_IN_CLAUDE_CODE:-}" != "1" ]; then
fmt_ok "Claude Code: skipped (set OPT_IN_CLAUDE_CODE=1 to install)"
else
# The native installer puts the claude binary in ~/.local/bin, which is not
# on PATH by default. Persist it to the rc file of the user's actual login
# shell so claude is available in future shells. Idempotent.
claude_rc="$(login_shell_rc)"

if [ ! -f "$claude_rc" ]; then
touch "$claude_rc"
fi

if ! grep -qF '.local/bin' "$claude_rc" 2>/dev/null; then
{
echo ""
echo "# Local user binaries (Claude Code, etc.)"
# shellcheck disable=SC2016 # Intentionally single-quoted: written literally to RC file
echo 'export PATH="$HOME/.local/bin:$PATH"'
} >> "$claude_rc"
echo " Added ~/.local/bin to PATH in $claude_rc"
fi

# Also add it for this session so the cmd_exists checks below work.
export PATH="$HOME/.local/bin:$PATH"

if cmd_exists claude; then
fmt_ok "claude already installed ($(claude --version 2>/dev/null | head -1))"
else
fmt_install "Claude Code CLI"
# Official cross-platform native installer — installs to ~/.local/bin.
# Used on every OS (never Homebrew).
curl -fsSL https://claude.ai/install.sh | bash

if cmd_exists claude; then
fmt_ok "Claude Code installed ($(claude --version 2>/dev/null | head -1))"
else
echo " WARNING: Claude Code was installed but is not yet on PATH."
echo " It will be available after restarting your shell."
fi
fi
fi
22 changes: 22 additions & 0 deletions lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,25 @@ if [ "$OS" = "unsupported" ]; then
echo "Supported: macOS, Ubuntu/Debian, Arch Linux (including Omarchy)."
exit 1
fi

# ---------------------------------------------------------------------------
# Shell configuration
# ---------------------------------------------------------------------------

# Path to the rc file of the user's actual login shell. Used when persisting
# PATH/env so it lands where the login shell will source it (bash -> ~/.bashrc,
# zsh -> ~/.zshrc). Falls back to the OS default shell when $SHELL is unset or
# unrecognized (macOS -> zsh, Linux -> bash). On Ubuntu/Arch login bash sources
# ~/.bashrc via ~/.profile / ~/.bash_profile, so ~/.bashrc is the right target.
login_shell_rc() {
case "$(basename "${SHELL:-}")" in
bash) echo "$HOME/.bashrc" ;;
zsh) echo "$HOME/.zshrc" ;;
*)
case "$OS" in
macos) echo "$HOME/.zshrc" ;;
*) echo "$HOME/.bashrc" ;;
esac
;;
esac
}
30 changes: 30 additions & 0 deletions lib/git_doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,33 @@ if cmd_exists gh; then
check_fail "gh --version returned unexpected output: $version_output"
fi
fi

# ---------------------------------------------------------------------------
# git credential helper
# ---------------------------------------------------------------------------

fmt_header "git credential helper"

if cmd_exists git && cmd_exists gh; then
if git config --get-regexp '^credential\..*github\.com.*\.helper$' 2>/dev/null | grep -q 'gh'; then
check_pass "git is configured to authenticate GitHub HTTPS via gh"
else
check_fail "git credential helper for github.com not set — run 'gh auth setup-git'"
fi
fi

# ---------------------------------------------------------------------------
# git identity
# ---------------------------------------------------------------------------

fmt_header "git identity"

if cmd_exists git; then
git_name="$(git config --global user.name || true)"
git_email="$(git config --global user.email || true)"
if [ -n "$git_name" ] && [ -n "$git_email" ]; then
check_pass "git identity set ($git_name <$git_email>)"
else
check_fail "git identity incomplete — set user.name and user.email globally"
fi
fi
50 changes: 50 additions & 0 deletions lib/git_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,53 @@ else
fi
fmt_ok "Authenticated with GitHub"
fi

# ---------------------------------------------------------------------------
# git credential helper (use gh for HTTPS GitHub auth)
# ---------------------------------------------------------------------------
#
# Without this, git over HTTPS (e.g. `git pull` on a repo cloned via
# `gh repo clone`) prompts for a username/password even when `gh` is
# authenticated. `gh auth setup-git` wires git's credential helper to
# `gh auth git-credential` for github.com. Idempotent.

fmt_header "git credential helper"

gh auth setup-git
fmt_ok "Configured git to authenticate via gh"

# ---------------------------------------------------------------------------
# git identity (global user.name / user.email)
# ---------------------------------------------------------------------------
#
# Derive the identity from the authenticated GitHub account. Only fill in
# values that are not already set globally — never clobber an identity the
# user configured deliberately.

fmt_header "git identity"

git_name="$(git config --global user.name || true)"
git_email="$(git config --global user.email || true)"

if [ -n "$git_name" ] && [ -n "$git_email" ]; then
fmt_ok "git identity already set ($git_name <$git_email>)"
else
gh_login="$(gh api user --jq '.login')"
gh_id="$(gh api user --jq '.id')"

if [ -z "$git_name" ]; then
git_name="$(gh api user --jq '.name // ""')"
# Display name is optional on GitHub -> fall back to the login.
[ -n "$git_name" ] || git_name="$gh_login"
git config --global user.name "$git_name"
fi

if [ -z "$git_email" ]; then
git_email="$(gh api user --jq '.email // ""')"
# Public email is often hidden -> fall back to the noreply address.
[ -n "$git_email" ] || git_email="${gh_id}+${gh_login}@users.noreply.github.com"
git config --global user.email "$git_email"
fi

fmt_ok "git identity set ($git_name <$git_email>)"
fi
16 changes: 9 additions & 7 deletions lib/mise_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,18 @@ else
esac
fi

# Ensure mise is activated in ~/.zshrc
if [ ! -f "$HOME/.zshrc" ]; then
touch "$HOME/.zshrc"
fi
# Ensure mise is activated in the user's login shell rc
mise_rc="$(login_shell_rc)"
[ -f "$mise_rc" ] || touch "$mise_rc"

if ! grep -qF "mise activate" "$HOME/.zshrc" 2>/dev/null; then
if ! grep -qF "mise activate" "$mise_rc" 2>/dev/null; then
{
echo ""
echo "# mise version manager"
# shellcheck disable=SC2016 # Intentionally single-quoted: written literally to RC file
echo 'eval "$(mise activate)"'
} >> "$HOME/.zshrc"
echo " Added mise activation to ~/.zshrc"
} >> "$mise_rc"
echo " Added mise activation to $mise_rc"
fi

# Activate mise for this session
Expand All @@ -60,6 +59,9 @@ fmt_header "Ruby (via mise)"
if mise which ruby > /dev/null 2>&1; then
fmt_ok "Ruby already available via mise"
else
echo 'setting mise ruby.compile=false'
mise settings ruby.compile=false

fmt_install "Ruby (latest stable via mise)"
mise use --global ruby@latest
fmt_ok "Ruby installed: $(mise exec -- ruby --version)"
Expand Down
13 changes: 6 additions & 7 deletions lib/packages_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,19 @@ case "$OS" in
fi
fi

# Ensure Homebrew activation is persisted in ~/.zshrc
# Ensure Homebrew activation is persisted in the user's login shell rc
if cmd_exists brew; then
if [ ! -f "$HOME/.zshrc" ]; then
touch "$HOME/.zshrc"
fi
brew_rc="$(login_shell_rc)"
[ -f "$brew_rc" ] || touch "$brew_rc"

if ! grep -qF "brew shellenv" "$HOME/.zshrc"; then
if ! grep -qF "brew shellenv" "$brew_rc"; then
{
echo ""
echo "# Homebrew"
# shellcheck disable=SC2016 # Intentionally single-quoted: written literally to RC file
echo 'eval "$(brew shellenv)"'
} >> "$HOME/.zshrc"
echo " Added Homebrew activation to ~/.zshrc"
} >> "$brew_rc"
echo " Added Homebrew activation to $brew_rc"
fi
fi
;;
Expand Down
Loading
Loading