Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d4fc0d9
fixes for macOS
entpiske Jun 11, 2026
4cf0a6f
Install rust on Ubuntu and Arch for building Ruby
entpiske Jun 11, 2026
1ab17dc
Allow skipping CircleCI auth with SKIP_CIRCLECI_AUTH=1
entpiske Jun 15, 2026
88e03c7
Simplify CircleCI auth skip to plain if/else
entpiske Jun 15, 2026
e9796df
Re-exec a fresh login shell at end of setup
entpiske Jun 15, 2026
e73c16f
Use exec newgrp docker instead of su to avoid password prompt
entpiske Jun 15, 2026
1dd13d9
Re-exec a login shell so shell config and docker group both apply
entpiske Jun 15, 2026
c4bcb81
Add opt-in Claude Code CLI setup and doctor checks
entpiske Jun 15, 2026
65f04ae
Persist ~/.local/bin to the login shell rc, not always ~/.zshrc
entpiske Jun 15, 2026
3732784
Document shell-rc selection rule in AGENTS.md
entpiske Jun 15, 2026
c86de21
Write mise/brew shell config to the login shell rc, not always ~/.zshrc
entpiske Jun 15, 2026
c0e6e52
Wire git HTTPS auth to gh via gh auth setup-git
entpiske Jun 16, 2026
49b264e
Set global git identity from the authenticated GitHub account
entpiske Jun 16, 2026
3ae2823
Set HOMEBREW_NO_ASK=1 so brew never prompts during setup
entpiske Jul 22, 2026
d20ae77
Install Xcode Command Line Tools first on macOS
entpiske Jul 22, 2026
95872f3
Run Homebrew installer non-interactively
entpiske Jul 22, 2026
7d3b377
Prime sudo for the non-interactive Homebrew install
entpiske Jul 22, 2026
193385c
Require the 1Password desktop app integration on macOS
entpiske Jul 22, 2026
e6908de
Drop Xcode Command Line Tools install from build essentials
entpiske Jul 22, 2026
43ebb5a
Stop corepack from hanging on the yarn download prompt
entpiske Jul 22, 2026
2bfda2f
Gate 1Password auth on op account list, not op signin
entpiske Jul 22, 2026
26a0cb6
Replace login-shell handoff with a restart notice
entpiske Jul 23, 2026
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
47 changes: 47 additions & 0 deletions lib/1password_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,53 @@ fmt_header "1Password Authentication"

if op whoami > /dev/null 2>&1; then
fmt_ok "1Password: already signed in"
elif [ "$OS" = "macos" ]; then
# On macOS, op must authenticate through the 1Password desktop app's CLI
# integration — NOT manually-added accounts or service accounts. When that
# integration is off, `op signin` drops into an interactive "add an account
# manually? [Y/n]" prompt that it reads from /dev/tty directly — so it can't
# be answered by piping stdin, and setup hangs on it. We must never reach it.
#
# Instead, gate on `op account list`, which only enumerates configured
# accounts and never prompts:
# - integration OFF => no accounts listed. Ask the user to enable it, retry.
# - integration ON => the app's account(s) are listed.
# Only once an account exists do we call `op signin`, which then triggers the
# desktop Authorize popup (a GUI dialog, not a tty prompt): exit 0 =
# Authorized, non-zero = Cancelled.
echo " 1Password CLI authenticates through the 1Password desktop app."
echo ""

# Wait until the desktop-app CLI integration exposes at least one account.
while true; do
accounts="$(op account list --format=json 2>/dev/null || true)"
accounts="$(printf '%s' "$accounts" | tr -d '[:space:]')"
if [ -n "$accounts" ] && [ "$accounts" != "[]" ]; then
break
fi

echo " ${COLOR_YELLOW}The 1Password CLI app integration is not enabled.${COLOR_RESET}"
echo ""
echo " In the 1Password desktop app:"
echo " 1. Open Settings (Cmd + ,)"
echo " 2. Select the Developer tab"
echo " 3. Enable \"Integrate with 1Password CLI\""
echo ""
read -r -p " Press Return once it's enabled to try again... " < /dev/tty
echo ""
done

# Integration is on and an account is available. Trigger the Authorize popup;
# the user must click Authorize (Cancel => non-zero exit, so we retry).
until op signin -f > /dev/null 2>&1; do
echo " ${COLOR_YELLOW}1Password authorization was not completed.${COLOR_RESET}"
echo " When the 1Password popup appears, click \"Authorize\" (not Cancel)."
echo ""
read -r -p " Press Return to try again... " < /dev/tty
echo ""
done

fmt_ok "1Password: signed in"
else
echo " 1Password CLI needs to be authenticated."
echo " This will open a browser/system auth prompt."
Expand Down
26 changes: 20 additions & 6 deletions lib/build_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ fmt_header "Build Essentials"

case "$OS" in
macos)
# Xcode Command Line Tools are required for native compilation
if xcode-select -p > /dev/null 2>&1; then
fmt_ok "Xcode Command Line Tools already installed"
# 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 "Xcode Command Line Tools"
xcode-select --install 2>/dev/null || true
echo " NOTE: A dialog may have opened. Complete the installation and re-run this script."
fmt_install "rust"
brew install rust
fi

# libyaml is required for building Ruby from source
Expand All @@ -32,6 +31,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 +48,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
Loading
Loading