feat(install): optional add-ons via --with and interactive prompt

Wire install_<name>.sh into the main installer through a shared catalog
so fnm/pyenv/nvm (and future tools) share one CLI and prompt path.
This commit is contained in:
Jeffrey
2026-08-07 14:23:30 +08:00
parent 42c8305871
commit bff5ceddf0
3 changed files with 160 additions and 8 deletions
+14 -3
View File
@@ -10,10 +10,21 @@ fzf-tab, k), plus eza, zoxide, fzf, and Nerd Fonts.
curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/install.sh | bash
```
Options: `--cp-hist` / `-c` imports bash history; `--non-interactive` / `-n`
skips the default-shell change.
Options:
From a git checkout:
- `--cp-hist` / `-c` — import bash history
- `--non-interactive` / `-n` — skip default-shell change and optional prompts
- `--with <names>` — install optional add-ons (comma-separated, repeatable).
Each name runs `install_<name>.sh` (currently: `fnm`, `pyenv`, `nvm`).
Interactive installs (without `-n` and without `--with`) prompt for the same list.
```bash
./install.sh --with fnm
./install.sh --with fnm,pyenv -n
curl -fsSL …/install.sh | bash -s -- --with fnm
```
From a git checkout (shell stubs only):
```bash
./deploy.sh
+137 -5
View File
@@ -5,27 +5,120 @@ set -euo pipefail
# Save the original working directory
ORIGINAL_DIR="$(pwd)"
# Optional add-ons: each name maps to install_<name>.sh in this repo.
# Append here when adding a new optional installer.
OPTIONAL_ADDONS=(fnm pyenv nvm)
# When run from a git checkout, prefer local install_*.sh; curl|bash uses EASYZSH_RAW_BASE.
SCRIPT_DIR=""
if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "bash" && -f "${BASH_SOURCE[0]}" ]]; then
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
fi
# Flags to determine if the arguments were passed
cp_hist_flag=false
noninteractive_flag=false
with_flag_set=false
selected_addons=()
# Loop through all arguments
for arg in "$@"
do
case $arg in
usage() {
cat <<'EOF'
Usage: install.sh [options]
Options:
-c, --cp-hist Copy bash history into zsh history
-n, --non-interactive Skip default-shell change and optional prompts
--with <names> Install optional add-ons (comma-separated).
Repeatable. Names match install_<name>.sh
(currently: fnm, pyenv, nvm).
-h, --help Show this help
Examples:
./install.sh
./install.sh --with fnm
./install.sh --with fnm,pyenv -n
curl -fsSL …/install.sh | bash -s -- --with fnm
EOF
}
is_known_addon() {
local name="$1"
local known
for known in "${OPTIONAL_ADDONS[@]}"; do
[[ "$known" == "$name" ]] && return 0
done
return 1
}
append_addons_csv() {
local csv="$1"
local part
local IFS=','
# shellcheck disable=SC2086
for part in $csv; do
part="${part#"${part%%[![:space:]]*}"}"
part="${part%"${part##*[![:space:]]}"}"
[[ -z "$part" ]] && continue
if ! is_known_addon "$part"; then
echo "Unknown optional add-on: $part (known: ${OPTIONAL_ADDONS[*]})" >&2
exit 2
fi
selected_addons+=("$part")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
--cp-hist|-c)
cp_hist_flag=true
shift
;;
--non-interactive|-n)
noninteractive_flag=true
shift
;;
--with)
with_flag_set=true
shift
if [[ $# -eq 0 || "$1" == -* ]]; then
echo "--with requires a comma-separated list of add-on names" >&2
exit 2
fi
append_addons_csv "$1"
shift
;;
--with=*)
with_flag_set=true
append_addons_csv "${1#--with=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $arg" >&2
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
dedupe_selected_addons() {
local _deduped=()
local _name _d _seen
for _name in "${selected_addons[@]+"${selected_addons[@]}"}"; do
_seen=false
for _d in "${_deduped[@]+"${_deduped[@]}"}"; do
[[ "$_d" == "$_name" ]] && _seen=true && break
done
[[ "$_seen" == false ]] && _deduped+=("$_name")
done
selected_addons=("${_deduped[@]+"${_deduped[@]}"}")
}
dedupe_selected_addons
if [ -f ~/.gitconfig ]; then
cp ~/.gitconfig ~/.gitconfig.backup
echo "Backup of .gitconfig created."
@@ -424,6 +517,45 @@ else
echo -e "\nNot copying bash_history to zsh_history, as --cp-hist or -c is not supplied\n"
fi
# Optional add-ons (install_<name>.sh). After shell stubs so patches land correctly.
run_optional_addon() {
local name="$1"
local script="install_${name}.sh"
echo -e "\n=== Optional add-on: ${name} ===\n"
if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/$script" ]]; then
bash "$SCRIPT_DIR/$script"
else
curl -fsSL "${EASYZSH_RAW_BASE}/${script}" | bash
fi
}
prompt_optional_addons() {
# Only when interactive and user did not pass --with.
echo -e "\nOptional add-ons (each is install_<name>.sh):"
local name
for name in "${OPTIONAL_ADDONS[@]}"; do
echo " - $name"
done
echo -n "Install which? Comma-separated names, or Enter to skip: "
local reply
read -r reply || reply=""
[[ -z "${reply//[[:space:]]/}" ]] && return 0
append_addons_csv "$reply"
}
if [[ "$with_flag_set" == false && "$noninteractive_flag" == false ]]; then
prompt_optional_addons
dedupe_selected_addons
fi
if ((${#selected_addons[@]})); then
for name in "${selected_addons[@]}"; do
run_optional_addon "$name"
done
elif [[ "$with_flag_set" == true ]]; then
echo -e "\n--with given but no add-ons selected; skipping optionals\n"
fi
if [ "$noninteractive_flag" = true ]; then
echo -e "Installation complete, exit terminal and enter a new zsh session\n"
echo -e "Make sure to change zsh to default shell by running: chsh -s $(command -v zsh)"
+9
View File
@@ -99,6 +99,15 @@ grep -Fq '"$ZSH_CONFIGS_DIR"/*(DN)' "$ROOT/.zshrc" || fail "existing extensionle
grep -Fq '_resolve_trash_cmd' "$ROOT/.zshrc" || fail "trash-backed rm helper is missing"
grep -Fq 'libglib2.0-bin' "$ROOT/install.sh" || fail "Ubuntu Trash support is not installed"
grep -Fq 'deploy.sh' "$ROOT/install.sh" || fail "install.sh does not invoke deploy.sh"
grep -Fq 'OPTIONAL_ADDONS=' "$ROOT/install.sh" || fail "install.sh missing OPTIONAL_ADDONS catalog"
grep -Fq -- '--with' "$ROOT/install.sh" || fail "install.sh missing --with option"
# --with parses known add-ons and rejects unknown ones without network.
if HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --with nosuch >/dev/null 2>&1; then
fail "install.sh accepted an unknown --with add-on"
fi
if ! HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --help >/dev/null; then
fail "install.sh --help failed"
fi
grep -Fq 'brew shellenv' "$ROOT/.zshenv" || fail "Homebrew is not initialized in .zshenv"
grep -Fq '/usr/local/bin/brew' "$ROOT/.zshenv" || fail "Intel Homebrew path is missing from .zshenv"
grep -Fq '$HOME/.local/bin' "$ROOT/.zshenv" || fail "~/.local/bin is not added in .zshenv"