From 780e79364f5970904b24f8fdbef7258ee6cae5e0 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Thu, 25 Jun 2026 22:40:13 +0530 Subject: [PATCH 01/18] fix #9: Add validation check for pkg_remove --- lib/platform.sh | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/platform.sh b/lib/platform.sh index 3bba570..d12e708 100644 --- a/lib/platform.sh +++ b/lib/platform.sh @@ -169,21 +169,33 @@ pkg_remove() { local to_remove=() for pkg in "${pkgs[@]}"; do + local is_installed=0 + if pkg_check "$pkg"; then + is_installed=1 + fi + if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg" if [ -f "$ref_file" ]; then - # Remove this tool from the reference file - sed -i "/^${BOOTSTRAP_CURRENT_TOOL}$/d" "$ref_file" - if [ -s "$ref_file" ]; then - log_info "Skipping removal of '$pkg'; it is required by other tools." - continue - else + if [ "$is_installed" -eq 0 ]; then + log_info "Package '$pkg' is no longer installed system-wide. Cleaning up stale reference." rm -f "$ref_file" + else + # Remove this tool from the reference file + sed -i "/^${BOOTSTRAP_CURRENT_TOOL}$/d" "$ref_file" + if [ -s "$ref_file" ]; then + log_info "Skipping removal of '$pkg'; it is required by other tools." + continue + else + rm -f "$ref_file" + fi fi fi fi - to_remove+=("$pkg") + if [ "$is_installed" -eq 1 ]; then + to_remove+=("$pkg") + fi done if [ ${#to_remove[@]} -eq 0 ]; then From 7f3ff45f0504d45e204fa4ef27bc72da4522b9da Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 18:19:23 +0530 Subject: [PATCH 02/18] refactor: Use jq instead of custom posix complient json.sh While json.sh worked decently for reading json files, I didn't want to implement writing to json files as well and make it completely unreadable due to the added complexity. So, I think its better to just use jq and keep things relatively simple with the tradeoff of a lightweight dependency --- bootstrap.sh | 8 +++-- installers/install_agy.sh | 13 ++------ lib/json.sh | 68 --------------------------------------- lib/plugins.sh | 51 ++++------------------------- 4 files changed, 16 insertions(+), 124 deletions(-) delete mode 100644 lib/json.sh diff --git a/bootstrap.sh b/bootstrap.sh index 8aac6c8..26c6d0c 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -41,7 +41,7 @@ else BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR" _BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" - _LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/json.sh" "lib/plugins.sh") + _LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/plugins.sh") _curl_args=() for _lib in "${_LIBS[@]}"; do @@ -82,7 +82,6 @@ install_bootstrap() { "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" - "lib/json.sh" "lib/plugins.sh" "commands/help.sh" "commands/con.sh" @@ -90,6 +89,11 @@ install_bootstrap() { "commands/up.sh" ) + if ! pkg_check jq >/dev/null 2>&1; then + log_info "jq is missing. Installing jq..." + pkg_install jq + fi + if [ -f "$_SCRIPT_DIR/b.sh" ] && [ -f "$_SCRIPT_DIR/lib/routes.sh" ]; then log_info "Using local files from repository..." for file in "${files[@]}"; do diff --git a/installers/install_agy.sh b/installers/install_agy.sh index 4ee8aab..8eef314 100644 --- a/installers/install_agy.sh +++ b/installers/install_agy.sh @@ -55,19 +55,12 @@ install_agy() { exit 1 fi - # POSIX-compliant JSON parser (no jq dependencies) - parse_json_key() { - local payload="$1" - local key="$2" - echo "$payload" | sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' - } - local version local url local sha512 - version=$(parse_json_key "$manifest_json" "version") - url=$(parse_json_key "$manifest_json" "url") - sha512=$(parse_json_key "$manifest_json" "sha512") + version=$(echo "$manifest_json" | jq -r '.version // empty') + url=$(echo "$manifest_json" | jq -r '.url // empty') + sha512=$(echo "$manifest_json" | jq -r '.sha512 // empty') if [ -z "$url" ] || [ -z "$sha512" ]; then log_error "Failed to parse release manifest." diff --git a/lib/json.sh b/lib/json.sh deleted file mode 100644 index fa85bb8..0000000 --- a/lib/json.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash - -# generic JSON parser in pure bash and awk. -# reads JSON from stdin and outputs a flattened list of key-value pairs. -# example input: {"plugins": {"my_plugin": {"version": "1.0", "arr": [1, 2]}}} -# example output: - # plugins.my_plugin.version="1.0" - # plugins.my_plugin.arr[0]=1 - # plugins.my_plugin.arr[1]=2 - -# pardon my french -parse_json() { - # Tokenize the JSON using grep - grep -oE '"([^"\\]|\\.)*"|true|false|null|[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?|[][}{:,]' | \ - awk ' - BEGIN { - depth=0; - key="" - } - { - token = $0 - if (token == "{") { - depth++ - is_key[depth] = 1 - array_idx[depth] = "" - } else if (token == "}") { - delete path[depth] - delete array_idx[depth] - depth-- - } else if (token == "[") { - depth++ - is_key[depth] = 0 - array_idx[depth] = 0 - } else if (token == "]") { - delete array_idx[depth] - delete path[depth] - depth-- - } else if (token == ":") { - is_key[depth] = 0 - } else if (token == ",") { - if (array_idx[depth] != "") { - array_idx[depth]++ - } else { - is_key[depth] = 1 - } - } else { - if (is_key[depth] == 1) { - # Remove quotes from the key - gsub(/^"|"$/, "", token) - path[depth] = token - } else { - # It is a value - p = "" - for (i=1; i<=depth; i++) { - if (array_idx[i] != "") { - p = p "[" array_idx[i] "]" - } else if (path[i] != "") { - p = p "." path[i] - } - } - # Remove leading dot - sub(/^\./, "", p) - print p "=" token - } - } - } - ' -} diff --git a/lib/plugins.sh b/lib/plugins.sh index c19400e..bd44d6d 100644 --- a/lib/plugins.sh +++ b/lib/plugins.sh @@ -1,50 +1,13 @@ #!/usr/bin/env bash -if [ -f "$BOOTSTRAP_DIR/lib/json.sh" ]; then - . "$BOOTSTRAP_DIR/lib/json.sh" -fi - -# Parses a plugin manifest using the generic json parser and outputs bash array assignments +# Parses a plugin manifest using jq and outputs bash array assignments parse_plugin_manifest() { - # The generic parser outputs lines like: - # plugins.myplugin.version="1.0" - # plugins.myplugin.url="https://..." - # We want to extract myplugin and the keys to build: - # PLUGIN_VERSIONS["myplugin"]="1.0" - # PLUGIN_URLS["myplugin"]="https://..." - - parse_json | awk -F'=' ' - { - path = $1 - val = $2 - - # Remove quotes around value for bash array assignment - gsub(/^"|"$/, "", val) - - # Match paths starting with "plugins." - if (match(path, /^plugins\./)) { - rest = substr(path, RLENGTH + 1) - # Find the last dot to separate plugin name from the property key - last_dot = 0 - for (i=length(rest); i>0; i--) { - if (substr(rest, i, 1) == ".") { - last_dot = i - break - } - } - if (last_dot > 0) { - plugin_name = substr(rest, 1, last_dot - 1) - prop = substr(rest, last_dot + 1) - if (prop == "version") { - print "PLUGIN_VERSIONS[\"" plugin_name "\"]=\"" val "\"" - } else if (prop == "url") { - print "PLUGIN_URLS[\"" plugin_name "\"]=\"" val "\"" - } else if (prop == "bootstrap") { - print "PLUGIN_BOOTSTRAP_VERSIONS[\"" plugin_name "\"]=\"" val "\"" - } - } - } - }' + jq -r ' + .plugins | to_entries[] | + (if .value.version then "PLUGIN_VERSIONS[\"" + .key + "\"]=\"" + .value.version + "\"" else empty end), + (if .value.url then "PLUGIN_URLS[\"" + .key + "\"]=\"" + .value.url + "\"" else empty end), + (if .value.bootstrap then "PLUGIN_BOOTSTRAP_VERSIONS[\"" + .key + "\"]=\"" + .value.bootstrap + "\"" else empty end) + ' } # Ensures that the plugin sources file exists, initializing it with the official repository by default From c42687a710100bf326ac30cf069892e8712c5326 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 18:36:39 +0530 Subject: [PATCH 03/18] feat: Added registry helpers for installers --- bootstrap.sh | 4 +- lib/registry_helpers.sh | 133 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 lib/registry_helpers.sh diff --git a/bootstrap.sh b/bootstrap.sh index 26c6d0c..d0bbd4a 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -41,7 +41,7 @@ else BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR" _BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" - _LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/plugins.sh") + _LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/plugins.sh" "lib/registry_helpers.sh") _curl_args=() for _lib in "${_LIBS[@]}"; do @@ -56,6 +56,7 @@ if [ -f "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" ]; then . "$BOOTSTRAP_SOURCE_DIR/lib/rollback.sh" . "$BOOTSTRAP_SOURCE_DIR/lib/platform.sh" . "$BOOTSTRAP_SOURCE_DIR/lib/shell_config.sh" + . "$BOOTSTRAP_SOURCE_DIR/lib/registry_helpers.sh" init_rollback_system else echo "Error: Failed to locate or download bootstrap libraries." >&2 @@ -82,6 +83,7 @@ install_bootstrap() { "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" + "lib/registry_helpers.sh" "lib/plugins.sh" "commands/help.sh" "commands/con.sh" diff --git a/lib/registry_helpers.sh b/lib/registry_helpers.sh new file mode 100644 index 0000000..6339a37 --- /dev/null +++ b/lib/registry_helpers.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Registry management helpers for Bootstrap + +# Ensures the registry file exists +ensure_registry() { + local registry_file="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/registry.json" + if [ ! -f "$registry_file" ]; then + mkdir -p "$(dirname "$registry_file")" + echo '{"tools": {}}' > "$registry_file" + fi + echo "$registry_file" +} + +# Safely applies a jq filter to the registry using a file lock +registry_set() { + local jq_filter="$1" + shift + local registry_file + registry_file=$(ensure_registry) + local lock_file="${registry_file}.lock" + + ( + flock -x 200 + local temp_file + temp_file=$(mktemp) + # Apply jq filter with any additional arguments passed in + jq "$@" "$jq_filter" "$registry_file" > "$temp_file" && mv "$temp_file" "$registry_file" + ) 200>"$lock_file" +} + +# Usage: register_tool [version] [source] +register_tool() { + local tool="$1" + local strategy="$2" + local version="${3:-}" + local source="${4:-}" + local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + local bindir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/bin" + + local filter='if .tools == null then .tools = {} else . end | + .tools[$tool].strategy = $strategy | + .tools[$tool].installed_at = $timestamp | + (if $version != "" then .tools[$tool].version = $version else . end) | + (if $source != "" then .tools[$tool].source = $source else . end) | + (if $strategy == "binary" then .tools[$tool].bin = ($bindir + "/" + $tool) else . end)' + + registry_set "$filter" \ + --arg tool "$tool" \ + --arg strategy "$strategy" \ + --arg version "$version" \ + --arg source "$source" \ + --arg timestamp "$timestamp" \ + --arg bindir "$bindir" +} + +# Usage: registry_add_sys_deps ... +registry_add_sys_deps() { + local tool="$1" + shift + if [ $# -eq 0 ]; then + return 0 + fi + + local deps_json + deps_json=$(printf '%s\n' "$@" | jq -R . | jq -s .) + + local filter='if .tools == null then .tools = {} else . end | + .tools[$tool].system_dependencies = ((.tools[$tool].system_dependencies // []) + $deps | unique)' + + registry_set "$filter" --arg tool "$tool" --argjson deps "$deps_json" +} + +# Usage: registry_remove_tool +registry_remove_tool() { + local tool="$1" + registry_set 'del(.tools[$tool])' --arg tool "$tool" +} + +# Usage: registry_get_sys_deps +registry_get_sys_deps() { + local tool="$1" + local registry_file="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/registry.json" + if [ -f "$registry_file" ]; then + jq -r --arg tool "$tool" '.tools[$tool].system_dependencies[]? // empty' "$registry_file" + fi +} + +# Usage: registry_check +# Validates that a tool is actually installed according to its strategy +registry_check() { + local tool="$1" + local registry_file + registry_file=$(ensure_registry) + + local strategy + strategy=$(jq -r --arg tool "$tool" '.tools[$tool].strategy // empty' "$registry_file") + + if [ -z "$strategy" ]; then + return 1 + fi + + if [ "$strategy" = "binary" ]; then + local bin_path + bin_path=$(jq -r --arg tool "$tool" '.tools[$tool].bin // empty' "$registry_file") + if [ -n "$bin_path" ] && [ -x "$bin_path" ]; then + return 0 + fi + elif [ "$strategy" = "managed" ]; then + if command -v "$tool" >/dev/null 2>&1; then + return 0 + fi + elif [ "$strategy" = "system" ]; then + local deps=() + while IFS= read -r dep; do + [ -n "$dep" ] && deps+=("$dep") + done < <(registry_get_sys_deps "$tool") + + if [ ${#deps[@]} -eq 0 ]; then + if command -v "$tool" >/dev/null 2>&1; then + return 0 + fi + else + if pkg_check "${deps[@]}"; then + return 0 + fi + fi + fi + + return 1 +} + +export -f ensure_registry registry_set register_tool registry_add_sys_deps registry_remove_tool registry_get_sys_deps registry_check From f5a266ff70b88d1c86a0d3bfe8cc7e1769d76306 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 19:57:47 +0530 Subject: [PATCH 04/18] feat: Implement the github release helper with `github.sh` --- bootstrap.sh | 4 ++- installers/install_lazygit.sh | 10 ++----- installers/install_yazi.sh | 10 +++---- lib/github.sh | 53 +++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 lib/github.sh diff --git a/bootstrap.sh b/bootstrap.sh index d0bbd4a..0b5c89b 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -41,7 +41,7 @@ else BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR" _BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" - _LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/plugins.sh" "lib/registry_helpers.sh") + _LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/plugins.sh" "lib/registry_helpers.sh" "lib/github.sh") _curl_args=() for _lib in "${_LIBS[@]}"; do @@ -57,6 +57,7 @@ if [ -f "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" ]; then . "$BOOTSTRAP_SOURCE_DIR/lib/platform.sh" . "$BOOTSTRAP_SOURCE_DIR/lib/shell_config.sh" . "$BOOTSTRAP_SOURCE_DIR/lib/registry_helpers.sh" + . "$BOOTSTRAP_SOURCE_DIR/lib/github.sh" init_rollback_system else echo "Error: Failed to locate or download bootstrap libraries." >&2 @@ -84,6 +85,7 @@ install_bootstrap() { "lib/platform.sh" "lib/shell_config.sh" "lib/registry_helpers.sh" + "lib/github.sh" "lib/plugins.sh" "commands/help.sh" "commands/con.sh" diff --git a/installers/install_lazygit.sh b/installers/install_lazygit.sh index 4adbb88..0963c15 100755 --- a/installers/install_lazygit.sh +++ b/installers/install_lazygit.sh @@ -30,11 +30,7 @@ install_lazygit() { fi local latest_tag="" - if has_command curl; then - latest_tag=$(curl -sL https://api.github.com/repos/jesseduffield/lazygit/releases/latest \ - | grep '"tag_name":' | head -n1 \ - | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) - fi + latest_tag=$(github_get_latest_release "jesseduffield/lazygit") if [ -z "$latest_tag" ]; then latest_tag="v0.62.2" # fallback @@ -49,8 +45,6 @@ install_lazygit() { arch_str="arm64" fi - local url="https://github.com/jesseduffield/lazygit/releases/download/${latest_tag}/lazygit_${version}_linux_${arch_str}.tar.gz" - TMP_DIR="$(make_temp_dir)" cleanup() { rm -rf "$TMP_DIR"; } trap cleanup EXIT @@ -58,7 +52,7 @@ install_lazygit() { local dest="$TMP_DIR/lazygit.tar.gz" log_info "Downloading lazygit ${latest_tag}..." - download_file "$url" "$dest" + github_download_asset "jesseduffield/lazygit" "$latest_tag" "lazygit_${version}_linux_${arch_str}\.tar\.gz" "$dest" log_info "Extracting..." tar -xzf "$dest" -C "$TMP_DIR" diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index 5d6fc9d..af3a7c6 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -68,15 +68,15 @@ install_yazi() { pkg_install curl git log_info "Fetching latest Yazi version from GitHub..." - local latest_tag - latest_tag=$(curl -sL https://api.github.com/repos/sxyazi/yazi/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) + local latest_tag="" + latest_tag=$(github_get_latest_release "sxyazi/yazi") + if [ -z "$latest_tag" ]; then latest_tag="v26.5.6" fi - local deb_url="https://github.com/sxyazi/yazi/releases/download/${latest_tag}/yazi-x86_64-unknown-linux-gnu.deb" - log_info "Downloading Yazi ${latest_tag} from ${deb_url}..." - download_file "$deb_url" "$TMP_DIR/yazi.deb" + log_info "Downloading Yazi ${latest_tag}..." + github_download_asset "sxyazi/yazi" "$latest_tag" "yazi-x86_64-unknown-linux-gnu\.deb" "$TMP_DIR/yazi.deb" log_info "Installing Yazi package..." sudo apt install -y "$TMP_DIR/yazi.deb" diff --git a/lib/github.sh b/lib/github.sh new file mode 100644 index 0000000..c171669 --- /dev/null +++ b/lib/github.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# GitHub API helper functions for Bootstrap installers + +# Usage: github_get_latest_release +# Prints the tag_name of the latest release. +github_get_latest_release() { + local repo="$1" + local tag + tag=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" | jq -r '.tag_name // empty') + echo "$tag" +} + +# Usage: github_get_download_url +# Finds the asset matching the regex pattern in the specified release tag and prints its download URL. +github_get_download_url() { + local repo="$1" + local tag="$2" + local pattern="$3" + + # If the tag is exactly 'latest', fetch the latest release asset list + local endpoint + if [ "$tag" = "latest" ]; then + endpoint="https://api.github.com/repos/$repo/releases/latest" + else + endpoint="https://api.github.com/repos/$repo/releases/tags/$tag" + fi + + local url + url=$(curl -fsSL "$endpoint" | jq -r --arg regex "$pattern" '.assets[] | select(.name | test($regex; "i")) | .browser_download_url' | head -n1) + echo "$url" +} + +# Usage: github_download_asset +# Resolves the URL for the matching asset and downloads it to dest_file. +github_download_asset() { + local repo="$1" + local tag="$2" + local pattern="$3" + local dest="$4" + + local url + url=$(github_get_download_url "$repo" "$tag" "$pattern") + + if [ -z "$url" ]; then + log_error "Could not find asset matching regex '$pattern' for $repo@$tag" + return 1 + fi + + log_info "Downloading $url ..." + download_file "$url" "$dest" +} + +export -f github_get_latest_release github_get_download_url github_download_asset From 4eec27570e417986484989155a36114b0a931b27 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 20:11:54 +0530 Subject: [PATCH 05/18] refactor: Installers use github_get_latest_release helpers --- installers/install_asciicinema.sh | 9 ++------- installers/install_bat.sh | 7 +++---- installers/install_node.sh | 2 +- installers/install_nvim.sh | 4 +--- installers/install_pnpm.sh | 19 +++++++++++-------- installers/install_starship.sh | 16 +++++----------- installers/install_uv.sh | 14 ++++---------- lib/github.sh | 5 +++++ 8 files changed, 32 insertions(+), 44 deletions(-) diff --git a/installers/install_asciicinema.sh b/installers/install_asciicinema.sh index 1346bb8..fccc014 100644 --- a/installers/install_asciicinema.sh +++ b/installers/install_asciicinema.sh @@ -21,12 +21,9 @@ cleanup() { trap cleanup EXIT install_asciicinema() { - local latest_tag="" if has_command curl; then log_info "Fetching latest asciinema version from GitHub..." - latest_tag=$(curl -sL https://api.github.com/repos/asciinema/asciinema/releases/latest \ - | grep '"tag_name":' | head -n1 \ - | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) + latest_tag=$(github_get_latest_release "asciinema/asciinema") fi if [ -z "$latest_tag" ]; then @@ -73,10 +70,8 @@ install_asciicinema() { *) log_error "Unsupported architecture: $arch"; exit 1 ;; esac - local download_url="https://github.com/asciinema/asciinema/releases/download/${latest_tag}/asciinema-${asciinema_arch}" - log_info "Downloading asciinema ${latest_tag} for ${arch}..." - download_file "$download_url" "$TMP_DIR/asciinema" + github_download_asset "asciinema/asciinema" "$latest_tag" "asciinema-${asciinema_arch}" "$TMP_DIR/asciinema" log_info "Installing asciinema to /usr/local/bin..." sudo cp "$TMP_DIR/asciinema" /usr/local/bin/asciinema diff --git a/installers/install_bat.sh b/installers/install_bat.sh index 8fe6ccf..843e7d2 100644 --- a/installers/install_bat.sh +++ b/installers/install_bat.sh @@ -41,7 +41,7 @@ install_bat() { log_info "Fetching latest Bat version from GitHub..." local latest_tag="" - latest_tag=$(curl -sL https://api.github.com/repos/sharkdp/bat/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) + latest_tag=$(github_get_latest_release "sharkdp/bat") if [ -z "$latest_tag" ]; then latest_tag="v0.26.1" @@ -61,9 +61,8 @@ install_bat() { deb_arch="arm64" fi - local deb_url="https://github.com/sharkdp/bat/releases/download/${latest_tag}/bat_${version}_${deb_arch}.deb" - log_info "Downloading Bat from ${deb_url}..." - download_file "$deb_url" "$TMP_DIR/bat.deb" + log_info "Downloading Bat ${latest_tag}..." + github_download_asset "sharkdp/bat" "$latest_tag" "bat_${version}_${deb_arch}\.deb" "$TMP_DIR/bat.deb" log_info "Installing Bat package..." sudo apt install -y "$TMP_DIR/bat.deb" diff --git a/installers/install_node.sh b/installers/install_node.sh index bfdce16..a3198cf 100644 --- a/installers/install_node.sh +++ b/installers/install_node.sh @@ -34,7 +34,7 @@ install_nvm() { # Try to fetch the latest version of NVM from GitHub API log_info "Fetching the latest NVM version..." local latest_tag="" - latest_tag=$(curl -sL https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) + latest_tag=$(github_get_latest_release "nvm-sh/nvm") if [ -z "$latest_tag" ]; then latest_tag="v0.40.5" # Fallback version if API request fails diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index c5e4c08..773d5c4 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -76,10 +76,8 @@ install_nvim() { *) log_error "Unsupported architecture: $arch"; exit 1 ;; esac - local nvim_url="https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-${nvim_arch}.tar.gz" - log_info "Downloading Neovim v${NVIM_VERSION} for ${arch}..." - download_file "$nvim_url" "$TMP_DIR/nvim.tar.gz" + github_download_asset "neovim/neovim" "v${NVIM_VERSION}" "nvim-${nvim_arch}\.tar\.gz" "$TMP_DIR/nvim.tar.gz" tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR" diff --git a/installers/install_pnpm.sh b/installers/install_pnpm.sh index 46f8c2b..5053305 100644 --- a/installers/install_pnpm.sh +++ b/installers/install_pnpm.sh @@ -127,14 +127,17 @@ install_pnpm() { } libc_suffix="$(detect_libc_suffix)" - # Fetch the latest version from the npm registry, or use PNPM_VERSION if set + # Fetch the latest version from GitHub, 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." + log_info "Fetching latest pnpm version from GitHub..." + local tag + tag=$(github_get_latest_release "pnpm/pnpm") + if [ -n "$tag" ]; then + version="${tag#v}" + else + log_error "Failed to fetch pnpm version info from GitHub." return 1 - } - version="$(echo "$version_json" | grep -o '"latest":[[:space:]]*"[0-9.]*"' | grep -o '[0-9.]*')" + fi else version="${PNPM_VERSION}" fi @@ -151,7 +154,7 @@ install_pnpm() { 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" || { + github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}\.tar\.gz" "$TMP_DIR/pnpm.tar.gz" || { log_error "Failed to download pnpm tarball." return 1 } @@ -166,7 +169,7 @@ install_pnpm() { } else # Older versions: distributed as a single executable binary - download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}" "$TMP_DIR/pnpm" || { + github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}" "$TMP_DIR/pnpm" || { log_error "Failed to download pnpm binary." return 1 } diff --git a/installers/install_starship.sh b/installers/install_starship.sh index da2d144..29553dc 100644 --- a/installers/install_starship.sh +++ b/installers/install_starship.sh @@ -45,21 +45,15 @@ install_starship() { log_info "Fetching latest Starship version from GitHub..." local latest_tag="" - latest_tag=$(curl -sL https://api.github.com/repos/starship/starship/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 Starship version found: $latest_tag" - download_url="https://github.com/starship/starship/releases/download/${latest_tag}/starship-${target}.tar.gz" - else + latest_tag=$(github_get_latest_release "starship/starship") + + if [ -z "$latest_tag" ]; then latest_tag="latest" - log_warn "Failed to fetch latest version from GitHub. Falling back to downloading latest release directly." - download_url="https://github.com/starship/starship/releases/latest/download/starship-${target}.tar.gz" fi - log_info "Downloading Starship from ${download_url}..." + log_info "Downloading Starship ${latest_tag}..." local archive="$TMP_DIR/starship.tar.gz" - download_file "$download_url" "$archive" + github_download_asset "starship/starship" "$latest_tag" "starship-${target}\.tar\.gz" "$archive" # Extract the binary log_info "Extracting Starship binary..." diff --git a/installers/install_uv.sh b/installers/install_uv.sh index 64ca56a..f538a09 100644 --- a/installers/install_uv.sh +++ b/installers/install_uv.sh @@ -54,21 +54,15 @@ install_uv() { log_info "Fetching latest uv version from GitHub..." local latest_tag="" - latest_tag=$(curl -sL https://api.github.com/repos/astral-sh/uv/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) + latest_tag=$(github_get_latest_release "astral-sh/uv") - local download_url - if [ -n "$latest_tag" ]; then - log_info "Latest uv version found: $latest_tag" - download_url="https://github.com/astral-sh/uv/releases/download/${latest_tag}/uv-${target}.tar.gz" - else + if [ -z "$latest_tag" ]; then latest_tag="latest" - log_warn "Failed to fetch latest version from GitHub. Falling back to downloading latest release directly." - download_url="https://github.com/astral-sh/uv/releases/latest/download/uv-${target}.tar.gz" fi - log_info "Downloading uv from ${download_url}..." + log_info "Downloading uv ${latest_tag}..." local archive="$TMP_DIR/uv.tar.gz" - download_file "$download_url" "$archive" + github_download_asset "astral-sh/uv" "$latest_tag" "uv-${target}\.tar\.gz" "$archive" # Extract the binaries log_info "Extracting uv binaries..." diff --git a/lib/github.sh b/lib/github.sh index c171669..5f7d9e4 100644 --- a/lib/github.sh +++ b/lib/github.sh @@ -3,6 +3,11 @@ # Usage: github_get_latest_release # Prints the tag_name of the latest release. + +# Installers still use this function instead of just directly invoking download_asset function: +# - Asset names often contain the version +# - Installers may compare the latest tag from github against the locally installed version before doing any work. +# - We need concrete version string so we can pass it to the reigster_tool function. github_get_latest_release() { local repo="$1" local tag From 0eaea2c997e68aadecf6dee6cb611778b8abc984 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 20:19:19 +0530 Subject: [PATCH 06/18] refactor: Unify fragmented install strategies within installers bat and yazi installers use latest binary releases from github over package manager for arch and fedora --- installers/install_bat.sh | 92 +++++++++++++--------------- installers/install_yazi.sh | 120 +++++++++++++++++-------------------- 2 files changed, 99 insertions(+), 113 deletions(-) diff --git a/installers/install_bat.sh b/installers/install_bat.sh index 843e7d2..8a10b7e 100644 --- a/installers/install_bat.sh +++ b/installers/install_bat.sh @@ -21,57 +21,51 @@ cleanup() { trap cleanup EXIT install_bat() { - local distro - distro=$(detect_distro) - - if [ "$distro" = "arch" ]; then - log_info "Arch Linux detected" - log_info "Installing Bat..." - pkg_install bat - - elif [ "$distro" = "fedora" ]; then - log_info "Fedora detected" - log_info "Installing Bat..." - pkg_install bat - - elif [ "$distro" = "debian" ]; then - log_info "Debian/Ubuntu detected" - - pkg_install curl - - log_info "Fetching latest Bat version from GitHub..." - local latest_tag="" - latest_tag=$(github_get_latest_release "sharkdp/bat") - - if [ -z "$latest_tag" ]; then - latest_tag="v0.26.1" - log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag" - else - log_info "Latest Bat version found: $latest_tag" + if has_command bat; then + if ! confirm "Bat is already installed. Reinstall/Upgrade?"; then + log_info "Skipping Bat installation." + return fi - - # Remove leading 'v' for file name version - local version="${latest_tag#v}" - - # Detect architecture mapping - local arch - arch=$(detect_arch) - local deb_arch="amd64" - if [ "$arch" = "arm64" ]; then - deb_arch="arm64" - fi - - log_info "Downloading Bat ${latest_tag}..." - github_download_asset "sharkdp/bat" "$latest_tag" "bat_${version}_${deb_arch}\.deb" "$TMP_DIR/bat.deb" - - log_info "Installing Bat package..." - sudo apt install -y "$TMP_DIR/bat.deb" - add_rollback_cmd "sudo apt remove -y bat" - - else - log_error "Unsupported distribution." - exit 1 fi + + local arch + arch=$(detect_arch) + local target="" + case "$arch" in + x86_64) target="x86_64-unknown-linux-gnu" ;; + arm64) target="aarch64-unknown-linux-gnu" ;; + *) log_error "Unsupported architecture: $arch"; exit 1 ;; + esac + + log_info "Fetching latest Bat version from GitHub..." + local latest_tag="" + latest_tag=$(github_get_latest_release "sharkdp/bat") + + if [ -z "$latest_tag" ]; then + latest_tag="v0.26.1" + log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag" + else + log_info "Latest Bat version found: $latest_tag" + fi + + log_info "Downloading Bat ${latest_tag}..." + local archive="$TMP_DIR/bat.tar.gz" + github_download_asset "sharkdp/bat" "$latest_tag" "bat-${latest_tag}-${target}\.tar\.gz" "$archive" + + log_info "Extracting Bat binary..." + tar -xzf "$archive" -C "$TMP_DIR" + + local extract_dir="$TMP_DIR/bat-${latest_tag}-${target}" + + local target_dir="$HOME/.local/bin" + mkdir -p "$target_dir" + + log_info "Installing Bat to $target_dir/bat..." + cp "$extract_dir/bat" "$target_dir/bat" + chmod +x "$target_dir/bat" + track_file "$target_dir/bat" + + register_tool "bat" "binary" "$latest_tag" "github:sharkdp/bat" } configure_shell() { diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index af3a7c6..4cb4295 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -45,72 +45,64 @@ EOF } install_yazi() { - local distro - distro=$(detect_distro) - - if [ "$distro" = "arch" ]; then - log_info "Arch Linux detected" - if has_command yazi; then - log_info "Yazi is already installed." + if has_command yazi; then + if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then + log_info "Skipping Yazi installation." + return fi - - log_info "Installing Yazi..." - pkg_install yazi - log_info "Installing dependencies subsequently..." - pkg_install ffmpeg 7zip jq poppler fd ripgrep fzf zoxide resvg imagemagick - - elif [ "$distro" = "debian" ]; then - log_info "Debian/Ubuntu detected" - if has_command yazi; then - log_info "Yazi is already installed." - fi - - pkg_install curl git - - log_info "Fetching latest Yazi version from GitHub..." - local latest_tag="" - latest_tag=$(github_get_latest_release "sxyazi/yazi") - - if [ -z "$latest_tag" ]; then - latest_tag="v26.5.6" - fi - - log_info "Downloading Yazi ${latest_tag}..." - github_download_asset "sxyazi/yazi" "$latest_tag" "yazi-x86_64-unknown-linux-gnu\.deb" "$TMP_DIR/yazi.deb" - - log_info "Installing Yazi package..." - sudo apt install -y "$TMP_DIR/yazi.deb" - add_rollback_cmd "sudo apt remove -y yazi" - - log_info "Installing dependencies subsequently..." - pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick 7zip || \ - pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick p7zip-full - - create_fd_symlink - - elif [ "$distro" = "fedora" ]; then - log_info "Fedora detected" - if has_command yazi; then - log_info "Yazi is already installed." - fi - - log_info "Installing dnf-plugins-core..." - pkg_install dnf-plugins-core - - log_info "Enabling lihaohong/yazi copr repo..." - sudo dnf copr enable -y lihaohong/yazi - - log_info "Installing Yazi (without weak dependencies first)..." - sudo dnf install -y yazi --setopt=install_weak_deps=False - add_rollback_cmd "sudo dnf remove -y yazi" - - log_info "Installing weak dependencies subsequently..." - pkg_install yazi - - else - log_error "Unsupported distribution." - exit 1 fi + + # Ensure required extraction tools are installed + if ! has_command unzip; then + log_info "unzip not found. Installing unzip..." + pkg_install unzip + fi + + local arch + arch=$(detect_arch) + local target="" + case "$arch" in + x86_64) target="x86_64-unknown-linux-gnu" ;; + arm64) target="aarch64-unknown-linux-gnu" ;; + *) log_error "Unsupported architecture: $arch"; exit 1 ;; + esac + + log_info "Fetching latest Yazi version from GitHub..." + local latest_tag="" + latest_tag=$(github_get_latest_release "sxyazi/yazi") + + if [ -z "$latest_tag" ]; then + latest_tag="v0.3.3" + log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag" + fi + + log_info "Downloading Yazi ${latest_tag}..." + local archive="$TMP_DIR/yazi.zip" + github_download_asset "sxyazi/yazi" "$latest_tag" "yazi-${target}\.zip" "$archive" + + log_info "Extracting Yazi binaries..." + unzip -q "$archive" -d "$TMP_DIR" + + local extract_dir="$TMP_DIR/yazi-${target}" + local target_dir="$HOME/.local/bin" + mkdir -p "$target_dir" + + log_info "Installing Yazi to $target_dir..." + cp "$extract_dir/yazi" "$target_dir/yazi" + cp "$extract_dir/ya" "$target_dir/ya" + chmod +x "$target_dir/yazi" "$target_dir/ya" + track_file "$target_dir/yazi" + track_file "$target_dir/ya" + + log_info "Installing system dependencies for Yazi..." + pkg_install ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find" + + create_fd_symlink + + register_tool "yazi" "binary" "$latest_tag" "github:sxyazi/yazi" + + # Add the system dependencies to the registry for uninstallation tracking + registry_add_sys_deps "yazi" ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find" } main() { From 36c7be07b3bd30e728ec5de38e13f2ec7d96c144 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 21:35:34 +0530 Subject: [PATCH 07/18] refactor: Migrate all installers to use installation strategies --- installers/install_agy.sh | 2 ++ installers/install_asciicinema.sh | 2 ++ installers/install_bat.sh | 1 + installers/install_docker.sh | 2 ++ installers/install_lazygit.sh | 2 ++ installers/install_node.sh | 2 ++ installers/install_nvim.sh | 2 ++ installers/install_pnpm.sh | 2 ++ installers/install_rust.sh | 2 ++ installers/install_starship.sh | 2 ++ installers/install_uv.sh | 2 ++ installers/install_yay.sh | 2 ++ installers/install_yazi.sh | 1 + installers/install_zoxide.sh | 2 ++ lib/registry.sh | 17 +++++++++++++++++ scripts/generate_registry.sh | 10 ++++++++++ 16 files changed, 53 insertions(+) diff --git a/installers/install_agy.sh b/installers/install_agy.sh index 8eef314..4a8150d 100644 --- a/installers/install_agy.sh +++ b/installers/install_agy.sh @@ -2,6 +2,7 @@ # Tool: agy # DisplayName: Antigravity # Description: Install Antigravity CLI +# Strategy: binary # # Antigravity CLI Installer Script (Linux Only) # @@ -127,6 +128,7 @@ install_agy() { track_file "$BINARY_PATH" log_success "Antigravity CLI successfully installed to $BINARY_PATH." + register_tool "agy" "binary" "" "github:sortedcord/agy" } configure_shell() { diff --git a/installers/install_asciicinema.sh b/installers/install_asciicinema.sh index fccc014..d8fbe2a 100644 --- a/installers/install_asciicinema.sh +++ b/installers/install_asciicinema.sh @@ -2,6 +2,7 @@ # Tool: asciicinema # DisplayName: asciicinema # Description: Install asciinema terminal recorder +# Strategy: binary # # asciinema Installer Script # @@ -84,6 +85,7 @@ install_asciicinema() { track_file "/usr/local/bin/asciicinema" log_success "asciinema ${latest_tag} installed." + register_tool "asciicinema" "binary" "$latest_tag" "github:asciinema/asciinema" } main() { diff --git a/installers/install_bat.sh b/installers/install_bat.sh index 8a10b7e..557482f 100644 --- a/installers/install_bat.sh +++ b/installers/install_bat.sh @@ -2,6 +2,7 @@ # Tool: bat # DisplayName: Bat # Description: Install Bat (alternative to cat) and configure alias +# Strategy: binary # # Bat Installer Script # diff --git a/installers/install_docker.sh b/installers/install_docker.sh index af0e477..ac66dd0 100644 --- a/installers/install_docker.sh +++ b/installers/install_docker.sh @@ -2,6 +2,7 @@ # Tool: docker # DisplayName: Docker # Description: Container runtime and orchestration platform +# Strategy: system # # Docker Installer Script # @@ -58,6 +59,7 @@ install_docker() { sudo systemctl enable --now docker.service || true sudo systemctl enable --now containerd.service || true fi + register_tool "docker" "system" "" "os-package-manager" } # ─── Main ───────────────────────────────────────────────────────────── diff --git a/installers/install_lazygit.sh b/installers/install_lazygit.sh index 0963c15..9697b65 100755 --- a/installers/install_lazygit.sh +++ b/installers/install_lazygit.sh @@ -2,6 +2,7 @@ # Tool: lazygit # DisplayName: lazygit # Description: Simple terminal UI for git commands +# Strategy: binary # # lazygit Installer Script # @@ -61,6 +62,7 @@ install_lazygit() { cp "$TMP_DIR/lazygit" "$HOME/.local/bin/lazygit" chmod +x "$HOME/.local/bin/lazygit" track_file "$HOME/.local/bin/lazygit" + register_tool "lazygit" "binary" "$latest_tag" "github:jesseduffield/lazygit" } # ─── Main ───────────────────────────────────────────────────────────── diff --git a/installers/install_node.sh b/installers/install_node.sh index a3198cf..6ce4377 100644 --- a/installers/install_node.sh +++ b/installers/install_node.sh @@ -2,6 +2,7 @@ # Tool: node # DisplayName: Node # Description: Install Node.js (LTS) and NVM +# Strategy: managed # # Node.js and NVM Installer Script # @@ -95,6 +96,7 @@ install_node() { nvm alias default 'lts/*' log_success "Node.js installed successfully!" set -u + register_tool "node" "managed" "$latest_tag" "github:nvm-sh/nvm" } main() { diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index 773d5c4..943e393 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -2,6 +2,7 @@ # Tool: nvim # DisplayName: Neovim # Description: Install Neovim 0.12.0 and configuration +# Strategy: binary # # Neovim Installer Script # @@ -91,6 +92,7 @@ install_nvim() { log_success "Installed:" nvim --version | head -n1 + register_tool "nvim" "binary" "$NVIM_VERSION" "github:neovim/neovim" } install_config() { diff --git a/installers/install_pnpm.sh b/installers/install_pnpm.sh index 5053305..0db0559 100644 --- a/installers/install_pnpm.sh +++ b/installers/install_pnpm.sh @@ -2,6 +2,7 @@ # Tool: pnpm # DisplayName: Pnpm # Description: Install pnpm package manager +# Strategy: binary # # pnpm Installer Script # @@ -182,6 +183,7 @@ install_pnpm() { track_dir "$HOME/.local/share/pnpm" log_success "pnpm v${version} installed successfully!" + register_tool "pnpm" "binary" "$version" "github:pnpm/pnpm" } # ─── Shell Configuration ───────────────────────────────────────────── diff --git a/installers/install_rust.sh b/installers/install_rust.sh index e6db38f..09c81c6 100644 --- a/installers/install_rust.sh +++ b/installers/install_rust.sh @@ -2,6 +2,7 @@ # Tool: rust # DisplayName: Rust # Description: Install Rustup and Rust compiler/toolchain +# Strategy: managed # # Rust Installer Script (Simplified Local Rustup Init) # @@ -87,6 +88,7 @@ install_rust() { "$dest" -y --no-modify-path add_rollback_cmd "rustup self uninstall -y" + register_tool "rust" "managed" "" "rustup" } configure_shell() { diff --git a/installers/install_starship.sh b/installers/install_starship.sh index 29553dc..4ef2d70 100644 --- a/installers/install_starship.sh +++ b/installers/install_starship.sh @@ -2,6 +2,7 @@ # Tool: starship # DisplayName: Starship # Description: Install Starship shell prompt +# Strategy: binary # # Starship Installer Script # @@ -66,6 +67,7 @@ install_starship() { cp "$TMP_DIR/starship" "$target_dir/starship" chmod +x "$target_dir/starship" track_file "$target_dir/starship" + register_tool "starship" "binary" "$latest_tag" "github:starship/starship" } configure_shell() { diff --git a/installers/install_uv.sh b/installers/install_uv.sh index f538a09..0c89324 100644 --- a/installers/install_uv.sh +++ b/installers/install_uv.sh @@ -2,6 +2,7 @@ # Tool: uv # DisplayName: uv # Description: Fast Python package installer and resolver +# Strategy: binary # # uv Installer Script # @@ -77,6 +78,7 @@ install_uv() { chmod +x "$target_dir/uv" "$target_dir/uvx" track_file "$target_dir/uv" track_file "$target_dir/uvx" + register_tool "uv" "binary" "$latest_tag" "github:astral-sh/uv" } configure_shell() { diff --git a/installers/install_yay.sh b/installers/install_yay.sh index aaa5a8d..34bb8a7 100755 --- a/installers/install_yay.sh +++ b/installers/install_yay.sh @@ -2,6 +2,7 @@ # Tool: yay # DisplayName: Yay # Description: Install Yay AUR helper +# Strategy: system # # Yay Installer Script # @@ -66,6 +67,7 @@ install_yay() { cd "$orig_dir" log_info "Cleaning up installer directory..." rm -rf "$clone_dir" + register_tool "yay" "system" "" "aur:yay-bin" } # ─── Main ───────────────────────────────────────────────────────────── diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index 4cb4295..cd193e3 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -2,6 +2,7 @@ # Tool: yazi # DisplayName: Yazi # Description: Install Yazi terminal file manager and dependencies +# Strategy: binary # # Yazi Installer Script # diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index 110baa3..957cc66 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -2,6 +2,7 @@ # Tool: zoxide # DisplayName: Zoxide # Description: Install Zoxide directory jumper +# Strategy: managed # # Zoxide Installer Script # @@ -41,6 +42,7 @@ install_zoxide() { log_info "Downloading and running the official zoxide installer..." curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh track_file "$HOME/.local/bin/zoxide" + register_tool "zoxide" "managed" "" "github:ajeetdsouza/zoxide" } configure_shell() { diff --git a/lib/registry.sh b/lib/registry.sh index abbb9b4..d465f00 100644 --- a/lib/registry.sh +++ b/lib/registry.sh @@ -34,4 +34,21 @@ declare -A INSTALLER_DISPLAYS=( [zoxide]="Zoxide" ) +declare -A INSTALLER_STRATEGIES=( + [agy]="binary" + [asciicinema]="binary" + [bat]="binary" + [docker]="system" + [lazygit]="binary" + [node]="managed" + [nvim]="binary" + [pnpm]="binary" + [rust]="managed" + [starship]="binary" + [uv]="binary" + [yay]="system" + [yazi]="binary" + [zoxide]="managed" +) + INSTALLER_KEYS=(agy asciicinema bat docker lazygit node nvim pnpm rust starship uv yay yazi zoxide) diff --git a/scripts/generate_registry.sh b/scripts/generate_registry.sh index e95a916..e3cbf99 100755 --- a/scripts/generate_registry.sh +++ b/scripts/generate_registry.sh @@ -13,6 +13,7 @@ echo "==> Generating registry.sh..." # Temporary arrays declare -A tools_desc declare -A tools_disp +declare -A tools_strat keys=() for f in "$INSTALLERS_DIR"/install_*.sh; do @@ -20,10 +21,12 @@ for f in "$INSTALLERS_DIR"/install_*.sh; do tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I') disp_name=$(grep -E "^# DisplayName:" "$f" | head -n1 | sed -E 's/^# DisplayName:\s*//I') desc=$(grep -E "^# Description:" "$f" | head -n1 | sed -E 's/^# Description:\s*//I' | sed -E 's/^Install\s+//I') + strat=$(grep -E "^# Strategy:" "$f" | head -n1 | sed -E 's/^# Strategy:\s*//I') if [ -n "$tool" ]; then tools_desc["$tool"]="$desc" tools_disp["$tool"]="${disp_name:-$tool}" + tools_strat["$tool"]="${strat:-unknown}" keys+=("$tool") fi done @@ -49,6 +52,13 @@ sorted_keys=($(printf '%s\n' "${keys[@]}" | sort)) done echo ")" echo "" + echo "declare -A INSTALLER_STRATEGIES=(" + for k in "${sorted_keys[@]}"; do + escaped_strat=$(echo "${tools_strat[$k]}" | sed 's/"/\\"/g') + echo " [$k]=\"$escaped_strat\"" + done + echo ")" + echo "" # Format keys output as space-separated list in array declaration format echo "INSTALLER_KEYS=(${sorted_keys[*]})" } > "$REGISTRY_FILE" From a4e5bc1175c475d6fbe8f090326e0f5155927409 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 21:37:11 +0530 Subject: [PATCH 08/18] refactor: Remove legacy backwards compat code Remove the configure shell code blocks --- installers/install_agy.sh | 5 ----- installers/install_bat.sh | 8 -------- installers/install_node.sh | 5 ----- installers/install_nvim.sh | 18 ------------------ installers/install_pnpm.sh | 5 ----- installers/install_rust.sh | 5 ----- installers/install_starship.sh | 6 ------ installers/install_uv.sh | 6 ------ installers/install_yazi.sh | 5 ----- installers/install_zoxide.sh | 5 ----- 10 files changed, 68 deletions(-) diff --git a/installers/install_agy.sh b/installers/install_agy.sh index 4a8150d..7685fb3 100644 --- a/installers/install_agy.sh +++ b/installers/install_agy.sh @@ -132,11 +132,6 @@ install_agy() { } configure_shell() { - # 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"' } diff --git a/installers/install_bat.sh b/installers/install_bat.sh index 557482f..2228aba 100644 --- a/installers/install_bat.sh +++ b/installers/install_bat.sh @@ -70,14 +70,6 @@ install_bat() { } configure_shell() { - # 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" "bat alias" - done - if [ -f "$HOME/.bash_aliases" ]; then - remove_block "$HOME/.bash_aliases" "bat alias" - fi write_alias_snippet "bat" "alias cat='bat --paging=never -p'" } diff --git a/installers/install_node.sh b/installers/install_node.sh index 6ce4377..fc27986 100644 --- a/installers/install_node.sh +++ b/installers/install_node.sh @@ -58,11 +58,6 @@ install_nvm() { } configure_shell() { - # 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" "nvm setup" - done local content content=$(cat << 'EOF' diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index 943e393..e0c4ab2 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -111,24 +111,6 @@ install_config() { } configure_shell() { - # Clean up legacy inline edits from bashrc and bash_aliases - IFS=' ' read -ra target_files <<< "$(get_shell_configs)" - for config_file in "${target_files[@]}"; do - if [ -f "$config_file" ]; then - local tmp_file - tmp_file=$(mktemp) - sed '/^export EDITOR="nvim"/d' "$config_file" > "$tmp_file" - cat "$tmp_file" > "$config_file" - rm -f "$tmp_file" - fi - done - if [ -f "$HOME/.bash_aliases" ]; then - local tmp_file - tmp_file=$(mktemp) - sed '/^alias vim="nvim"/d' "$HOME/.bash_aliases" > "$tmp_file" - cat "$tmp_file" > "$HOME/.bash_aliases" - rm -f "$tmp_file" - fi write_alias_snippet "nvim" 'alias vim="nvim"' write_env_snippet "nvim" 'export EDITOR="nvim"' diff --git a/installers/install_pnpm.sh b/installers/install_pnpm.sh index 0db0559..729719c 100644 --- a/installers/install_pnpm.sh +++ b/installers/install_pnpm.sh @@ -189,11 +189,6 @@ install_pnpm() { # ─── Shell Configuration ───────────────────────────────────────────── configure_shell() { - # 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" "pnpm setup" - done # pnpm's `setup --force` configures PNPM_HOME and PATH automatically, # but we also add an env block to ensure PNPM_HOME is set consistently. diff --git a/installers/install_rust.sh b/installers/install_rust.sh index 09c81c6..065ac5c 100644 --- a/installers/install_rust.sh +++ b/installers/install_rust.sh @@ -95,11 +95,6 @@ configure_shell() { # Add ~/.cargo/bin to PATH for the current process export PATH="$HOME/.cargo/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" "rust init" - done write_env_snippet "rust" '. "$HOME/.cargo/env"' } diff --git a/installers/install_starship.sh b/installers/install_starship.sh index 4ef2d70..3f2f6a0 100644 --- a/installers/install_starship.sh +++ b/installers/install_starship.sh @@ -74,12 +74,6 @@ 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" - remove_block "$config_file" "starship init" - done write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"' write_env_snippet "starship" 'eval "$(starship init bash)"' diff --git a/installers/install_uv.sh b/installers/install_uv.sh index 0c89324..baf0a77 100644 --- a/installers/install_uv.sh +++ b/installers/install_uv.sh @@ -85,12 +85,6 @@ 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" - remove_block "$config_file" "uv completion" - done write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"' write_env_snippet "uv" 'eval "$(uv generate-shell-completion bash)"' diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index cd193e3..5c70f71 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -22,11 +22,6 @@ cleanup() { trap cleanup EXIT add_y_wrapper() { - # 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" "yazi wrapper" - done local wrapper_content wrapper_content=$(cat << 'EOF' diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index 957cc66..c03f137 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -49,11 +49,6 @@ 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" "zoxide init" - done write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"' write_env_snippet "zoxide" 'eval "$(zoxide init --cmd cd bash)"' From 29de051b7d30a2fc303e3e5c7d97c428ef40f2db Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 21:40:48 +0530 Subject: [PATCH 09/18] refactor: Remove standalone exec prevention code blocks from installers --- installers/install_agy.sh | 6 ------ installers/install_asciicinema.sh | 6 ------ installers/install_bat.sh | 6 ------ installers/install_docker.sh | 6 ------ installers/install_lazygit.sh | 6 ------ installers/install_node.sh | 6 ------ installers/install_nvim.sh | 6 ------ installers/install_pnpm.sh | 6 ------ installers/install_rust.sh | 6 ------ installers/install_starship.sh | 6 ------ installers/install_uv.sh | 6 ------ installers/install_yay.sh | 6 ------ installers/install_yazi.sh | 6 ------ installers/install_zoxide.sh | 6 ------ 14 files changed, 84 deletions(-) diff --git a/installers/install_agy.sh b/installers/install_agy.sh index 7685fb3..69335ff 100644 --- a/installers/install_agy.sh +++ b/installers/install_agy.sh @@ -7,12 +7,6 @@ # Antigravity CLI Installer Script (Linux Only) # -# 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 # Constants diff --git a/installers/install_asciicinema.sh b/installers/install_asciicinema.sh index d8fbe2a..5ba51e1 100644 --- a/installers/install_asciicinema.sh +++ b/installers/install_asciicinema.sh @@ -7,12 +7,6 @@ # asciinema 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)" diff --git a/installers/install_bat.sh b/installers/install_bat.sh index 2228aba..0462470 100644 --- a/installers/install_bat.sh +++ b/installers/install_bat.sh @@ -7,12 +7,6 @@ # Bat 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)" diff --git a/installers/install_docker.sh b/installers/install_docker.sh index ac66dd0..2b0e382 100644 --- a/installers/install_docker.sh +++ b/installers/install_docker.sh @@ -7,12 +7,6 @@ # Docker 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 # ─── Installation Logic ────────────────────────────────────────────── diff --git a/installers/install_lazygit.sh b/installers/install_lazygit.sh index 9697b65..f90aad6 100755 --- a/installers/install_lazygit.sh +++ b/installers/install_lazygit.sh @@ -7,12 +7,6 @@ # lazygit 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 # ─── Installation Logic ────────────────────────────────────────────── diff --git a/installers/install_node.sh b/installers/install_node.sh index fc27986..3dce836 100644 --- a/installers/install_node.sh +++ b/installers/install_node.sh @@ -7,12 +7,6 @@ # Node.js and NVM 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)" diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index e0c4ab2..5b4e97a 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -7,12 +7,6 @@ # Neovim 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 NVIM_VERSION="0.12.0" diff --git a/installers/install_pnpm.sh b/installers/install_pnpm.sh index 729719c..27eec6a 100644 --- a/installers/install_pnpm.sh +++ b/installers/install_pnpm.sh @@ -18,12 +18,6 @@ # curl -fsSL https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash - # -# 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)" diff --git a/installers/install_rust.sh b/installers/install_rust.sh index 065ac5c..351c97b 100644 --- a/installers/install_rust.sh +++ b/installers/install_rust.sh @@ -7,12 +7,6 @@ # Rust Installer Script (Simplified Local Rustup Init) # -# 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)" diff --git a/installers/install_starship.sh b/installers/install_starship.sh index 3f2f6a0..3ee872f 100644 --- a/installers/install_starship.sh +++ b/installers/install_starship.sh @@ -7,12 +7,6 @@ # Starship 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)" diff --git a/installers/install_uv.sh b/installers/install_uv.sh index baf0a77..159b64f 100644 --- a/installers/install_uv.sh +++ b/installers/install_uv.sh @@ -7,12 +7,6 @@ # uv 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)" diff --git a/installers/install_yay.sh b/installers/install_yay.sh index 34bb8a7..105f0b9 100755 --- a/installers/install_yay.sh +++ b/installers/install_yay.sh @@ -7,12 +7,6 @@ # Yay 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 # ─── Installation Logic ────────────────────────────────────────────── diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index 5c70f71..dc80b0c 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -7,12 +7,6 @@ # Yazi 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)" diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index c03f137..5d72b73 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -7,12 +7,6 @@ # Zoxide 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 install_curl() { From 4c1c7de0b7bf76a3d08f33c76453bf3995513367 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 21:49:24 +0530 Subject: [PATCH 10/18] refactor: Remove redundant curl availability checks --- .agents/skills/add_installer/SKILL.md | 3 +-- bootstrap.sh | 5 ----- installers/install_nvim.sh | 2 +- installers/install_rust.sh | 10 +--------- installers/install_starship.sh | 6 ------ installers/install_uv.sh | 6 ------ installers/install_zoxide.sh | 8 +------- 7 files changed, 4 insertions(+), 36 deletions(-) diff --git a/.agents/skills/add_installer/SKILL.md b/.agents/skills/add_installer/SKILL.md index b7c2cc0..947be75 100644 --- a/.agents/skills/add_installer/SKILL.md +++ b/.agents/skills/add_installer/SKILL.md @@ -200,14 +200,13 @@ trap cleanup EXIT ### Distro-specific mapping ```bash -pkg_install "arch:neovim|debian:nvim|fedora:neovim" "curl" "git" +pkg_install "arch:neovim|debian:nvim|fedora:neovim" "git" ``` ### Fetching latest GitHub release tag ```bash local latest_tag="" -if has_command curl; then latest_tag=$(curl -sL https://api.github.com/repos///releases/latest \ | grep '"tag_name":' | head -n1 \ | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) diff --git a/bootstrap.sh b/bootstrap.sh index 0b5c89b..3c6366d 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -8,11 +8,6 @@ if [ -z "${BASH_VERSION:-}" ]; then exit 1 fi -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required to run this script." >&2 - exit 1 -fi - # Detect if the script is sourced is_sourced=false if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index 5b4e97a..87c60b1 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -28,7 +28,7 @@ check_config_dir() { install_packages() { log_info "Detecting distribution and installing dependencies..." pkg_install \ - git tar curl unzip ripgrep fzf nodejs npm xclip wl-clipboard \ + git tar unzip ripgrep fzf nodejs npm xclip wl-clipboard \ "arch:fd|debian:fd-find|fedora:fd-find" \ "arch:cmake|debian:cmake|fedora:cmake" \ "arch:make|debian:build-essential|fedora:make" \ diff --git a/installers/install_rust.sh b/installers/install_rust.sh index 351c97b..2107376 100644 --- a/installers/install_rust.sh +++ b/installers/install_rust.sh @@ -15,14 +15,6 @@ cleanup() { } trap cleanup EXIT -# Ensure we have curl -install_downloader() { - if ! has_command curl; then - log_info "curl not found. Installing curl..." - pkg_install curl - fi -} - detect_target_triple() { local ostype ostype="$(uname -s)" @@ -60,7 +52,7 @@ install_rust() { log_info "Rust (rustup) is already installed." fi - install_downloader + local target target=$(detect_target_triple) diff --git a/installers/install_starship.sh b/installers/install_starship.sh index 3ee872f..ec89f86 100644 --- a/installers/install_starship.sh +++ b/installers/install_starship.sh @@ -20,12 +20,6 @@ install_starship() { log_info "Starship is already installed." 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) diff --git a/installers/install_uv.sh b/installers/install_uv.sh index 159b64f..9d79cc3 100644 --- a/installers/install_uv.sh +++ b/installers/install_uv.sh @@ -23,12 +23,6 @@ install_uv() { 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) diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index 5d72b73..659eb1e 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -9,12 +9,6 @@ set -euo pipefail -install_curl() { - if ! has_command curl; then - log_info "curl not found. Installing curl..." - pkg_install curl - fi -} install_fzf() { if has_command fzf; then @@ -31,7 +25,7 @@ install_zoxide() { log_info "Zoxide is already installed." fi - install_curl + log_info "Downloading and running the official zoxide installer..." curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh From d5c90d6e85f48716fc989eb3137d2a9b7c73ba84 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 23:52:03 +0530 Subject: [PATCH 11/18] refactor: Use XDG compliant isolated directory structure - Using $BOOTSTRAP_BIN, $BOOTSTRAP_OPT, etc - Add defensive fallback for undefined vars in common.sh --- .agents/skills/add_installer/SKILL.md | 6 +++--- bootstrap.sh | 26 +++++++++++++++++++++++++- commands/uninstall.sh | 3 +++ installers/install_agy.sh | 3 +-- installers/install_asciicinema.sh | 10 +++++----- installers/install_bat.sh | 2 +- installers/install_lazygit.sh | 2 +- installers/install_node.sh | 20 ++++++++++---------- installers/install_nvim.sh | 12 +++++++----- installers/install_pnpm.sh | 2 +- installers/install_rust.sh | 15 +++++++++++---- installers/install_starship.sh | 5 +---- installers/install_uv.sh | 5 +---- installers/install_yazi.sh | 3 +-- installers/install_zoxide.sh | 3 --- lib/common.sh | 11 ++++++++++- lib/registry_helpers.sh | 6 +++--- 17 files changed, 84 insertions(+), 50 deletions(-) diff --git a/.agents/skills/add_installer/SKILL.md b/.agents/skills/add_installer/SKILL.md index 947be75..3a65d7f 100644 --- a/.agents/skills/add_installer/SKILL.md +++ b/.agents/skills/add_installer/SKILL.md @@ -59,7 +59,7 @@ The central router `lib/routes.sh` and autocomplete function in `b.sh` will dyna ### Step 3: Implement Rollback Tracking (Crucial) To ensure the user can seamlessly use `b rb `, all manual modifications must be tracked: -- When extracting binaries to `~/.local/bin/`, use `track_file "$HOME/.local/bin/binary"`. +- When extracting binaries to `~/.local/bin/`, use `track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/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. @@ -116,8 +116,8 @@ install_() { # Or manual downloads (always use download_file for resumability!): # local url="https://..." # download_file "$url" "$TMP_DIR/binary" - # cp "$TMP_DIR/binary" "$HOME/.local/bin/binary" - # track_file "$HOME/.local/bin/binary" # Important for rollback! + # cp "$TMP_DIR/binary" "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary" + # track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary" # Important for rollback! } # ─── Shell Configuration (if needed) ───────────────────────────────── diff --git a/bootstrap.sh b/bootstrap.sh index 3c6366d..53ab262 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -65,10 +65,27 @@ install_bootstrap() { [ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc") local routes_dir="$HOME/.config/bootstrap" - mkdir -p "$routes_dir" mkdir -p "$routes_dir/env.d" mkdir -p "$routes_dir/aliases.d" + # Initialize XDG directories + mkdir -p "$HOME/.local/share/bootstrap/bin" + mkdir -p "$HOME/.local/share/bootstrap/opt" + mkdir -p "$HOME/.local/share/bootstrap/runtimes" + mkdir -p "$HOME/.local/state/bootstrap/logs" + mkdir -p "$HOME/.local/state/bootstrap/rollback" + mkdir -p "$HOME/.cache/bootstrap/downloads" + mkdir -p "$HOME/.cache/bootstrap/tmp" + + # Create the universal binary PATH snippet + cat << 'EOF' > "$routes_dir/env.d/bootstrap-bin.sh" +export BOOTSTRAP_BIN="$BOOTSTRAP_BIN" +case ":$PATH:" in + *":$BOOTSTRAP_BIN:"*) ;; + *) export PATH="$BOOTSTRAP_BIN:$PATH" ;; +esac +EOF + # List of all files to download/copy local files=( "VERSION" @@ -142,6 +159,13 @@ install_bootstrap() { # >>> bootstrap-cli setup >>> export BOOTSTRAP_DIR="$HOME/.config/bootstrap" +export BOOTSTRAP_DATA_DIR="$HOME/.local/share/bootstrap" +export BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap" +export BOOTSTRAP_CACHE_DIR="$HOME/.cache/bootstrap" +export BOOTSTRAP_BIN="$BOOTSTRAP_DATA_DIR/bin" +export BOOTSTRAP_OPT="$BOOTSTRAP_DATA_DIR/opt" +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 diff --git a/commands/uninstall.sh b/commands/uninstall.sh index 8dde304..a4934aa 100644 --- a/commands/uninstall.sh +++ b/commands/uninstall.sh @@ -78,6 +78,9 @@ EOF fi # Remove the installation directory +rm -rf "$BOOTSTRAP_DATA_DIR" +rm -rf "$BOOTSTRAP_STATE_DIR" +rm -rf "$BOOTSTRAP_CACHE_DIR" rm -rf "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}" if [ "$FORCE" = "true" ]; then diff --git a/installers/install_agy.sh b/installers/install_agy.sh index 69335ff..793a086 100644 --- a/installers/install_agy.sh +++ b/installers/install_agy.sh @@ -11,7 +11,7 @@ set -euo pipefail # Constants DOWNLOAD_BASE_URL="https://antigravity-cli-auto-updater-974169037036.us-central1.run.app" -TARGET_DIR="$HOME/.local/bin" +TARGET_DIR="$BOOTSTRAP_BIN" BINARY_PATH="$TARGET_DIR/agy" install_agy() { @@ -127,7 +127,6 @@ install_agy() { configure_shell() { - write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"' } run_handoff() { diff --git a/installers/install_asciicinema.sh b/installers/install_asciicinema.sh index 5ba51e1..f9e1dd4 100644 --- a/installers/install_asciicinema.sh +++ b/installers/install_asciicinema.sh @@ -68,14 +68,14 @@ install_asciicinema() { log_info "Downloading asciinema ${latest_tag} for ${arch}..." github_download_asset "asciinema/asciinema" "$latest_tag" "asciinema-${asciinema_arch}" "$TMP_DIR/asciinema" - log_info "Installing asciinema to /usr/local/bin..." - sudo cp "$TMP_DIR/asciinema" /usr/local/bin/asciinema - sudo chmod +x /usr/local/bin/asciinema - track_file "/usr/local/bin/asciinema" + log_info "Installing asciinema to $BOOTSTRAP_BIN..." + cp "$TMP_DIR/asciinema" "$BOOTSTRAP_BIN/asciinema" + chmod +x "$BOOTSTRAP_BIN/asciinema" + track_file "$BOOTSTRAP_BIN/asciinema" # Create compatibility symlink matching the installer name spelling log_info "Creating compatibility symlink for asciicinema..." - sudo ln -sf /usr/local/bin/asciinema /usr/local/bin/asciicinema + ln -sf "$BOOTSTRAP_BIN/asciinema" /usr/local/bin/asciicinema track_file "/usr/local/bin/asciicinema" log_success "asciinema ${latest_tag} installed." diff --git a/installers/install_bat.sh b/installers/install_bat.sh index 0462470..cf4a77a 100644 --- a/installers/install_bat.sh +++ b/installers/install_bat.sh @@ -52,7 +52,7 @@ install_bat() { local extract_dir="$TMP_DIR/bat-${latest_tag}-${target}" - local target_dir="$HOME/.local/bin" + local target_dir="$BOOTSTRAP_BIN" mkdir -p "$target_dir" log_info "Installing Bat to $target_dir/bat..." diff --git a/installers/install_lazygit.sh b/installers/install_lazygit.sh index f90aad6..ed52d77 100755 --- a/installers/install_lazygit.sh +++ b/installers/install_lazygit.sh @@ -52,7 +52,7 @@ install_lazygit() { log_info "Extracting..." tar -xzf "$dest" -C "$TMP_DIR" - mkdir -p "$HOME/.local/bin" + mkdir -p "$BOOTSTRAP_BIN" cp "$TMP_DIR/lazygit" "$HOME/.local/bin/lazygit" chmod +x "$HOME/.local/bin/lazygit" track_file "$HOME/.local/bin/lazygit" diff --git a/installers/install_node.sh b/installers/install_node.sh index 3dce836..f730527 100644 --- a/installers/install_node.sh +++ b/installers/install_node.sh @@ -16,7 +16,7 @@ cleanup() { trap cleanup EXIT install_nvm() { - if has_command nvm || [ -s "$HOME/.nvm/nvm.sh" ]; then + if has_command nvm || [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then log_info "NVM is already installed." fi @@ -42,20 +42,20 @@ install_nvm() { log_info "Downloading NVM from $nvm_url..." download_file "$nvm_url" "$TMP_DIR/nvm.tar.gz" - log_info "Extracting NVM archive directly to $HOME/.nvm (stripping versioned subfolder to keep config generic)..." - mkdir -p "$HOME/.nvm" - tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1 + log_info "Extracting NVM archive directly to $BOOTSTRAP_RUNTIMES/nvm (stripping versioned subfolder to keep config generic)..." + mkdir -p "$BOOTSTRAP_RUNTIMES/nvm" + tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$BOOTSTRAP_RUNTIMES/nvm" --strip-components=1 - track_dir "$HOME/.nvm" + track_dir "$BOOTSTRAP_RUNTIMES/nvm" - log_success "NVM source files successfully extracted to $HOME/.nvm." + log_success "NVM source files successfully extracted to $BOOTSTRAP_RUNTIMES/nvm." } configure_shell() { local content content=$(cat << 'EOF' -export NVM_DIR="$HOME/.nvm" +export NVM_DIR="$BOOTSTRAP_RUNTIMES/nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load NVM [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # Load NVM bash completion EOF @@ -66,10 +66,10 @@ EOF install_node() { # Ensure NVM is loaded in this script context - if [ -s "$HOME/.nvm/nvm.sh" ]; then + if [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then # Temporarily disable nounset as nvm.sh does not support set -u set +u - . "$HOME/.nvm/nvm.sh" + . "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" else log_error "Could not load NVM to install Node.js." return 1 @@ -97,7 +97,7 @@ main() { if has_command node; then log_success "Node.js (via NVM) installation and configuration complete." log_info "Installed Node version: $(node --version)" - log_info "Installed NVM version: $(nvm --version 2>/dev/null || cat "$HOME/.nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")" + log_info "Installed NVM version: $(nvm --version 2>/dev/null || cat "$BOOTSTRAP_RUNTIMES/nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")" else log_success "Installation complete." fi diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index 87c60b1..12f3ac3 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -10,7 +10,8 @@ set -euo pipefail NVIM_VERSION="0.12.0" -NVIM_INSTALL_DIR="/opt/nvim" +NVIM_INSTALL_DIR="$BOOTSTRAP_OPT/nvim" +NVIM_BIN_DIR="$BOOTSTRAP_BIN" NVIM_CONFIG_REPO="https://git.adityagupta.dev/sortedcord/editor.git" NVIM_CONFIG_DIR="$HOME/.config/nvim" @@ -76,13 +77,14 @@ install_nvim() { tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR" - sudo rm -rf "$NVIM_INSTALL_DIR" - sudo mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR" + rm -rf "$NVIM_INSTALL_DIR" + mkdir -p "$(dirname "$NVIM_INSTALL_DIR")" + mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR" - sudo ln -sf "$NVIM_INSTALL_DIR/bin/nvim" /usr/local/bin/nvim + ln -sf "$NVIM_INSTALL_DIR/bin/nvim" "$NVIM_BIN_DIR/nvim" track_dir "$NVIM_INSTALL_DIR" - track_file "/usr/local/bin/nvim" + track_file "$NVIM_BIN_DIR/nvim" log_success "Installed:" nvim --version | head -n1 diff --git a/installers/install_pnpm.sh b/installers/install_pnpm.sh index 27eec6a..b5422ad 100644 --- a/installers/install_pnpm.sh +++ b/installers/install_pnpm.sh @@ -189,7 +189,7 @@ configure_shell() { local content content=$(cat << 'EOF' # pnpm -export PNPM_HOME="$HOME/.local/share/pnpm" +export PNPM_HOME="$BOOTSTRAP_RUNTIMES/pnpm" case ":$PATH:" in *":$PNPM_HOME:"*) ;; *) export PATH="$PNPM_HOME:$PATH" ;; diff --git a/installers/install_rust.sh b/installers/install_rust.sh index 2107376..a5eb901 100644 --- a/installers/install_rust.sh +++ b/installers/install_rust.sh @@ -48,7 +48,10 @@ detect_target_triple() { } install_rust() { - if has_command rustup || [ -f "$HOME/.cargo/bin/rustup" ]; then + export CARGO_HOME="$BOOTSTRAP_RUNTIMES/cargo" + export RUSTUP_HOME="$BOOTSTRAP_RUNTIMES/rustup" + + if has_command rustup || [ -f "$BOOTSTRAP_RUNTIMES/cargo/bin/rustup" ]; then log_info "Rust (rustup) is already installed." fi @@ -78,11 +81,15 @@ install_rust() { } configure_shell() { - # Add ~/.cargo/bin to PATH for the current process - export PATH="$HOME/.cargo/bin:$PATH" - write_env_snippet "rust" '. "$HOME/.cargo/env"' + local snippet_content=$(cat << 'EOF' +export CARGO_HOME="$BOOTSTRAP_RUNTIMES/cargo" +export RUSTUP_HOME="$BOOTSTRAP_RUNTIMES/rustup" +. "$CARGO_HOME/env" +EOF +) + write_env_snippet "rust" "$snippet_content" } main() { diff --git a/installers/install_starship.sh b/installers/install_starship.sh index ec89f86..f84e9f0 100644 --- a/installers/install_starship.sh +++ b/installers/install_starship.sh @@ -49,7 +49,7 @@ install_starship() { tar -xzf "$archive" -C "$TMP_DIR" # Install to ~/.local/bin - local target_dir="$HOME/.local/bin" + local target_dir="$BOOTSTRAP_BIN" mkdir -p "$target_dir" log_info "Installing Starship to $target_dir/starship..." cp "$TMP_DIR/starship" "$target_dir/starship" @@ -59,11 +59,8 @@ install_starship() { } configure_shell() { - # Add ~/.local/bin to PATH for the current process - export PATH="$HOME/.local/bin:$PATH" - write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"' write_env_snippet "starship" 'eval "$(starship init bash)"' } diff --git a/installers/install_uv.sh b/installers/install_uv.sh index 9d79cc3..ff14677 100644 --- a/installers/install_uv.sh +++ b/installers/install_uv.sh @@ -58,7 +58,7 @@ install_uv() { tar -xzf "$archive" --strip-components 1 -C "$TMP_DIR" # Install to ~/.local/bin - local target_dir="$HOME/.local/bin" + local target_dir="$BOOTSTRAP_BIN" mkdir -p "$target_dir" log_info "Installing uv and uvx to $target_dir..." cp "$TMP_DIR/uv" "$target_dir/uv" @@ -70,11 +70,8 @@ install_uv() { } configure_shell() { - # Add ~/.local/bin to PATH for the current process - export PATH="$HOME/.local/bin:$PATH" - write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"' write_env_snippet "uv" 'eval "$(uv generate-shell-completion bash)"' } diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index dc80b0c..e2bf1f8 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -30,7 +30,6 @@ y() { } EOF ) - write_alias_snippet "yazi" "$wrapper_content" } @@ -74,7 +73,7 @@ install_yazi() { unzip -q "$archive" -d "$TMP_DIR" local extract_dir="$TMP_DIR/yazi-${target}" - local target_dir="$HOME/.local/bin" + local target_dir="$BOOTSTRAP_BIN" mkdir -p "$target_dir" log_info "Installing Yazi to $target_dir..." diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index 659eb1e..6428906 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -34,11 +34,8 @@ install_zoxide() { } configure_shell() { - # Add ~/.local/bin to PATH for the current process - export PATH="$HOME/.local/bin:$PATH" - write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"' write_env_snippet "zoxide" 'eval "$(zoxide init --cmd cd bash)"' } diff --git a/lib/common.sh b/lib/common.sh index ac96f45..10d1151 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -7,6 +7,15 @@ if [ -n "${_LIB_COMMON_SOURCED:-}" ]; then fi _LIB_COMMON_SOURCED=1 +# Export global environment paths with default fallbacks +export BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}" +export BOOTSTRAP_DATA_DIR="${BOOTSTRAP_DATA_DIR:-$HOME/.local/share/bootstrap}" +export BOOTSTRAP_STATE_DIR="${BOOTSTRAP_STATE_DIR:-$HOME/.local/state/bootstrap}" +export BOOTSTRAP_CACHE_DIR="${BOOTSTRAP_CACHE_DIR:-$HOME/.cache/bootstrap}" +export BOOTSTRAP_BIN="${BOOTSTRAP_BIN:-$BOOTSTRAP_DATA_DIR/bin}" +export BOOTSTRAP_OPT="${BOOTSTRAP_OPT:-$BOOTSTRAP_DATA_DIR/opt}" +export BOOTSTRAP_RUNTIMES="${BOOTSTRAP_RUNTIMES:-$BOOTSTRAP_DATA_DIR/runtimes}" + # Ensure running in Bash require_bash() { if [ -z "${BASH_VERSION:-}" ]; then @@ -88,7 +97,7 @@ version_lt() { download_file() { local url="$1" local dest="$2" - local cache_dir="$HOME/.local/state/bootstrap/cache" + local cache_dir="$BOOTSTRAP_CACHE_DIR/downloads" mkdir -p "$cache_dir" diff --git a/lib/registry_helpers.sh b/lib/registry_helpers.sh index 6339a37..1d9ae79 100644 --- a/lib/registry_helpers.sh +++ b/lib/registry_helpers.sh @@ -3,7 +3,7 @@ # Ensures the registry file exists ensure_registry() { - local registry_file="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/registry.json" + local registry_file="$BOOTSTRAP_STATE_DIR/registry.json" if [ ! -f "$registry_file" ]; then mkdir -p "$(dirname "$registry_file")" echo '{"tools": {}}' > "$registry_file" @@ -36,7 +36,7 @@ register_tool() { local source="${4:-}" local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - local bindir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/bin" + local bindir="$BOOTSTRAP_BIN" local filter='if .tools == null then .tools = {} else . end | .tools[$tool].strategy = $strategy | @@ -80,7 +80,7 @@ registry_remove_tool() { # Usage: registry_get_sys_deps registry_get_sys_deps() { local tool="$1" - local registry_file="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/registry.json" + local registry_file="$BOOTSTRAP_STATE_DIR/registry.json" if [ -f "$registry_file" ]; then jq -r --arg tool "$tool" '.tools[$tool].system_dependencies[]? // empty' "$registry_file" fi From 0c16640593ee0ce035cf724d000ccfead339cdc4 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 27 Jun 2026 00:19:14 +0530 Subject: [PATCH 12/18] refactor: Simplified `pkg_install` and `pkg_remove` `pkg_install` and `pkg_remove` are just wrappers that map aliases and directly invooke the system package manager. - They do not manage state or inject rollback commands --- lib/platform.sh | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/lib/platform.sh b/lib/platform.sh index d12e708..9cd8a10 100644 --- a/lib/platform.sh +++ b/lib/platform.sh @@ -86,18 +86,7 @@ pkg_install() { if ! pkg_check "$pkg"; then to_install+=("$pkg") fi - - # Reference counting logic - if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then - local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg" - if ! grep -q "^${BOOTSTRAP_CURRENT_TOOL}$" "$ref_file" 2>/dev/null; then - echo "$BOOTSTRAP_CURRENT_TOOL" >> "$ref_file" - # Register rollback command - if type add_rollback_cmd >/dev/null 2>&1; then - add_rollback_cmd "pkg_remove $pkg" - fi - fi - fi + done if [ ${#to_install[@]} -eq 0 ]; then @@ -174,24 +163,6 @@ pkg_remove() { is_installed=1 fi - if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then - local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg" - if [ -f "$ref_file" ]; then - if [ "$is_installed" -eq 0 ]; then - log_info "Package '$pkg' is no longer installed system-wide. Cleaning up stale reference." - rm -f "$ref_file" - else - # Remove this tool from the reference file - sed -i "/^${BOOTSTRAP_CURRENT_TOOL}$/d" "$ref_file" - if [ -s "$ref_file" ]; then - log_info "Skipping removal of '$pkg'; it is required by other tools." - continue - else - rm -f "$ref_file" - fi - fi - fi - fi if [ "$is_installed" -eq 1 ]; then to_remove+=("$pkg") From cee345e3f0ea7489003f00d7ca07dbb7e87620d3 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 27 Jun 2026 00:20:56 +0530 Subject: [PATCH 13/18] feat: Unified Uninstallation and Rollback - Removed `BOOTSTRAP_PACKAGES_DIR` b rb can now uninstall that particular tool --- lib/rollback.sh | 48 ++++++++++++++++++++++++++++++++++++++++-------- lib/routes.sh | 7 ++++++- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/rollback.sh b/lib/rollback.sh index 8bd8cd6..d726a6c 100644 --- a/lib/rollback.sh +++ b/lib/rollback.sh @@ -8,11 +8,11 @@ _LIB_ROLLBACK_SOURCED=1 BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap" BOOTSTRAP_HISTORY_LOG="$BOOTSTRAP_STATE_DIR/history.log" BOOTSTRAP_UNINSTALLERS_DIR="$BOOTSTRAP_STATE_DIR/uninstallers" -BOOTSTRAP_PACKAGES_DIR="$BOOTSTRAP_STATE_DIR/packages" + init_rollback_system() { mkdir -p "$BOOTSTRAP_UNINSTALLERS_DIR" - mkdir -p "$BOOTSTRAP_PACKAGES_DIR" + touch "$BOOTSTRAP_HISTORY_LOG" } @@ -84,6 +84,41 @@ execute_rollback() { log_success "Rollback of '$tool' complete." } + +uninstall_tool() { + local tool="$1" + + # 1. Execute the rollback manifest to remove files/dirs/env/aliases + execute_rollback "$tool" + + # 2. Reference counting and cleanup of system dependencies + local registry_file="$BOOTSTRAP_STATE_DIR/registry.json" + if [ -f "$registry_file" ] && jq -e --arg tool "$tool" '.tools | has($tool)' "$registry_file" >/dev/null; then + while IFS= read -r dep; do + [ -z "$dep" ] && continue + local other_users + other_users=$(jq -r --arg tool "$tool" --arg dep "$dep" ' + .tools | to_entries | map(select(.key != $tool and (.value.system_dependencies | type == "array") and (.value.system_dependencies | index($dep)))) | length + ' "$registry_file") + + if [ "$other_users" -eq 0 ]; then + log_info "System dependency '$dep' is no longer required by any registered tool. Removing..." + pkg_remove "$dep" + else + log_info "Keeping system dependency '$dep' (required by other tools)" + fi + done < <(registry_get_sys_deps "$tool") + + # Remove from registry + registry_remove_tool "$tool" + fi + + # 3. Remove the tool from history.log + if [ -f "$BOOTSTRAP_HISTORY_LOG" ]; then + sed -i "/^INSTALL: ${tool}$/d" "$BOOTSTRAP_HISTORY_LOG" + fi +} + rollback_bare() { if [ ! -s "$BOOTSTRAP_HISTORY_LOG" ]; then log_info "No history available to rollback." @@ -95,9 +130,7 @@ rollback_bare() { if [[ "$last_line" == INSTALL:* ]]; then local tool="${last_line#INSTALL: }" - execute_rollback "$tool" - # Remove the last line efficiently - sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG" + uninstall_tool "$tool" elif [[ "$last_line" == SAVEPOINT:* ]]; then local sp="${last_line#SAVEPOINT: }" log_warn "Last action was savepoint '$sp'. Cannot bare-rollback a savepoint." @@ -122,8 +155,7 @@ rollback_to_savepoint() { break elif [[ "$last_line" == INSTALL:* ]]; then local tool="${last_line#INSTALL: }" - execute_rollback "$tool" - sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG" + uninstall_tool "$tool" elif [[ "$last_line" == SAVEPOINT:* ]]; then local sp="${last_line#SAVEPOINT: }" log_info "Removing intermediate savepoint '$sp'..." @@ -135,4 +167,4 @@ rollback_to_savepoint() { done } -export -f init_rollback_system setup_uninstaller_context add_rollback_cmd track_file track_dir create_savepoint mark_install_success execute_rollback rollback_bare rollback_to_savepoint +export -f init_rollback_system setup_uninstaller_context add_rollback_cmd track_file track_dir create_savepoint mark_install_success execute_rollback uninstall_tool rollback_bare rollback_to_savepoint diff --git a/lib/routes.sh b/lib/routes.sh index d41cf80..157a747 100755 --- a/lib/routes.sh +++ b/lib/routes.sh @@ -275,7 +275,12 @@ for script in "${SCRIPTS[@]}"; do if [ -z "$target" ]; then rollback_bare else - rollback_to_savepoint "$target" + local registry_file="$BOOTSTRAP_STATE_DIR/registry.json" + if [ -f "$registry_file" ] && jq -e --arg t "$target" '.tools | has($t)' "$registry_file" >/dev/null; then + uninstall_tool "$target" + else + rollback_to_savepoint "$target" + fi fi exit 0 ;; From 83c524441c16f8aba9a6eb43ea02b4edd2484891 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 27 Jun 2026 00:39:33 +0530 Subject: [PATCH 14/18] fix #19: Prevent Checkpoint from having same bug as --- lib/rollback.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/rollback.sh b/lib/rollback.sh index d726a6c..00f20f4 100644 --- a/lib/rollback.sh +++ b/lib/rollback.sh @@ -51,6 +51,13 @@ track_dir() { create_savepoint() { local name="$1" + + # Prevent savepoints from having the same name as a tool + if [ -n "${INSTALLERS[$name]:-}" ]; then + log_error "Cannot create savepoint named '$name' because it conflicts with a tool name." + return 1 + fi + echo "SAVEPOINT: $name" >> "$BOOTSTRAP_HISTORY_LOG" log_success "Savepoint '$name' created." } From 2ddd28d4d497b5ca057411d5be2aa63a322aaab1 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 28 Jun 2026 07:47:14 +0530 Subject: [PATCH 15/18] feat: Added Multi-tool uninstallation via comma-separated arguments - If multiple uninstalled tools share a system dependency, the dependency is only uninstalled once the last tool using it is removed. --- lib/routes.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/routes.sh b/lib/routes.sh index 157a747..632b931 100755 --- a/lib/routes.sh +++ b/lib/routes.sh @@ -275,11 +275,20 @@ for script in "${SCRIPTS[@]}"; do if [ -z "$target" ]; then rollback_bare else - local registry_file="$BOOTSTRAP_STATE_DIR/registry.json" - if [ -f "$registry_file" ] && jq -e --arg t "$target" '.tools | has($t)' "$registry_file" >/dev/null; then - uninstall_tool "$target" + # Split comma-separated targets + IFS=',' read -ra TARGETS <<< "$target" + + if [ ${#TARGETS[@]} -gt 1 ]; then + for t in "${TARGETS[@]}"; do + uninstall_tool "$t" + done else - rollback_to_savepoint "$target" + local registry_file="$BOOTSTRAP_STATE_DIR/registry.json" + if [ -f "$registry_file" ] && jq -e --arg t "$target" '.tools | has($t)' "$registry_file" >/dev/null; then + uninstall_tool "$target" + else + rollback_to_savepoint "$target" + fi fi fi exit 0 From 03e9c20e5488fbda987c7f9be0438aa63e54e6bc Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 28 Jun 2026 07:53:41 +0530 Subject: [PATCH 16/18] refactor: Linting fixes --- b.sh | 8 +++++++- bootstrap.sh | 6 ++++-- commands/con.sh | 1 + commands/help.sh | 1 + commands/uninstall.sh | 3 +-- commands/up.sh | 4 +++- installers/install_lazygit.sh | 3 ++- installers/install_node.sh | 3 ++- installers/install_nvim.sh | 1 + installers/install_rust.sh | 1 + installers/install_starship.sh | 1 + installers/install_uv.sh | 1 + installers/install_zoxide.sh | 1 + lib/common.sh | 1 + lib/platform.sh | 1 + lib/plugins.sh | 4 ++-- lib/registry.sh | 2 ++ lib/routes.sh | 6 +++--- lib/shell_config.sh | 2 ++ plugins/todo.sh | 2 +- scripts/generate_registry.sh | 10 ++++++---- 21 files changed, 44 insertions(+), 18 deletions(-) diff --git a/b.sh b/b.sh index d380330..8160689 100755 --- a/b.sh +++ b/b.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # >>> bootstrap-cli b function >>> # Shortcut for downloading and running bootstrap/install scripts b() { @@ -19,6 +20,7 @@ b() { _version_lt() { [ "$1" = "$2" ] && return 1 local IFS=. + # shellcheck disable=SC2206 local i ver1=($1) ver2=($2) for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done @@ -41,7 +43,7 @@ b() { local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" local local_ver="0.0.0" - [ -f "$routes_dir/VERSION" ] && local_ver=$(cat "$routes_dir/VERSION" 2>/dev/null | tr -d '[:space:]') + [ -f "$routes_dir/VERSION" ] && local_ver=$(tr -d '[:space:]' < "$routes_dir/VERSION" 2>/dev/null) local remote_ver if remote_ver=$(curl -fsSL "$base_url/VERSION" 2>/dev/null); then @@ -71,6 +73,7 @@ b() { # Sourced again in the parent shell after successfully running the command if [ $ret -eq 0 ]; then if [ -f "$HOME/.bashrc" ]; then + # shellcheck source=/dev/null . "$HOME/.bashrc" fi fi @@ -123,6 +126,7 @@ _b_completion() { return 0 fi + # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return 0 fi @@ -161,6 +165,7 @@ _b_completion() { return 0 fi + # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "$installer_keys" -- "$cur") ) return 0 fi @@ -170,6 +175,7 @@ _b_completion() { # List of directories in ~/.config/ to choose from local config_dirs config_dirs=$(find "$HOME/.config" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null) + # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "$config_dirs" -- "$cur") ) return 0 fi diff --git a/bootstrap.sh b/bootstrap.sh index 53ab262..db8d966 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -20,7 +20,7 @@ fi # Locate or download libraries so that sourced installers can use them BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}" -_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)" +_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _SCRIPT_DIR="$(pwd)" if [ -f "$_SCRIPT_DIR/lib/common.sh" ]; then # Dev/local mode: source directly from repo @@ -268,7 +268,7 @@ EOF # Display centered version of bootstrap in bold _version="" if [ -f "$_version_file" ]; then - _version=$(cat "$_version_file" | tr -d '\r\n') + _version=$(tr -d '\r\n' < "$_version_file") fi if [ -z "$_version" ]; then _version="0.0.0" # Fallback if VERSION missing @@ -335,6 +335,7 @@ EOF # Load the b function immediately in the current subshell if [ -f "$HOME/.config/bootstrap/b.sh" ]; then + # shellcheck source=/dev/null . "$HOME/.config/bootstrap/b.sh" fi @@ -347,6 +348,7 @@ else # Sourced mode (e.g., when sourced by installers or manually by user) # Load the b function in the current shell context if [ -f "$HOME/.config/bootstrap/b.sh" ]; then + # shellcheck source=/dev/null . "$HOME/.config/bootstrap/b.sh" fi fi diff --git a/commands/con.sh b/commands/con.sh index 0d5e797..798be76 100644 --- a/commands/con.sh +++ b/commands/con.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Command: con # Edits configurations in ~/.config/ diff --git a/commands/help.sh b/commands/help.sh index 1e216c3..70fc156 100644 --- a/commands/help.sh +++ b/commands/help.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Command: help # Lists all available bootstrap commands and installers diff --git a/commands/uninstall.sh b/commands/uninstall.sh index a4934aa..fe2cc40 100644 --- a/commands/uninstall.sh +++ b/commands/uninstall.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Command: uninstall (gone) # Removes bootstrap CLI and cleans up shell configuration files @@ -45,7 +46,6 @@ if [ -f "$HOME/.bash_aliases" ]; then # 2. Remove specific aliases added by bootstrap (e.g. vim -> nvim) if grep -q '^alias vim="nvim"$' "$HOME/.bash_aliases" 2>/dev/null; then - local tmp_file tmp_file=$(mktemp) sed '/^alias vim="nvim"$/d' "$HOME/.bash_aliases" > "$tmp_file" cat "$tmp_file" > "$HOME/.bash_aliases" @@ -61,7 +61,6 @@ fi # If force is false, leave a lightweight 'b back' shortcut function in shell config files if [ "$FORCE" = "false" ]; then - local b_back_content b_back_content=$(cat << 'EOF' b() { if [ "${1:-}" = "back" ]; then diff --git a/commands/up.sh b/commands/up.sh index 32f0bf4..d9103d4 100644 --- a/commands/up.sh +++ b/commands/up.sh @@ -1,9 +1,11 @@ +# shellcheck shell=bash # Command: up # Manually checks for updates and runs the updater if a newer version is found. # Source libraries if needed if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then _LIB_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib" + # shellcheck source=/dev/null . "$_LIB_DIR/common.sh" fi @@ -14,7 +16,7 @@ log_info "Checking for updates..." local_ver="0.0.0" version_file="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/VERSION" if [ -f "$version_file" ]; then - local_ver=$(cat "$version_file" | tr -d '[:space:]') + local_ver=$(tr -d '[:space:]' < "$version_file") fi base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" diff --git a/installers/install_lazygit.sh b/installers/install_lazygit.sh index ed52d77..6d87bcd 100755 --- a/installers/install_lazygit.sh +++ b/installers/install_lazygit.sh @@ -34,7 +34,8 @@ install_lazygit() { local version="${latest_tag#v}" - local arch=$(detect_arch) + local arch + arch=$(detect_arch) local arch_str="x86_64" if [ "$arch" = "arm64" ]; then arch_str="arm64" diff --git a/installers/install_node.sh b/installers/install_node.sh index f730527..203b815 100644 --- a/installers/install_node.sh +++ b/installers/install_node.sh @@ -69,6 +69,7 @@ install_node() { if [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then # Temporarily disable nounset as nvm.sh does not support set -u set +u + # shellcheck source=/dev/null . "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" else log_error "Could not load NVM to install Node.js." @@ -97,7 +98,7 @@ main() { if has_command node; then log_success "Node.js (via NVM) installation and configuration complete." log_info "Installed Node version: $(node --version)" - log_info "Installed NVM version: $(nvm --version 2>/dev/null || cat "$BOOTSTRAP_RUNTIMES/nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")" + log_info "Installed NVM version: $(nvm --version 2>/dev/null || grep '"version":' "$BOOTSTRAP_RUNTIMES/nvm/package.json" | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")" else log_success "Installation complete." fi diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index 12f3ac3..8b2c66e 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2016 # Tool: nvim # DisplayName: Neovim # Description: Install Neovim 0.12.0 and configuration diff --git a/installers/install_rust.sh b/installers/install_rust.sh index a5eb901..55fff9c 100644 --- a/installers/install_rust.sh +++ b/installers/install_rust.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2016 # Tool: rust # DisplayName: Rust # Description: Install Rustup and Rust compiler/toolchain diff --git a/installers/install_starship.sh b/installers/install_starship.sh index f84e9f0..1075760 100644 --- a/installers/install_starship.sh +++ b/installers/install_starship.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2016 # Tool: starship # DisplayName: Starship # Description: Install Starship shell prompt diff --git a/installers/install_uv.sh b/installers/install_uv.sh index ff14677..117b42b 100644 --- a/installers/install_uv.sh +++ b/installers/install_uv.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2016 # Tool: uv # DisplayName: uv # Description: Fast Python package installer and resolver diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index 6428906..560687c 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2016 # Tool: zoxide # DisplayName: Zoxide # Description: Install Zoxide directory jumper diff --git a/lib/common.sh b/lib/common.sh index 10d1151..271de45 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -81,6 +81,7 @@ make_temp_dir() { version_lt() { [ "$1" = "$2" ] && return 1 local IFS=. + # shellcheck disable=SC2206 local i ver1=($1) ver2=($2) for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done diff --git a/lib/platform.sh b/lib/platform.sh index 9cd8a10..b646133 100644 --- a/lib/platform.sh +++ b/lib/platform.sh @@ -11,6 +11,7 @@ if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then # Assumes common.sh is in the same directory as platform.sh # We resolve the directory of the current script _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + # shellcheck source=/dev/null . "$_LIB_DIR/common.sh" fi diff --git a/lib/plugins.sh b/lib/plugins.sh index bd44d6d..7abcf60 100644 --- a/lib/plugins.sh +++ b/lib/plugins.sh @@ -57,7 +57,7 @@ EOF for temp_file in "${temp_manifests[@]}"; do if [ -s "$temp_file" ]; then - cat "$temp_file" | parse_plugin_manifest >> "$cache_file" + parse_plugin_manifest < "$temp_file" >> "$cache_file" fi rm -f "$temp_file" done @@ -131,7 +131,7 @@ run_plugin() { if [ -n "$compat_ver" ]; then local current_ver="0.0.0" if [ -f "$BOOTSTRAP_DIR/VERSION" ]; then - current_ver=$(cat "$BOOTSTRAP_DIR/VERSION" | tr -d '[:space:]') + current_ver=$(tr -d '[:space:]' < "$BOOTSTRAP_DIR/VERSION") fi if version_lt "$compat_ver" "$current_ver"; then log_warn "Plugin '$plugin_name' is only tested up to bootstrap version $compat_ver (current: $current_ver). It may be incompatible." diff --git a/lib/registry.sh b/lib/registry.sh index d465f00..82a8eea 100644 --- a/lib/registry.sh +++ b/lib/registry.sh @@ -1,3 +1,5 @@ +# shellcheck shell=bash +# shellcheck disable=SC2034 # This file is auto-generated by scripts/generate_registry.sh. Do not edit manually. declare -A INSTALLERS=( diff --git a/lib/routes.sh b/lib/routes.sh index 632b931..0e51db5 100755 --- a/lib/routes.sh +++ b/lib/routes.sh @@ -262,7 +262,7 @@ for script in "${SCRIPTS[@]}"; do fi ;; fall) - local savepoint_name="${1:-}" + savepoint_name="${1:-}" if [ -z "$savepoint_name" ]; then log_error "Usage: b fall " exit 1 @@ -271,7 +271,7 @@ for script in "${SCRIPTS[@]}"; do exit 0 ;; rb) - local target="${1:-}" + target="${1:-}" if [ -z "$target" ]; then rollback_bare else @@ -283,7 +283,7 @@ for script in "${SCRIPTS[@]}"; do uninstall_tool "$t" done else - local registry_file="$BOOTSTRAP_STATE_DIR/registry.json" + registry_file="$BOOTSTRAP_STATE_DIR/registry.json" if [ -f "$registry_file" ] && jq -e --arg t "$target" '.tools | has($t)' "$registry_file" >/dev/null; then uninstall_tool "$target" else diff --git a/lib/shell_config.sh b/lib/shell_config.sh index acf6632..028ab9f 100644 --- a/lib/shell_config.sh +++ b/lib/shell_config.sh @@ -9,6 +9,7 @@ _LIB_SHELL_CONFIG_SOURCED=1 # Source common utilities if not already loaded if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + # shellcheck source=/dev/null . "$_LIB_DIR/common.sh" fi @@ -170,6 +171,7 @@ create_fd_symlink() { source_bashrc() { if [ -f "$HOME/.bashrc" ]; then log_info "Re-sourcing ~/.bashrc..." + # shellcheck source=/dev/null . "$HOME/.bashrc" fi } diff --git a/plugins/todo.sh b/plugins/todo.sh index f9dc0e1..e146e86 100644 --- a/plugins/todo.sh +++ b/plugins/todo.sh @@ -98,7 +98,7 @@ main() { log_success "Removed task #$task_num." ;; clear) - > "$TODO_FILE" + true > "$TODO_FILE" log_success "Cleared all tasks from your todo list." ;; --help|-h) diff --git a/scripts/generate_registry.sh b/scripts/generate_registry.sh index e3cbf99..74933d2 100755 --- a/scripts/generate_registry.sh +++ b/scripts/generate_registry.sh @@ -32,29 +32,31 @@ for f in "$INSTALLERS_DIR"/install_*.sh; do done # Sort keys alphabetically -sorted_keys=($(printf '%s\n' "${keys[@]}" | sort)) +mapfile -t sorted_keys < <(printf '%s\n' "${keys[@]}" | sort) { + echo "# shellcheck shell=bash" + echo "# shellcheck disable=SC2034" echo "# This file is auto-generated by scripts/generate_registry.sh. Do not edit manually." echo "" echo "declare -A INSTALLERS=(" for k in "${sorted_keys[@]}"; do # Escape any double quotes in description - escaped_desc=$(echo "${tools_desc[$k]}" | sed 's/"/\\"/g') + escaped_desc="${tools_desc[$k]//\"/\\\"}" echo " [$k]=\"$escaped_desc\"" done echo ")" echo "" echo "declare -A INSTALLER_DISPLAYS=(" for k in "${sorted_keys[@]}"; do - escaped_disp=$(echo "${tools_disp[$k]}" | sed 's/"/\\"/g') + escaped_disp="${tools_disp[$k]//\"/\\\"}" echo " [$k]=\"$escaped_disp\"" done echo ")" echo "" echo "declare -A INSTALLER_STRATEGIES=(" for k in "${sorted_keys[@]}"; do - escaped_strat=$(echo "${tools_strat[$k]}" | sed 's/"/\\"/g') + escaped_strat="${tools_strat[$k]//\"/\\\"}" echo " [$k]=\"$escaped_strat\"" done echo ")" From f9a25ff37e7a15a84a95e5b3cac0438c8e594088 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 28 Jun 2026 08:17:30 +0530 Subject: [PATCH 17/18] refactor(Installers): Update all installers to use library helpers and Update add_installer skill --- .agents/skills/add_installer/SKILL.md | 75 +++++++++++++-------------- installers/install_docker.sh | 3 +- installers/install_node.sh | 1 + installers/install_nvim.sh | 11 ++++ installers/install_yay.sh | 1 + installers/install_yazi.sh | 1 + installers/install_zoxide.sh | 1 + 7 files changed, 54 insertions(+), 39 deletions(-) diff --git a/.agents/skills/add_installer/SKILL.md b/.agents/skills/add_installer/SKILL.md index 3a65d7f..98eb5ee 100644 --- a/.agents/skills/add_installer/SKILL.md +++ b/.agents/skills/add_installer/SKILL.md @@ -44,25 +44,27 @@ If the user provides an official install or curl script in the prompt: - 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. +- 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 -At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following three metadata headers: +At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following metadata headers: ```bash # Tool: # DisplayName: # Description: +# Strategy: (binary | managed | system) ``` 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) -To ensure the user can seamlessly use `b rb `, all manual modifications must be tracked: -- When extracting binaries to `~/.local/bin/`, use `track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/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. +To ensure the user can seamlessly use `b rb ` to uninstall or rollback a tool: +- 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. ### Step 4: Verify (optional) @@ -75,22 +77,15 @@ Verify that the installer works and appears in the help output: ## Installer Script Template -Every installer follows this exact boilerplate structure. Copy this and fill in the tool-specific logic: +Every installer follows this exact structure. Copy this and fill in the tool-specific logic (notice there are no standalone execution guards or curl checks): ```bash #!/usr/bin/env bash # Tool: # DisplayName: # Description: Short description of what it installs +# Strategy: (binary | managed | system) # -# 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 @@ -110,14 +105,14 @@ install_() { fi # --- Tool-specific installation logic goes here --- - # Use pkg_install for distro packages (it automatically handles rollback hooks!): + # Use pkg_install for distro packages: # pkg_install "arch:|debian:|fedora:" # Or manual downloads (always use download_file for resumability!): # local url="https://..." # download_file "$url" "$TMP_DIR/binary" - # cp "$TMP_DIR/binary" "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary" - # track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary" # Important for rollback! + # cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary" + # track_file "$BOOTSTRAP_BIN/binary" # Important for rollback! } # ─── Shell Configuration (if needed) ───────────────────────────────── @@ -159,7 +154,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in | `confirm "prompt"` | Interactive yes/no prompt, returns 0 for yes | | `has_command ` | Check if a command exists (returns 0/1) | | `make_temp_dir` | Create and echo a temp directory path | -| `download_file ` | Resumable and cached download of `` to ``. Uses `~/.local/state/bootstrap/cache/`. | +| `download_file ` | Resumable and cached download of `` to ``. Uses `$BOOTSTRAP_CACHE_DIR/downloads/`. | ### From `lib/platform.sh` @@ -167,7 +162,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in |---|---| | `detect_distro` | Echoes `arch`, `debian`, `fedora`, or `unknown` | | `detect_arch` | Echoes `x86_64` or `arm64` | -| `pkg_install ...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. Automatically integrates with rollback context and handles package reference counting. | +| `pkg_install ...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. | | `pkg_check ...` | Returns 0 if packages are installed. Supports identical mapping syntax. | ### From `lib/rollback.sh` @@ -176,7 +171,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in |---|---| | `track_file ` | Registers a file for deletion during `b rb` rollback. | | `track_dir ` | Registers a directory for recursive deletion during rollback. | -| `add_rollback_cmd ` | Adds a raw bash command to the uninstall manifest (e.g., `add_rollback_cmd "sudo npm uninstall -g "`). | +| `add_rollback_cmd ` | Adds a raw bash command to the uninstall manifest (e.g., `add_rollback_cmd "rm -rf "`). | ### From `lib/shell_config.sh` @@ -185,6 +180,14 @@ 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. | +### From `lib/github.sh` + +| Function | Description | +|---|---| +| `github_get_latest_release ` | Fetches the latest release tag (e.g., `v1.0.0`) from the GitHub API. | +| `github_get_download_url ` | Finds an asset matching a regex in the specified release and prints its URL. | +| `github_download_asset ` | Resolves the URL for the matching asset and downloads it directly to ``. | + --- ## Common Patterns @@ -207,10 +210,7 @@ pkg_install "arch:neovim|debian:nvim|fedora:neovim" "git" ```bash local latest_tag="" - latest_tag=$(curl -sL https://api.github.com/repos///releases/latest \ - | grep '"tag_name":' | head -n1 \ - | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true) -fi +latest_tag=$(github_get_latest_release "owner/repo") if [ -z "$latest_tag" ]; then latest_tag="v1.0.0" # fallback @@ -218,19 +218,18 @@ if [ -z "$latest_tag" ]; then fi ``` -### Resumable Download and Extraction +### GitHub Download and Extraction ```bash -local url="https://github.com/owner/repo/releases/download/${version}/archive.tar.gz" -local dest="$TMP_DIR/archive.tar.gz" +local archive="$TMP_DIR/archive.tar.gz" -# Resumable, cached download -download_file "$url" "$dest" +# Download the asset matching a regex pattern for the specific release tag +github_download_asset "owner/repo" "$latest_tag" "app-.*-linux-x86_64\.tar\.gz" "$archive" # Extract and install -tar -xzf "$dest" -C "$TMP_DIR" -sudo cp "$TMP_DIR/binary" /usr/local/bin/binary -track_file "/usr/local/bin/binary" +tar -xzf "$archive" -C "$TMP_DIR" +cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary" +track_file "$BOOTSTRAP_BIN/binary" ``` --- @@ -239,10 +238,10 @@ track_file "/usr/local/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 `~/.local/bin/`, you MUST call `track_file`. If you run `makepkg`, you MUST call `add_rollback_cmd` for `pacman -R`. +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`. -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. -8. **Clean Official Scripts**: When implementing official curl/install scripts provided in the prompt, strip them of bloat, macOS/Windows support, and redundant shell setups before writing the script. +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. +8. **Clean Official Scripts**: When implementing official curl/install scripts, strip them of bloat, macOS/Windows support, and redundant shell setups. Do not check or install `curl`. 9. **No manual shell re-sourcing**: Do NOT manually run `source ~/.bashrc` or print instructions asking the user to run it. Sourcing of the shell configuration is handled automatically by the central router and CLI at the end of the installation. diff --git a/installers/install_docker.sh b/installers/install_docker.sh index 2b0e382..b20a5fb 100644 --- a/installers/install_docker.sh +++ b/installers/install_docker.sh @@ -24,8 +24,9 @@ install_docker() { fi fi - # Use pkg_install for distro packages (it automatically handles rollback hooks for the packages!) + # Use pkg_install for distro packages and explicitly register them for reference-counted rollback pkg_install "arch:docker|debian:docker.io|fedora:docker" + registry_add_sys_deps "docker" "arch:docker|debian:docker.io|fedora:docker" # Ensure docker group exists (some distros might not create it immediately) if ! getent group docker >/dev/null 2>&1; then diff --git a/installers/install_node.sh b/installers/install_node.sh index 203b815..3aafe2a 100644 --- a/installers/install_node.sh +++ b/installers/install_node.sh @@ -24,6 +24,7 @@ install_nvm() { if ! has_command tar; then log_info "tar not found. Installing tar..." pkg_install tar + registry_add_sys_deps "node" "tar" fi # Try to fetch the latest version of NVM from GitHub API diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index 8b2c66e..d2fbe9a 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -40,6 +40,17 @@ install_packages() { "debian:python3-venv" \ "fedora:gcc-c++" + registry_add_sys_deps "nvim" \ + git tar unzip ripgrep fzf nodejs npm xclip wl-clipboard \ + "arch:fd|debian:fd-find|fedora:fd-find" \ + "arch:cmake|debian:cmake|fedora:cmake" \ + "arch:make|debian:build-essential|fedora:make" \ + "arch:gcc|debian:build-essential|fedora:gcc" \ + "arch:python|debian:python3|fedora:python3" \ + "debian:python3-pip|fedora:python3-pip" \ + "debian:python3-venv" \ + "fedora:gcc-c++" + create_fd_symlink log_info "Installing tree-sitter-cli globally..." diff --git a/installers/install_yay.sh b/installers/install_yay.sh index 105f0b9..072dbf0 100755 --- a/installers/install_yay.sh +++ b/installers/install_yay.sh @@ -38,6 +38,7 @@ install_yay() { else log_info "Dependencies (git and base-devel) are already present. Skipping package installation." fi + registry_add_sys_deps "yay" "git" "base-devel" log_info "Cloning yay-bin repository..." local clone_dir diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index e2bf1f8..dd1d3cb 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -45,6 +45,7 @@ install_yazi() { if ! has_command unzip; then log_info "unzip not found. Installing unzip..." pkg_install unzip + registry_add_sys_deps "yazi" "unzip" fi local arch diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index 560687c..e8065aa 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -19,6 +19,7 @@ install_fzf() { log_info "fzf not found. Installing fzf..." pkg_install fzf + registry_add_sys_deps "zoxide" "fzf" } install_zoxide() { From 6e0f95162382684f3e7a0cf4d5f11f53b7e7f8ca Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 28 Jun 2026 08:45:30 +0530 Subject: [PATCH 18/18] merge: Master into fix/reference-counting Update hyperfine installer to follow the new installer strategy redesign Update registry --- .agents/skills/add_installer/SKILL.md | 42 +++++++---- bootstrap.sh | 2 + installers/install_hyperfine.sh | 103 ++++++++++++++++++++++++++ lib/registry.sh | 5 +- lib/shell_config.sh | 34 +++++++-- 5 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 installers/install_hyperfine.sh 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