feat: Added pnpm support
This commit is contained in:
261
.agents/skills/add_installer/SKILL.md
Normal file
261
.agents/skills/add_installer/SKILL.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
name: add_installer
|
||||
description: Add a new installer script to the bootstrap CLI project. Use this skill whenever the user asks to create a new installer, add a new tool/package to bootstrap, or register a new `b <name>` command.
|
||||
---
|
||||
|
||||
# Add a New Installer to Bootstrap CLI
|
||||
|
||||
This skill provides everything needed to add a new installer to the bootstrap project without reading the entire codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Bootstrap CLI (`b`) is a bash-based tool installer and system bootstrapper. Users run `b <name>` to install tools (e.g., `b nvim`, `b bat`). The project lives at the workspace root.
|
||||
|
||||
### Key Directories
|
||||
|
||||
```
|
||||
bootstrap/
|
||||
├── installers/ # Individual installer scripts (install_<name>.sh)
|
||||
├── lib/ # Shared libraries sourced by all installers
|
||||
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
|
||||
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install()
|
||||
│ └── shell_config.sh # get_shell_configs(), inject_block(), remove_block(), add_alias_if_missing(), add_env_if_missing()
|
||||
├── commands/ # Non-installer commands (help, con, uninstall)
|
||||
├── routes.sh # Central router + installer registry
|
||||
├── bootstrap.sh # Metascript for environment setup + library loading
|
||||
├── b.sh # The `b` shell function and autocompletion
|
||||
└── VERSION
|
||||
```
|
||||
|
||||
## Step-by-Step Checklist
|
||||
|
||||
When adding a new installer named `<name>`:
|
||||
|
||||
### Step 1: Create the installer script
|
||||
|
||||
Create `installers/install_<name>.sh` using the template below.
|
||||
|
||||
### Step 2: Register in `routes.sh`
|
||||
|
||||
Make **two** edits to `routes.sh`:
|
||||
|
||||
1. **Add to the `INSTALLERS` associative array** (line ~19-26). Insert a new entry in alphabetical order:
|
||||
```bash
|
||||
[<name>]="Short description of what it installs"
|
||||
```
|
||||
|
||||
2. **Add to the `INSTALLER_KEYS` array** (line ~28). Insert the key in alphabetical order:
|
||||
```bash
|
||||
INSTALLER_KEYS=(agy bat <name> node nvim yazi zoxide)
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Both arrays must be kept in sync and in alphabetical order.
|
||||
|
||||
### Step 3: Verify (optional)
|
||||
|
||||
Run `bash routes.sh` or `b all` to confirm the new installer appears in the help output.
|
||||
|
||||
---
|
||||
|
||||
## Installer Script Template
|
||||
|
||||
Every installer follows this exact boilerplate structure. Copy this and fill in the tool-specific logic:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# <ToolName> Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
eval "$(wget -qO- "$METASCRIPT_URL")"
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: Neither wget nor curl is installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Installation Logic ──────────────────────────────────────────────
|
||||
|
||||
install_<name>() {
|
||||
if has_command <command_name>; then
|
||||
if ! confirm "<ToolName> is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping <ToolName> installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install <ToolName>?"; then
|
||||
log_info "Skipping <ToolName> installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Tool-specific installation logic goes here ---
|
||||
# Use pkg_install for distro packages:
|
||||
# pkg_install <package>
|
||||
# Use detect_distro for distro-specific logic:
|
||||
# local distro; distro=$(detect_distro)
|
||||
# Use detect_arch for arch-specific logic:
|
||||
# local arch; arch=$(detect_arch)
|
||||
# For GitHub releases, use curl/wget pattern (see bat installer for reference)
|
||||
}
|
||||
|
||||
# ─── Shell Configuration (if needed) ─────────────────────────────────
|
||||
|
||||
configure_shell() {
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
log_info "Configuring <ToolName> in $config_file..."
|
||||
|
||||
# Use inject_block to add shell init/aliases/env vars:
|
||||
# inject_block "$config_file" "<name> init" "<content>"
|
||||
# Use add_alias_if_missing for simple aliases:
|
||||
# add_alias_if_missing "$config_file" "<alias>" "<value>"
|
||||
# Use add_env_if_missing for environment variables:
|
||||
# add_env_if_missing "$config_file" "VAR_NAME" "value"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
install_<name>
|
||||
configure_shell # Remove this line if no shell config is needed
|
||||
|
||||
echo
|
||||
log_success "<ToolName> installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Library Functions
|
||||
|
||||
These are pre-loaded by `bootstrap.sh` — no need to source them manually in installers.
|
||||
|
||||
### From `lib/common.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `log_info "msg"` | Blue `[INFO]` message to stdout |
|
||||
| `log_success "msg"` | Green `[SUCCESS]` message to stdout |
|
||||
| `log_warn "msg"` | Yellow `[WARNING]` message to stderr |
|
||||
| `log_error "msg"` | Red `[ERROR]` message to stderr |
|
||||
| `confirm "prompt"` | Interactive yes/no prompt, returns 0 for yes |
|
||||
| `has_command <cmd>` | Check if a command exists (returns 0/1) |
|
||||
| `make_temp_dir` | Create and echo a temp directory path |
|
||||
|
||||
### From `lib/platform.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `detect_distro` | Echoes `arch`, `debian`, `fedora`, or `unknown` |
|
||||
| `detect_arch` | Echoes `x86_64` or `arm64` |
|
||||
| `pkg_install <pkg>...` | Install packages via the system package manager. Supports distro-specific mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"` |
|
||||
|
||||
### From `lib/shell_config.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `get_shell_configs` | Space-separated list of existing RC files (`~/.bashrc`, `~/.zshrc`) |
|
||||
| `inject_block <file> <name> <content>` | Idempotently inject a named block into a config file (removes old block first) |
|
||||
| `remove_block <file> <name>` | Remove a named block from a config file |
|
||||
| `add_alias_if_missing <file> <alias> <value>` | Add an alias line if not already present |
|
||||
| `add_env_if_missing <file> <var> <value>` | Add an `export VAR="value"` line if not already present |
|
||||
| `create_fd_symlink` | Symlink `fdfind` → `fd` on Debian/Ubuntu |
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Temp directory with cleanup
|
||||
|
||||
```bash
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
```
|
||||
|
||||
### Distro-specific installation (e.g., GitHub .deb for Debian, pacman for Arch)
|
||||
|
||||
```bash
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
case "$distro" in
|
||||
arch)
|
||||
pkg_install <package>
|
||||
;;
|
||||
debian)
|
||||
# Download .deb from GitHub releases
|
||||
;;
|
||||
fedora)
|
||||
pkg_install <package>
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported distribution."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Fetching latest GitHub release tag
|
||||
|
||||
```bash
|
||||
local latest_tag=""
|
||||
if has_command curl; then
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/<owner>/<repo>/releases/latest \
|
||||
| grep '"tag_name":' | head -n1 \
|
||||
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
elif has_command wget; then
|
||||
latest_tag=$(wget -qO- https://api.github.com/repos/<owner>/<repo>/releases/latest \
|
||||
| grep '"tag_name":' | head -n1 \
|
||||
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v1.0.0" # fallback
|
||||
log_warn "Failed to fetch latest version. Falling back to: $latest_tag"
|
||||
fi
|
||||
```
|
||||
|
||||
### Shell block injection (idempotent)
|
||||
|
||||
```bash
|
||||
# Block name should be unique and descriptive
|
||||
inject_block "$config_file" "<tool> init" 'eval "$(tool init bash)"'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules & Conventions
|
||||
|
||||
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
|
||||
2. **Alphabetical order**: Keep `INSTALLERS` entries and `INSTALLER_KEYS` in alphabetical order in `routes.sh`.
|
||||
3. **Confirmation prompts**: Always ask before installing. Check if already installed first.
|
||||
4. **Idempotent**: Installers must be safe to re-run. Use `inject_block` (not append) for shell configs.
|
||||
5. **No hardcoded paths**: Use `$HOME`, library functions, and `detect_*` helpers.
|
||||
6. **Error handling**: Use `set -euo pipefail` after sourcing `bootstrap.sh`.
|
||||
7. **Metascript boilerplate**: The first 22 lines of every installer are identical — always copy them verbatim.
|
||||
8. **`main "$@"`**: Always end with this pattern to pass through CLI arguments.
|
||||
245
installers/install_pnpm.sh
Normal file
245
installers/install_pnpm.sh
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# pnpm Installer Script
|
||||
#
|
||||
# Installs pnpm (fast, disk-space-efficient package manager for Node.js).
|
||||
# Supports glibc and musl (Alpine) Linux on x86_64 and arm64.
|
||||
#
|
||||
# Linux runtime requirements:
|
||||
# - glibc 2.27+ and libatomic.so.1 (for glibc builds)
|
||||
# - Debian/Ubuntu: apt-get install -y libatomic1
|
||||
# - Fedora/RHEL: dnf install -y libatomic
|
||||
#
|
||||
# Docker usage:
|
||||
# wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
eval "$(wget -qO- "$METASCRIPT_URL")"
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: Neither wget nor curl is installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ─── Helper Functions ─────────────────────────────────────────────────
|
||||
|
||||
download() {
|
||||
if has_command curl; then
|
||||
curl -fsSL "$1"
|
||||
else
|
||||
wget -qO- "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
is_glibc_compatible() {
|
||||
getconf GNU_LIBC_VERSION >/dev/null 2>&1 || ldd --version >/dev/null 2>&1 || return 1
|
||||
}
|
||||
|
||||
# Detect libc suffix — empty for glibc, "-musl" for musl-based distros
|
||||
detect_libc_suffix() {
|
||||
if ! is_glibc_compatible; then
|
||||
printf -- '-musl'
|
||||
fi
|
||||
}
|
||||
|
||||
# pnpm v11.0.0-rc.3 renamed release assets. Older versions use legacy names.
|
||||
use_legacy_assets() {
|
||||
local version="$1"
|
||||
local major
|
||||
major="$(echo "$version" | cut -d. -f1)"
|
||||
if [ "$major" -lt 11 ] 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
case "$version" in
|
||||
11.0.0-rc.1|11.0.0-rc.2) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Legacy asset basename for pre-v11.0.0-rc.3 releases
|
||||
legacy_asset_basename() {
|
||||
local arch libc_suffix
|
||||
arch="$1"
|
||||
libc_suffix="$2"
|
||||
if [ -n "$libc_suffix" ]; then
|
||||
printf 'pnpm-linuxstatic-%s' "$arch"
|
||||
else
|
||||
printf 'pnpm-linux-%s' "$arch"
|
||||
fi
|
||||
}
|
||||
|
||||
# Release-page asset basename (without extension)
|
||||
asset_basename() {
|
||||
local version arch libc_suffix
|
||||
version="$1"
|
||||
arch="$2"
|
||||
libc_suffix="$3"
|
||||
if use_legacy_assets "$version"; then
|
||||
legacy_asset_basename "$arch" "$libc_suffix"
|
||||
else
|
||||
printf 'pnpm-linux-%s%s' "$arch" "$libc_suffix"
|
||||
fi
|
||||
}
|
||||
|
||||
# Map system arch to pnpm's naming (x64 / arm64)
|
||||
detect_pnpm_arch() {
|
||||
local arch
|
||||
arch="$(uname -m | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
case "${arch}" in
|
||||
x86_64 | amd64) arch="x64" ;;
|
||||
arm64 | aarch64) arch="arm64" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
# Double check 32-bit OS reported as 64-bit
|
||||
if [ "${arch}" = "x64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
|
||||
return 1
|
||||
elif [ "${arch}" = "arm64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s' "${arch}"
|
||||
}
|
||||
|
||||
# ─── Installation Logic ──────────────────────────────────────────────
|
||||
|
||||
install_pnpm() {
|
||||
if has_command pnpm; then
|
||||
if ! confirm "pnpm is already installed ($(pnpm --version)). Reinstall/Upgrade?"; then
|
||||
log_info "Skipping pnpm installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install pnpm?"; then
|
||||
log_info "Skipping pnpm installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
local arch libc_suffix version_json version major_version asset_base
|
||||
|
||||
arch="$(detect_pnpm_arch)" || {
|
||||
log_error "pnpm currently only provides pre-built binaries for x86_64/arm64 architectures."
|
||||
return 1
|
||||
}
|
||||
libc_suffix="$(detect_libc_suffix)"
|
||||
|
||||
# Fetch the latest version from the npm registry, or use PNPM_VERSION if set
|
||||
if [ -z "${PNPM_VERSION:-}" ]; then
|
||||
log_info "Fetching latest pnpm version from npm registry..."
|
||||
version_json="$(download "https://registry.npmjs.org/@pnpm/exe")" || {
|
||||
log_error "Failed to fetch pnpm version info from npm registry."
|
||||
return 1
|
||||
}
|
||||
version="$(echo "$version_json" | grep -o '"latest":[[:space:]]*"[0-9.]*"' | grep -o '[0-9.]*')"
|
||||
else
|
||||
version="${PNPM_VERSION}"
|
||||
fi
|
||||
|
||||
# Normalize major version (strip leading "v", extract digits)
|
||||
major_version="$(printf '%s' "$version" | sed -E 's/^v//; s/^([0-9]+).*/\1/')"
|
||||
if [ -z "$major_version" ]; then
|
||||
log_error "Invalid PNPM_VERSION: $version"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Downloading pnpm v${version} (linux-${arch}${libc_suffix})..."
|
||||
asset_base="$(asset_basename "$version" "$arch" "$libc_suffix")"
|
||||
|
||||
if [ "$major_version" -ge 11 ]; then
|
||||
# v11+: distributed as tarballs containing the binary and dist/ directory
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}.tar.gz" > "$TMP_DIR/pnpm.tar.gz" || {
|
||||
log_error "Failed to download pnpm tarball."
|
||||
return 1
|
||||
}
|
||||
tar -xzf "$TMP_DIR/pnpm.tar.gz" -C "$TMP_DIR" || {
|
||||
log_error "Failed to extract pnpm tarball."
|
||||
return 1
|
||||
}
|
||||
chmod +x "$TMP_DIR/pnpm"
|
||||
SHELL="${SHELL:-/bin/bash}" "$TMP_DIR/pnpm" setup --force || {
|
||||
log_error "pnpm setup failed."
|
||||
return 1
|
||||
}
|
||||
else
|
||||
# Older versions: distributed as a single executable binary
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}" > "$TMP_DIR/pnpm" || {
|
||||
log_error "Failed to download pnpm binary."
|
||||
return 1
|
||||
}
|
||||
chmod +x "$TMP_DIR/pnpm"
|
||||
SHELL="${SHELL:-/bin/bash}" "$TMP_DIR/pnpm" setup --force || {
|
||||
log_error "pnpm setup failed."
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
log_success "pnpm v${version} installed successfully!"
|
||||
}
|
||||
|
||||
# ─── Shell Configuration ─────────────────────────────────────────────
|
||||
|
||||
configure_shell() {
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
|
||||
# pnpm's `setup --force` configures PNPM_HOME and PATH automatically,
|
||||
# but we also add an env block to ensure PNPM_HOME is set consistently.
|
||||
local content
|
||||
content=$(cat << 'EOF'
|
||||
# pnpm
|
||||
export PNPM_HOME="$HOME/.local/share/pnpm"
|
||||
case ":$PATH:" in
|
||||
*":$PNPM_HOME:"*) ;;
|
||||
*) export PATH="$PNPM_HOME:$PATH" ;;
|
||||
esac
|
||||
EOF
|
||||
)
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
log_info "Configuring pnpm in $config_file..."
|
||||
inject_block "$config_file" "pnpm setup" "$content"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
install_pnpm
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
if has_command pnpm; then
|
||||
log_success "pnpm installation and configuration complete."
|
||||
log_info "Installed pnpm version: $(pnpm --version 2>/dev/null || echo 'unknown')"
|
||||
else
|
||||
log_success "Installation complete."
|
||||
log_info "Please close and reopen your terminal or run: source ~/.bashrc (or source ~/.zshrc) to verify."
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -21,11 +21,12 @@ declare -A INSTALLERS=(
|
||||
[bat]="Install Bat (alternative to cat) and configure alias"
|
||||
[node]="Install Node.js (LTS) and NVM"
|
||||
[nvim]="Install Neovim 0.11.7 and configuration"
|
||||
[pnpm]="Install pnpm package manager"
|
||||
[yazi]="Install Yazi terminal file manager and dependencies"
|
||||
[zoxide]="Install Zoxide directory jumper"
|
||||
)
|
||||
# Order in which installers should be displayed
|
||||
INSTALLER_KEYS=(agy bat node nvim yazi zoxide)
|
||||
INSTALLER_KEYS=(agy bat node nvim pnpm yazi zoxide)
|
||||
|
||||
SCRIPT_NAMES="${1:-}"
|
||||
if [ -z "$SCRIPT_NAMES" ] || [ "$SCRIPT_NAMES" = "-h" ] || [ "$SCRIPT_NAMES" = "--help" ]; then
|
||||
|
||||
Reference in New Issue
Block a user