From d5c90d6e85f48716fc989eb3137d2a9b7c73ba84 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Fri, 26 Jun 2026 23:52:03 +0530 Subject: [PATCH] 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