diff --git a/.agents/skills/add_installer/SKILL.md b/.agents/skills/add_installer/SKILL.md index 98eb5ee..109cbaa 100644 --- a/.agents/skills/add_installer/SKILL.md +++ b/.agents/skills/add_installer/SKILL.md @@ -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,18 +35,27 @@ bootstrap/ When adding a new installer named ``: -### Step 1: Create the installer script +### Step 1: Analyze user request & gather details -Create `installers/install_.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_` function. +If the user provides an official install or curl script in the prompt, or a link to one: +- Execute the `curl` command (or use `read_url_content` or `read_browser_page`) 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), unnecessary shell configuration logic, and self-update logic. +- Implement only the core, essential Linux installation logic inside the `install_` function. - Do NOT add checks for `curl` or attempt to install it. Assume `curl` is installed and available. -### 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_` 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 metadata headers: ```bash @@ -58,15 +67,16 @@ 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 ` to uninstall or rollback a tool: +To ensure the user can seamlessly use `b rb ` to uninstall or rollback a tool, all manual modifications must be tracked: - When extracting binaries, copy them to `$BOOTSTRAP_BIN` and use `track_file "$BOOTSTRAP_BIN/binary"`. Never use `sudo` or write to system folders like `/usr/local/bin`. - When creating directories (e.g., in `$BOOTSTRAP_OPT` or `$BOOTSTRAP_RUNTIMES`), use `track_dir` to register them. - When running manual commands (like compiling or registry changes), log their inverses: `add_rollback_cmd "rm -rf ..."` or equivalent cleanup. -Note: `write_env_snippet` and `write_alias_snippet` will automatically track themselves. +- When installing packages via `pkg_install`, always register them with `registry_add_sys_deps` for reference-counted rollback tracking. +- Note: `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. @@ -121,6 +131,7 @@ configure_shell() { # Use drop-in snippets for shell configuration (they auto-rollback) # write_env_snippet "" "export VAR_NAME=value\neval \"\$( init bash)\"" # write_alias_snippet "" "alias =''" + # write_completion_snippet "" "source <( completion bash)" : } @@ -179,6 +190,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in |---|---| | `write_env_snippet ` | Creates an isolated `env.d/` shell drop-in snippet and registers it for rollback. | | `write_alias_snippet ` | Creates an isolated `aliases.d/` shell drop-in snippet and registers it for rollback. | +| `write_completion_snippet ` | Creates an isolated `completions.d/` bash completion snippet and registers it for rollback. | ### From `lib/github.sh` @@ -238,8 +250,8 @@ track_file "$BOOTSTRAP_BIN/binary" 1. **File naming**: Always `install_.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 `$BOOTSTRAP_BIN`, you MUST call `track_file`. -4. **Shell Drop-ins**: Always use `write_env_snippet` or `write_alias_snippet` instead of manually injecting code directly into `~/.bashrc`. +3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `$BOOTSTRAP_BIN`, you MUST call `track_file`. If you build/install manually (e.g. via `makepkg -si`), you MUST call `add_rollback_cmd "sudo pacman -R --noconfirm "` or equivalent. +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`, `$BOOTSTRAP_BIN`, `$BOOTSTRAP_OPT`, `$BOOTSTRAP_RUNTIMES`, library functions, and `detect_*` helpers. 6. **Error handling**: Use `set -euo pipefail`. 7. **No Standalone Execution Guards**: Do NOT add guards to prevent running the installer directly; simplify code paths to focus on clean runs. diff --git a/bootstrap.sh b/bootstrap.sh index db8d966..0ca0eed 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -67,6 +67,7 @@ install_bootstrap() { local routes_dir="$HOME/.config/bootstrap" mkdir -p "$routes_dir/env.d" mkdir -p "$routes_dir/aliases.d" + mkdir -p "$routes_dir/completions.d" # Initialize XDG directories mkdir -p "$HOME/.local/share/bootstrap/bin" @@ -169,6 +170,7 @@ export BOOTSTRAP_RUNTIMES="$BOOTSTRAP_DATA_DIR/runtimes" [ -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 diff --git a/installers/install_hyperfine.sh b/installers/install_hyperfine.sh new file mode 100644 index 0000000..b5d0d80 --- /dev/null +++ b/installers/install_hyperfine.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Tool: hyperfine +# DisplayName: Hyperfine +# Description: Command-line benchmarking tool +# Strategy: binary +# + +set -euo pipefail + +TMP_DIR="$(make_temp_dir)" +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +install_hyperfine() { + if has_command 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 + + # 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=$(github_get_latest_release "sharkdp/hyperfine") + + if [ -z "$latest_tag" ]; then + latest_tag="v1.20.0" + log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag" + else + log_info "Latest Hyperfine version found: $latest_tag" + fi + + local archive="$TMP_DIR/hyperfine.tar.gz" + github_download_asset "sharkdp/hyperfine" "$latest_tag" "hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz" "$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 + 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 $BOOTSTRAP_BIN + local target_dir="$BOOTSTRAP_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 + + register_tool "hyperfine" "binary" "$latest_tag" "github:sharkdp/hyperfine" +} + +main() { + install_hyperfine + + echo + log_success "Hyperfine installation complete." +} + +main "$@" diff --git a/lib/registry.sh b/lib/registry.sh index 82a8eea..66e5a53 100644 --- a/lib/registry.sh +++ b/lib/registry.sh @@ -7,6 +7,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" @@ -24,6 +25,7 @@ declare -A INSTALLER_DISPLAYS=( [asciicinema]="asciicinema" [bat]="Bat" [docker]="Docker" + [hyperfine]="Hyperfine" [lazygit]="lazygit" [node]="Node" [nvim]="Neovim" @@ -41,6 +43,7 @@ declare -A INSTALLER_STRATEGIES=( [asciicinema]="binary" [bat]="binary" [docker]="system" + [hyperfine]="binary" [lazygit]="binary" [node]="managed" [nvim]="binary" @@ -53,4 +56,4 @@ declare -A INSTALLER_STRATEGIES=( [zoxide]="managed" ) -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) diff --git a/lib/shell_config.sh b/lib/shell_config.sh index 028ab9f..282a428 100644 --- a/lib/shell_config.sh +++ b/lib/shell_config.sh @@ -159,6 +159,34 @@ remove_alias_snippet() { fi } +# Write completion snippet to completions.d/ +# Usage: write_completion_snippet +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 +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 @@ -178,8 +206,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