14 Commits

Author SHA1 Message Date
83c524441c fix #19: Prevent Checkpoint from having same bug as
Some checks failed
Lint / lint (push) Failing after 14s
Lint / lint (pull_request) Failing after 13s
2026-06-27 00:39:33 +05:30
cee345e3f0 feat: Unified Uninstallation and Rollback
- Removed `BOOTSTRAP_PACKAGES_DIR`
b rb <tool> can now uninstall that particular tool
2026-06-27 00:20:56 +05:30
0c16640593 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
2026-06-27 00:19:14 +05:30
d5c90d6e85 refactor: Use XDG compliant isolated directory structure
Some checks failed
Lint / lint (push) Failing after 21s
Lint / lint (pull_request) Failing after 16s
- Using $BOOTSTRAP_BIN, $BOOTSTRAP_OPT, etc
- Add defensive fallback for undefined vars in common.sh
2026-06-26 23:52:03 +05:30
4c1c7de0b7 refactor: Remove redundant curl availability checks
Some checks failed
Lint / lint (push) Failing after 56s
Lint / lint (pull_request) Failing after 1m5s
2026-06-26 21:49:24 +05:30
29de051b7d refactor: Remove standalone exec prevention code blocks from installers
Some checks failed
Lint / lint (push) Failing after 16s
Lint / lint (pull_request) Failing after 35s
2026-06-26 21:40:48 +05:30
a4e5bc1175 refactor: Remove legacy backwards compat code
Some checks failed
Lint / lint (push) Failing after 37s
Lint / lint (pull_request) Failing after 47s
Remove the configure shell code blocks
2026-06-26 21:37:11 +05:30
36c7be07b3 refactor: Migrate all installers to use installation strategies 2026-06-26 21:35:34 +05:30
0eaea2c997 refactor: Unify fragmented install strategies within installers
Some checks failed
Lint / lint (push) Failing after 33s
Lint / lint (pull_request) Failing after 17s
bat and yazi installers use latest binary releases from github over
package manager for arch and fedora
2026-06-26 20:19:19 +05:30
4eec27570e refactor: Installers use github_get_latest_release helpers
Some checks failed
Lint / lint (push) Failing after 14s
Lint / lint (pull_request) Failing after 13s
2026-06-26 20:11:54 +05:30
f5a266ff70 feat: Implement the github release helper with github.sh 2026-06-26 19:57:47 +05:30
c42687a710 feat: Added registry helpers for installers
Some checks failed
Lint / lint (push) Failing after 13s
Lint / lint (pull_request) Failing after 13s
2026-06-26 18:36:39 +05:30
7f3ff45f05 refactor: Use jq instead of custom posix complient json.sh
Some checks failed
Lint / lint (push) Failing after 14s
Lint / lint (pull_request) Failing after 15s
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
2026-06-26 18:19:23 +05:30
780e79364f fix #9: Add validation check for pkg_remove
Some checks failed
Lint / lint (push) Failing after 3m14s
Lint / lint (pull_request) Failing after 17s
2026-06-25 22:40:13 +05:30
27 changed files with 533 additions and 553 deletions

View File

@@ -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 <name>`, 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_<name>() {
# 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) ─────────────────────────────────
@@ -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/<owner>/<repo>/releases/latest \
| grep '"tag_name":' | head -n1 \
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)

View File

@@ -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
@@ -41,7 +36,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" "lib/registry_helpers.sh" "lib/github.sh")
_curl_args=()
for _lib in "${_LIBS[@]}"; do
@@ -56,6 +51,8 @@ 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"
. "$BOOTSTRAP_SOURCE_DIR/lib/github.sh"
init_rollback_system
else
echo "Error: Failed to locate or download bootstrap libraries." >&2
@@ -68,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"
@@ -82,7 +96,8 @@ install_bootstrap() {
"lib/rollback.sh"
"lib/platform.sh"
"lib/shell_config.sh"
"lib/json.sh"
"lib/registry_helpers.sh"
"lib/github.sh"
"lib/plugins.sh"
"commands/help.sh"
"commands/con.sh"
@@ -90,6 +105,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
@@ -139,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

View File

@@ -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

View File

@@ -2,21 +2,16 @@
# Tool: agy
# DisplayName: Antigravity
# Description: Install Antigravity CLI
# Strategy: binary
#
# 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
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() {
@@ -55,19 +50,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."
@@ -134,16 +122,11 @@ install_agy() {
track_file "$BINARY_PATH"
log_success "Antigravity CLI successfully installed to $BINARY_PATH."
register_tool "agy" "binary" "" "github:sortedcord/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"'
}
run_handoff() {

View File

@@ -2,16 +2,11 @@
# Tool: asciicinema
# DisplayName: asciicinema
# Description: Install asciinema terminal recorder
# Strategy: binary
#
# 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)"
@@ -21,12 +16,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,22 +65,21 @@ 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
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."
register_tool "asciicinema" "binary" "$latest_tag" "github:asciinema/asciinema"
}
main() {

View File

@@ -2,16 +2,11 @@
# Tool: bat
# DisplayName: Bat
# Description: Install Bat (alternative to cat) and configure alias
# Strategy: binary
#
# 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)"
@@ -21,69 +16,54 @@ 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=$(curl -sL https://api.github.com/repos/sharkdp/bat/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
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
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 "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="$BOOTSTRAP_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() {
# 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'"
}

View File

@@ -2,16 +2,11 @@
# Tool: docker
# DisplayName: Docker
# Description: Container runtime and orchestration platform
# Strategy: system
#
# 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 ──────────────────────────────────────────────
@@ -58,6 +53,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 ─────────────────────────────────────────────────────────────

View File

@@ -2,16 +2,11 @@
# Tool: lazygit
# DisplayName: lazygit
# Description: Simple terminal UI for git commands
# Strategy: binary
#
# 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 ──────────────────────────────────────────────
@@ -30,11 +25,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 +40,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,15 +47,16 @@ 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"
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"
register_tool "lazygit" "binary" "$latest_tag" "github:jesseduffield/lazygit"
}
# ─── Main ─────────────────────────────────────────────────────────────

View File

@@ -2,16 +2,11 @@
# Tool: node
# DisplayName: Node
# Description: Install Node.js (LTS) and NVM
# Strategy: managed
#
# 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)"
@@ -21,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
@@ -34,7 +29,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
@@ -47,25 +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() {
# 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'
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
@@ -76,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
@@ -95,6 +85,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() {
@@ -106,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

View File

@@ -2,20 +2,16 @@
# Tool: nvim
# DisplayName: Neovim
# Description: Install Neovim 0.12.0 and configuration
# Strategy: binary
#
# 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"
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"
@@ -33,7 +29,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" \
@@ -76,23 +72,23 @@ 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"
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
register_tool "nvim" "binary" "$NVIM_VERSION" "github:neovim/neovim"
}
install_config() {
@@ -111,24 +107,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"'

View File

@@ -2,6 +2,7 @@
# Tool: pnpm
# DisplayName: Pnpm
# Description: Install pnpm package manager
# Strategy: binary
#
# pnpm Installer Script
#
@@ -17,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)"
@@ -127,14 +122,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 +149,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 +164,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
}
@@ -179,23 +177,19 @@ 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 ─────────────────────────────────────────────
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.
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" ;;

View File

@@ -2,16 +2,11 @@
# Tool: rust
# DisplayName: Rust
# Description: Install Rustup and Rust compiler/toolchain
# Strategy: managed
#
# 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)"
@@ -20,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)"
@@ -61,11 +48,14 @@ 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
install_downloader
local target
target=$(detect_target_triple)
@@ -87,19 +77,19 @@ install_rust() {
"$dest" -y --no-modify-path
add_rollback_cmd "rustup self uninstall -y"
register_tool "rust" "managed" "" "rustup"
}
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"'
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() {

View File

@@ -2,16 +2,11 @@
# Tool: starship
# DisplayName: Starship
# Description: Install Starship shell prompt
# Strategy: binary
#
# 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)"
@@ -25,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)
@@ -45,47 +34,33 @@ 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..."
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"
chmod +x "$target_dir/starship"
track_file "$target_dir/starship"
register_tool "starship" "binary" "$latest_tag" "github:starship/starship"
}
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)"'
}

View File

@@ -2,16 +2,11 @@
# Tool: uv
# DisplayName: uv
# Description: Fast Python package installer and resolver
# Strategy: binary
#
# 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)"
@@ -28,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)
@@ -54,28 +43,22 @@ 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..."
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"
@@ -83,20 +66,12 @@ 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() {
# 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)"'
}

View File

@@ -2,16 +2,11 @@
# Tool: yay
# DisplayName: Yay
# Description: Install Yay AUR helper
# Strategy: system
#
# 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 ──────────────────────────────────────────────
@@ -66,6 +61,7 @@ install_yay() {
cd "$orig_dir"
log_info "Cleaning up installer directory..."
rm -rf "$clone_dir"
register_tool "yay" "system" "" "aur:yay-bin"
}
# ─── Main ─────────────────────────────────────────────────────────────

View File

@@ -2,16 +2,11 @@
# Tool: yazi
# DisplayName: Yazi
# Description: Install Yazi terminal file manager and dependencies
# Strategy: binary
#
# 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)"
@@ -21,11 +16,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'
@@ -40,77 +30,68 @@ y() {
}
EOF
)
write_alias_snippet "yazi" "$wrapper_content"
}
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=$(curl -sL https://api.github.com/repos/sxyazi/yazi/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
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 "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="$BOOTSTRAP_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() {

View File

@@ -2,24 +2,13 @@
# Tool: zoxide
# DisplayName: Zoxide
# Description: Install Zoxide directory jumper
# Strategy: managed
#
# 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() {
if ! has_command curl; then
log_info "curl not found. Installing curl..."
pkg_install curl
fi
}
install_fzf() {
if has_command fzf; then
@@ -36,24 +25,17 @@ 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
track_file "$HOME/.local/bin/zoxide"
register_tool "zoxide" "managed" "" "github:ajeetdsouza/zoxide"
}
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)"'
}

View File

@@ -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"

58
lib/github.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# GitHub API helper functions for Bootstrap installers
# Usage: github_get_latest_release <owner/repo>
# 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
tag=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" | jq -r '.tag_name // empty')
echo "$tag"
}
# Usage: github_get_download_url <owner/repo> <tag> <regex_pattern>
# 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 <owner/repo> <tag> <regex_pattern> <dest_file>
# 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

View File

@@ -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
}
}
}
'
}

View File

@@ -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
@@ -169,21 +158,15 @@ pkg_remove() {
local to_remove=()
for pkg in "${pkgs[@]}"; do
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
rm -f "$ref_file"
fi
fi
local is_installed=0
if pkg_check "$pkg"; then
is_installed=1
fi
to_remove+=("$pkg")
if [ "$is_installed" -eq 1 ]; then
to_remove+=("$pkg")
fi
done
if [ ${#to_remove[@]} -eq 0 ]; then

View File

@@ -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

View File

@@ -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)

133
lib/registry_helpers.sh Normal file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Registry management helpers for Bootstrap
# Ensures the registry file exists
ensure_registry() {
local registry_file="$BOOTSTRAP_STATE_DIR/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 <tool_name> <strategy> [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_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 <tool_name> <dep1> <dep2>...
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 <tool_name>
registry_remove_tool() {
local tool="$1"
registry_set 'del(.tools[$tool])' --arg tool "$tool"
}
# Usage: registry_get_sys_deps <tool_name>
registry_get_sys_deps() {
local tool="$1"
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
}
# Usage: registry_check <tool_name>
# 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

View File

@@ -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"
}
@@ -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."
}
@@ -84,6 +91,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 +137,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 +162,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 +174,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

View File

@@ -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
;;

View File

@@ -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"