3 Commits

Author SHA1 Message Date
671cf7f818 feat(Installer): Add installer for hyperfine 2026-06-27 08:17:31 +05:30
f5227569b1 feat(skill): Update add_installer skill 2026-06-27 08:09:46 +05:30
15d3a1a59d feat: Support for drop-in completions
`shell_config.sh` has support for tool completions using the
`write_completion_snippet` and `write_alias_snippet`
2026-06-27 07:58:10 +05:30
5 changed files with 190 additions and 19 deletions

View File

@@ -20,7 +20,7 @@ bootstrap/
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install(), pkg_check(), pkg_remove()
│ ├── rollback.sh # Rollback tracking (track_file, track_dir, add_rollback_cmd)
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet, write_completion_snippet
│ ├── registry.sh # Dynamically generated installer registry
│ └── routes.sh # Central router script
├── commands/ # Non-installer commands (help, con, uninstall)
@@ -35,17 +35,26 @@ bootstrap/
When adding a new installer named `<name>`:
### Step 1: Create the installer script
### Step 1: Analyze user request & gather details
Create `installers/install_<name>.sh` using the template below.
When the user asks you to create an installer, they often provide either an official `curl` install script or a link to a `.tar.gz` release.
You MUST do the following before writing the script:
If the user provides an official install or curl script in the prompt:
- Read and analyze the script.
- Remove redundant parts like macOS and Windows compatibility.
- Strip unnecessary shell boilerplate, self-update logic, and other bloat.
- Implement only the essential Linux installation logic inside the `install_<name>` function.
**If the user provides a `curl` command or link to an install script:**
- Execute the `curl` command (or use `read_url_content`) to fetch the script and analyze what it actually does under the hood.
- Do NOT simply execute the official script blindly in your installer.
- Re-write its functionality according to the conventions of the bootstrap installer.
- Strip away redundant code, OS checks for macOS/Windows (we only target Linux), and unnecessary shell configuration logic.
- Implement only the core, essential Linux installation logic inside the `install_<name>` function.
### Step 2: Add metadata comments to the top of your installer script
**If the user provides a link to a `.tar.gz` (or `.zip`):**
- First, download the archive to a temporary directory and extract it to inspect its contents.
- Analyze the extracted folder structure to decide what needs to be installed (e.g., binaries, man pages, completions) and what should be ignored/deleted.
- Write the `install_<name>` function to download, extract, and copy only those essential files. (Use `download_file` and temporary directories, see "Resumable Download and Extraction" below).
### Step 2: Create the installer script
### Step 3: Add metadata comments to the top of your installer script
At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following three metadata headers:
```bash
@@ -56,15 +65,15 @@ At the top of your new installer script, right below `#!/usr/bin/env bash`, add
The central router `lib/routes.sh` and autocomplete function in `b.sh` will dynamically parse this metadata from all `install_*.sh` scripts to register the installer and keys automatically! No manual edits to `lib/routes.sh` or `b.sh` are required.
### Step 3: Implement Rollback Tracking (Crucial)
### Step 4: Implement Rollback Tracking (Crucial)
To ensure the user can seamlessly use `b rb <name>`, all manual modifications must be tracked:
- When extracting binaries to `~/.local/bin/`, use `track_file "$HOME/.local/bin/binary"`.
- When creating directories like `~/.config/tool/`, use `track_dir "$HOME/.config/tool"`.
- When running manual apt/dnf/npm commands, log their inverses: `add_rollback_cmd "sudo npm uninstall -g package"`.
Note: `pkg_install`, `write_env_snippet`, and `write_alias_snippet` will automatically track themselves.
Note: `pkg_install`, `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` will automatically track themselves.
### Step 4: Verify (optional)
### Step 5: Verify (optional)
Verify that the installer works and appears in the help output:
- Run `b all` to confirm it appears in the help list.
@@ -126,6 +135,7 @@ configure_shell() {
# Use drop-in snippets for shell configuration (they auto-rollback)
# write_env_snippet "<name>" "export VAR_NAME=value\neval \"\$(<name> init bash)\""
# write_alias_snippet "<name>" "alias <name>='<command>'"
# write_completion_snippet "<name>" "source <(<command> completion bash)"
:
}
@@ -184,6 +194,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|---|---|
| `write_env_snippet <name> <content>` | Creates an isolated `env.d/` shell drop-in snippet and registers it for rollback. |
| `write_alias_snippet <name> <content>` | Creates an isolated `aliases.d/` shell drop-in snippet and registers it for rollback. |
| `write_completion_snippet <name> <content>` | Creates an isolated `completions.d/` bash completion snippet and registers it for rollback. |
---
@@ -241,7 +252,7 @@ track_file "/usr/local/bin/binary"
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
2. **Confirmation prompts**: Always ask before installing. Check if already installed first.
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `~/.local/bin/`, you MUST call `track_file`. If you run `makepkg`, you MUST call `add_rollback_cmd` for `pacman -R`.
4. **Shell Drop-ins**: Always use `write_env_snippet` or `write_alias_snippet` instead of manually injecting code directly into `~/.bashrc`.
4. **Shell Drop-ins**: Always use `write_env_snippet`, `write_alias_snippet`, or `write_completion_snippet` instead of manually injecting code directly into `~/.bashrc`.
5. **No hardcoded paths**: Use `$HOME`, library functions, and `detect_*` helpers.
6. **Error handling**: Use `set -euo pipefail` after the guard block.
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.

View File

@@ -71,6 +71,7 @@ install_bootstrap() {
mkdir -p "$routes_dir"
mkdir -p "$routes_dir/env.d"
mkdir -p "$routes_dir/aliases.d"
mkdir -p "$routes_dir/completions.d"
# List of all files to download/copy
local files=(
@@ -142,6 +143,7 @@ export BOOTSTRAP_DIR="$HOME/.config/bootstrap"
[ -f "$BOOTSTRAP_DIR/b.sh" ] && . "$BOOTSTRAP_DIR/b.sh"
for f in "$BOOTSTRAP_DIR/env.d/"*.sh; do [ -r "$f" ] && . "$f"; done
for f in "$BOOTSTRAP_DIR/aliases.d/"*.sh; do [ -r "$f" ] && . "$f"; done
for f in "$BOOTSTRAP_DIR/completions.d/"*.sh; do [ -r "$f" ] && . "$f"; done
# <<< bootstrap-cli setup <<<
EOF

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# Tool: hyperfine
# DisplayName: Hyperfine
# Description: Command-line benchmarking tool
#
# Hyperfine Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
install_hyperfine() {
if has_command hyperfine || [ -f "$HOME/.local/bin/hyperfine" ]; then
if ! confirm "Hyperfine is already installed. Reinstall/Upgrade?"; then
log_info "Skipping Hyperfine installation."
return
fi
else
if ! confirm "Install Hyperfine?"; then
log_info "Skipping Hyperfine installation."
return
fi
fi
# Ensure curl is installed
if ! has_command curl; then
log_info "curl not found. Installing curl..."
pkg_install curl
fi
# Detect architecture
local raw_arch
raw_arch=$(detect_arch)
local arch=""
case "$raw_arch" in
x86_64) arch="x86_64" ;;
arm64) arch="aarch64" ;;
*) log_error "Unsupported Linux architecture: $raw_arch"; exit 1 ;;
esac
log_info "Fetching latest Hyperfine version from GitHub..."
local latest_tag=""
latest_tag=$(curl -sL https://api.github.com/repos/sharkdp/hyperfine/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
local download_url
if [ -n "$latest_tag" ]; then
log_info "Latest Hyperfine version found: $latest_tag"
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
else
latest_tag="v1.20.0"
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
fi
log_info "Downloading Hyperfine from ${download_url}..."
local archive="$TMP_DIR/hyperfine.tar.gz"
download_file "$download_url" "$archive"
# Extract the archive
log_info "Extracting Hyperfine archive..."
tar -xzf "$archive" -C "$TMP_DIR"
local extract_dir="$TMP_DIR/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu"
if [ ! -d "$extract_dir" ]; then
# Handle case where directory name might differ (e.g. without leading v in directory name or tag)
extract_dir=$(find "$TMP_DIR" -maxdepth 1 -type d -name "hyperfine-*" | head -n1)
fi
if [ -z "$extract_dir" ] || [ ! -d "$extract_dir" ]; then
log_error "Failed to locate extracted Hyperfine directory."
exit 1
fi
# Install binary to ~/.local/bin
local target_dir="$HOME/.local/bin"
mkdir -p "$target_dir"
log_info "Installing Hyperfine to $target_dir/hyperfine..."
cp "$extract_dir/hyperfine" "$target_dir/hyperfine"
chmod +x "$target_dir/hyperfine"
track_file "$target_dir/hyperfine"
# Install man page if present
if [ -f "$extract_dir/hyperfine.1" ]; then
local man_dir="$HOME/.local/share/man/man1"
mkdir -p "$man_dir"
log_info "Installing man page to $man_dir/hyperfine.1..."
cp "$extract_dir/hyperfine.1" "$man_dir/hyperfine.1"
track_file "$man_dir/hyperfine.1"
fi
# Install autocomplete if present
if [ -f "$extract_dir/autocomplete/hyperfine.bash" ]; then
log_info "Installing bash completions..."
local comp_content
comp_content=$(cat "$extract_dir/autocomplete/hyperfine.bash")
write_completion_snippet "hyperfine" "$comp_content"
fi
}
configure_shell() {
# Add ~/.local/bin to PATH for the current process
export PATH="$HOME/.local/bin:$PATH"
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "local-bin path"
done
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
}
main() {
install_hyperfine
configure_shell
echo
log_success "Hyperfine installation and configuration complete."
}
main "$@"

View File

@@ -5,6 +5,7 @@ declare -A INSTALLERS=(
[asciicinema]="asciinema terminal recorder"
[bat]="Bat (alternative to cat) and configure alias"
[docker]="Container runtime and orchestration platform"
[hyperfine]="Command-line benchmarking tool"
[lazygit]="Simple terminal UI for git commands"
[node]="Node.js (LTS) and NVM"
[nvim]="Neovim 0.12.0 and configuration"
@@ -22,6 +23,7 @@ declare -A INSTALLER_DISPLAYS=(
[asciicinema]="asciicinema"
[bat]="Bat"
[docker]="Docker"
[hyperfine]="Hyperfine"
[lazygit]="lazygit"
[node]="Node"
[nvim]="Neovim"
@@ -34,4 +36,4 @@ declare -A INSTALLER_DISPLAYS=(
[zoxide]="Zoxide"
)
INSTALLER_KEYS=(agy asciicinema bat docker lazygit node nvim pnpm rust starship uv yay yazi zoxide)
INSTALLER_KEYS=(agy asciicinema bat docker hyperfine lazygit node nvim pnpm rust starship uv yay yazi zoxide)

View File

@@ -158,6 +158,34 @@ remove_alias_snippet() {
fi
}
# Write completion snippet to completions.d/
# Usage: write_completion_snippet <name> <content>
write_completion_snippet() {
local name="$1"
local content="$2"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
mkdir -p "$dir"
log_info "Writing completion snippet '$name' to $dir/${name}.sh"
echo "$content" > "$dir/${name}.sh"
if type add_rollback_cmd >/dev/null 2>&1; then
add_rollback_cmd "rm -f \"$dir/${name}.sh\""
fi
}
# Remove completion snippet from completions.d/
# Usage: remove_completion_snippet <name>
remove_completion_snippet() {
local name="$1"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
if [ -f "$dir/${name}.sh" ]; then
log_info "Removing completion snippet '$name'"
rm -f "$dir/${name}.sh"
fi
}
# Setup fd symlink for Debian/Ubuntu (fdfind -> fd)
create_fd_symlink() {
if ! has_command fd && has_command fdfind; then
@@ -176,8 +204,4 @@ source_bashrc() {
# Export functions and variables for subshells
export _LIB_SHELL_CONFIG_SOURCED=1
export -f get_shell_configs remove_block inject_block add_alias_if_missing add_env_if_missing create_fd_symlink write_env_snippet write_alias_snippet remove_env_snippet remove_alias_snippet source_bashrc
export -f get_shell_configs remove_block inject_block add_alias_if_missing add_env_if_missing create_fd_symlink write_env_snippet write_alias_snippet remove_env_snippet remove_alias_snippet write_completion_snippet remove_completion_snippet source_bashrc