Skip to content
Merged
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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ If you touch provisioning code, preserve these:
- `require_debian` inspects `/etc/os-release` for `ID=debian` or Debian-like `ID_LIKE`, matching `install/lib.sh`. The presence of `apt-get` is not OS detection.
- Standalone modules cannot assume `base.sh` ran; pull prerequisites with `ensure_cmd <cmd> [pkg]`.
- `install/debian.sh` and `provision/workflow.sh` carry overlapping package lists on purpose. `install.sh` has to work on a Debian box that never ran `provision/`, and the suite stays standalone from `install/` (same reason `provision/lib.sh` duplicates `is_yes`). The drift is accepted; keep the two in sync by hand.
- Debian renames `bat` to `batcat` and `fd` to `fdfind`. Wrapper links go in `~/.local/bin`, never `/usr/local/bin` — no sudo, and it matches `install/debian.sh` so the two paths cannot leave conflicting links behind. A link is only written when the destination is absent or is already our own link: `ln -sf` unlinks first, and `has_cmd` cannot see a binary the user installed there while `~/.local/bin` is off `PATH`, so an unguarded link destroys it.
- Debian renames `bat` to `batcat` and `fd` to `fdfind`. Wrapper links go in `~/.local/bin`, never `/usr/local/bin` — no sudo, and it matches `install/debian.sh` so the two paths cannot leave conflicting links behind. A link is only written when the destination is absent or is already our own link (`ln -sf` unlinks first, so an unguarded link destroys whatever the user put there). That destination check runs *before* the "distro already ships the real name" `has_cmd` skip: `lib.sh` puts `~/.local/bin` on `PATH`, so our own wrapper — or the user's own binary — satisfies `has_cmd bat`, and checking in the other order would silently skip instead of reporting.
- `lib.sh` prepends `~/.local/bin` to `PATH` at source time, so a module sees what an earlier one installed (`claude`, the `bat`/`fd` wrappers) without a new login shell — `run-all.sh` runs modules as children, which inherit it. Interactive shells get the same path from Debian's `~/.profile` and `.zsh/common.zsh`.
- Each module ends by running what it installed through `show_versions` (`lib.sh`), which prints `cmd: <first line of version> (<resolved path>)` or warns when the command is not on `PATH` — an installed-but-unreachable binary has to fail loudly. It tries `--version`, `-V`, `-v` in order and takes the first clean exit (`tmux` needs `-V`, `dig` needs `-v`); `tailscale` is done by hand because its CLI has no version *flag* at all, only a `version` subcommand.
- Packages that exist on only some Debian-like releases go through `apt_install_optional`, never the main `apt_install` list. `require_debian` accepts bookworm, trixie and Ubuntu; `lazygit` is trixie-only, and one missing package in a batched `apt-get install -y` exits 100 and takes the module — and `run-all.sh` — down under `set -e`.
- `base.sh` installs `build-essential` rather than bare `make`: gcc/g++/libc6-dev are what native builds (node-gyp, Python wheels, nvim tree-sitter) actually need, and it supersedes `make`. No clang — nothing in the suite uses it.
- `sshd.sh` must refuse to disable password auth without a verified escape hatch: an `authorized_keys` that `ssh-keygen -l` parses **and** effective `PubkeyAuthentication yes`, or Tailscale SSH up. Fail closed.
Expand Down
3 changes: 3 additions & 0 deletions provision/base.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ require_sudo
apt_update
apt_install "${PACKAGES[@]}"
log_ok "Base packages installed"

log_info "Versions"
show_versions curl wget git gpg gcc xz dig
10 changes: 3 additions & 7 deletions provision/claude.sh
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,7 @@ else
log_warn "claude at $claude_cmd is not a native install under $VERSIONS_DIR; skipping verification"
fi

"$claude_cmd" --version
log_ok "claude is installed"

# .zsh/common.zsh already puts ~/.local/bin on PATH; other shells may not.
case ":${PATH}:" in
*":$HOME/.local/bin:"*) ;;
*) log_warn "$HOME/.local/bin is not on this shell's PATH; start a new login shell to pick up claude" ;;
esac
# Resolved through PATH, not $claude_cmd: this doubles as the check that a
# fresh install is actually reachable by name (lib.sh adds ~/.local/bin).
show_versions claude
2 changes: 1 addition & 1 deletion provision/gh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ log_ok "GitHub CLI apt repo configured"

apt_update
apt_install gh
gh --version
log_ok "gh installed"
show_versions gh
32 changes: 32 additions & 0 deletions provision/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,35 @@ apt_install_optional() {

# Standalone module runs can't assume base.sh ran first. ensure_cmd <cmd> [pkg]
ensure_cmd() { has_cmd "$1" || apt_install "${2:-$1}"; }

# Report what a module installed: proof each command resolves on PATH and runs.
# The version flag is not universal (tmux wants -V, dig wants -v), so try them
# in turn and take the first that exits clean.
show_versions() {
local cmd flag out
for cmd in "$@"; do
if ! has_cmd "$cmd"; then
log_warn "$cmd is not on PATH"
continue
fi
out="?"
for flag in --version -V -v; do
if out="$("$cmd" "$flag" 2>&1)"; then
out="$(head -n1 <<< "$out")"
break
fi
out="?"
done
log_ok "$cmd: $out ($(command -v "$cmd"))"
done
}

# Modules install into ~/.local/bin (claude, and the bat/fd wrappers), which is
# not on a fresh Debian's non-login PATH. Prepend it here, at source time, so a
# later module sees what an earlier one installed without waiting for a new
# login shell — run-all.sh's children inherit this. Interactive shells get it
# from ~/.profile (Debian) or .zsh/common.zsh.
case ":${PATH}:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
3 changes: 3 additions & 0 deletions provision/tailscale.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ else
log_ok "tailscale installed"
fi

# `tailscale version`, not show_versions: the CLI has no --version flag.
log_ok "tailscale: $(tailscale version | head -n1) ($(command -v tailscale))"

if tailscale status > /dev/null 2>&1; then
sudo tailscale set --ssh=true
log_warn "tailscale already up; ensured SSH is enabled"
Expand Down
17 changes: 13 additions & 4 deletions provision/test/inside.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ for want in bat fd; do
|| fail "workflow.sh did not create a usable ~/.local/bin/$want wrapper"
done

# Every module reports a version per tool it installed — the proof that each one
# is on PATH and runnable, not merely unpacked.
out="$(./provision/workflow.sh < /dev/null)"
for cmd in nvim tmux mosh rg fzf bat fd jq; do
grep -q "$cmd: " <<< "$out" || fail "workflow.sh did not report a $cmd version"
done
! grep -q ': ?' <<< "$out" || fail "workflow.sh could not read some version"

# A package this release does not carry must warn, not abort under set -e.
# lazygit is the real case (trixie has it, bookworm and Ubuntu do not).
(
Expand Down Expand Up @@ -78,12 +86,13 @@ out="$(./provision/claude.sh < /dev/null)"
grep -q "already installed" <<< "$out" \
|| fail "claude.sh rerun did not take the skip path"

# ...including when ~/.local/bin is off PATH, which is the state right after
# a fresh install in the same shell.
# ...including when the caller's PATH lacks ~/.local/bin, which is the state
# right after a fresh install in the same shell. lib.sh must repair PATH so the
# module finds claude by name rather than by hardcoded path.
out="$(env PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
./provision/claude.sh < /dev/null)"
grep -q "not on PATH" <<< "$out" \
|| fail "claude.sh did not detect the off-PATH install"
grep -q "already installed at $HOME/.local/bin/claude" <<< "$out" \
|| fail "lib.sh did not put ~/.local/bin on PATH for a caller that lacks it"

# A bypass flag set to a value that reads as "off" must keep verification on.
out="$(PROVISION_SKIP_CLAUDE_VERIFY=0 ./provision/claude.sh < /dev/null)"
Expand Down
24 changes: 13 additions & 11 deletions provision/workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,27 @@ link_wrapper() {
# Separate statement: local expands every argument before assigning any of
# them, so a dest that reads $want on the same line trips set -u.
dest="$HOME/.local/bin/$want"
# Guard on the real name: a distro that ships `bat` as bat needs no wrapper.
if has_cmd "$want" || ! has_cmd "$have"; then
return 0
fi
# Nothing to bridge if the distro never renamed it.
has_cmd "$have" || return 0
src="$(command -v "$have")"
# ln -sf unlinks whatever is there first, and has_cmd cannot see a binary the
# user installed here while ~/.local/bin is off PATH — exactly the case this
# script warns about. Only ever replace nothing, or our own link.
# ln -sf unlinks whatever is there first, so check before writing: only ever
# replace nothing, or our own link. Checked before the has_cmd guard below —
# lib.sh puts ~/.local/bin on PATH, so a binary the user dropped here would
# otherwise look like the distro shipping the real name and be skipped
# silently instead of reported.
if [ -e "$dest" ] && [ "$(readlink -f "$dest")" != "$(readlink -f "$src")" ]; then
log_warn "$dest already exists and is not a link to $src; leaving it alone"
return 0
fi
# A distro that ships `bat` as bat, elsewhere on PATH, needs no wrapper.
if has_cmd "$want" && [ ! -e "$dest" ]; then
return 0
fi
ln -sf "$src" "$dest"
log_ok "link $dest -> $src"
}
link_wrapper bat batcat
link_wrapper fd fdfind

case ":${PATH}:" in
*":$HOME/.local/bin:"*) ;;
*) log_warn "$HOME/.local/bin is not on this shell's PATH; start a new login shell to pick up bat/fd" ;;
esac
log_info "Versions"
show_versions nvim tmux mosh rg fzf bat fd jq lazygit