14 Commits

24 changed files with 788 additions and 423 deletions

View File

@@ -77,20 +77,10 @@ Every installer follows this exact boilerplate structure. Copy this and fill in
# <ToolName> Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -259,7 +249,7 @@ inject_block "$config_file" "<tool> init" 'eval "$(tool init bash)"'
3. **Confirmation prompts**: Always ask before installing. Check if already installed first.
4. **Idempotent**: Installers must be safe to re-run. Use `inject_block` (not append) for shell configs.
5. **No hardcoded paths**: Use `$HOME`, library functions, and `detect_*` helpers.
6. **Error handling**: Use `set -euo pipefail` after sourcing `bootstrap.sh`.
7. **Metascript boilerplate**: The first 22 lines of every installer are identical — always copy them verbatim.
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. **`main "$@"`**: Always end with this pattern to pass through CLI arguments.
9. **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.

View File

@@ -1 +1 @@
1.2.0
2.0.0

View File

@@ -18,6 +18,10 @@ is_sourced=false
if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then
is_sourced=true
fi
# Detect eval from installers based on presence of specific variables
if [ -n "${METASCRIPT_URL:-}" ]; then
is_sourced=true
fi
# Locate or download libraries so that sourced installers can use them
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
@@ -25,22 +29,19 @@ _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
if [ -f "$_SCRIPT_DIR/lib/common.sh" ]; then
# Dev/local mode: source directly from repo
. "$_SCRIPT_DIR/lib/common.sh"
. "$_SCRIPT_DIR/lib/platform.sh"
. "$_SCRIPT_DIR/lib/shell_config.sh"
BOOTSTRAP_SOURCE_DIR="$_SCRIPT_DIR"
elif [ -f "$BOOTSTRAP_DIR/lib/common.sh" ]; then
# Installed mode: source from bootstrap dir
. "$BOOTSTRAP_DIR/lib/common.sh"
. "$BOOTSTRAP_DIR/lib/platform.sh"
. "$BOOTSTRAP_DIR/lib/shell_config.sh"
BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_DIR"
else
# Standalone/remote mode: download to a temp directory and source
export BOOTSTRAP_TMP_DIR
BOOTSTRAP_TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$BOOTSTRAP_TMP_DIR"' EXIT
BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR"
_BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
_LIBS=("lib/common.sh" "lib/platform.sh" "lib/shell_config.sh")
_LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh")
_curl_args=()
for _lib in "${_LIBS[@]}"; do
@@ -48,15 +49,17 @@ else
_curl_args+=("-o" "$BOOTSTRAP_TMP_DIR/$_lib" "$_BASE_URL/$_lib")
done
curl -fsSL "${_curl_args[@]}" 2>/dev/null
if [ -f "$BOOTSTRAP_TMP_DIR/lib/common.sh" ]; then
. "$BOOTSTRAP_TMP_DIR/lib/common.sh"
. "$BOOTSTRAP_TMP_DIR/lib/platform.sh"
. "$BOOTSTRAP_TMP_DIR/lib/shell_config.sh"
else
echo "Error: Failed to download bootstrap libraries." >&2
exit 1
fi
fi
if [ -f "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" ]; then
. "$BOOTSTRAP_SOURCE_DIR/lib/common.sh"
. "$BOOTSTRAP_SOURCE_DIR/lib/rollback.sh"
. "$BOOTSTRAP_SOURCE_DIR/lib/platform.sh"
. "$BOOTSTRAP_SOURCE_DIR/lib/shell_config.sh"
init_rollback_system
else
echo "Error: Failed to locate or download bootstrap libraries." >&2
exit 1
fi
# Install/update the bootstrap loader and download all necessary files
@@ -66,6 +69,8 @@ install_bootstrap() {
local routes_dir="$HOME/.config/bootstrap"
mkdir -p "$routes_dir"
mkdir -p "$routes_dir/env.d"
mkdir -p "$routes_dir/aliases.d"
# List of all files to download/copy
local files=(
@@ -74,6 +79,7 @@ install_bootstrap() {
"lib/routes.sh"
"lib/registry.sh"
"lib/common.sh"
"lib/rollback.sh"
"lib/platform.sh"
"lib/shell_config.sh"
"commands/help.sh"
@@ -125,13 +131,15 @@ install_bootstrap() {
# 2. Clean up old loader block if it exists
remove_block "$config_file" "bootstrap-cli setup"
# 3. Append the new lightweight loader block
# 3. Append the new lightweight loader block that sources modular configs
log_info "Adding bootstrap loader to $config_file..."
cat << 'EOF' >> "$config_file"
# >>> bootstrap-cli setup >>>
export BOOTSTRAP_DIR="$HOME/.config/bootstrap"
[ -f "$BOOTSTRAP_DIR/b.sh" ] && . "$BOOTSTRAP_DIR/b.sh"
for f in "$BOOTSTRAP_DIR/env.d/"*.sh; do [ -r "$f" ] && . "$f"; done
for f in "$BOOTSTRAP_DIR/aliases.d/"*.sh; do [ -r "$f" ] && . "$f"; done
# <<< bootstrap-cli setup <<<
EOF
@@ -172,19 +180,14 @@ if [ "$is_sourced" = false ]; then
clear 2>/dev/null || true
# Locate or download pixel_art.ansi and VERSION
_art_file="$_SCRIPT_DIR/assets/pixel_art.ansi"
_version_file="$_SCRIPT_DIR/VERSION"
_art_file="$BOOTSTRAP_SOURCE_DIR/assets/pixel_art.ansi"
_version_file="$BOOTSTRAP_SOURCE_DIR/VERSION"
if [ ! -f "$_art_file" ]; then
if [ -n "${BOOTSTRAP_TMP_DIR:-}" ] && [ -d "$BOOTSTRAP_TMP_DIR" ]; then
_art_file="$BOOTSTRAP_TMP_DIR/pixel_art.ansi"
_version_file="$BOOTSTRAP_TMP_DIR/VERSION"
_base_url="${_BASE_URL:-https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master}"
if [ ! -f "$_art_file" ]; then
curl -fsSL -o "$_art_file" "$_base_url/assets/pixel_art.ansi" 2>/dev/null
curl -fsSL -o "$_version_file" "$_base_url/VERSION" 2>/dev/null
fi
fi
if [ ! -f "$_art_file" ] && [ -n "${BOOTSTRAP_TMP_DIR:-}" ]; then
_base_url="${_BASE_URL:-https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master}"
mkdir -p "$(dirname "$_art_file")"
curl -fsSL -o "$_art_file" "$_base_url/assets/pixel_art.ansi" 2>/dev/null || true
curl -fsSL -o "$_version_file" "$_base_url/VERSION" 2>/dev/null || true
fi
if [ -f "$_art_file" ]; then

107
docs/rollback_design.md Normal file
View File

@@ -0,0 +1,107 @@
# Bootstrap CLI: Procedural Rollback System Design
## 1. Objective
Provide a robust rollback mechanism by dynamically generating an uninstallation command list during the installation process. This avoids external parsers, keeps dependencies low, and leverages native Bash execution. It also includes a stateful savepoint system to revert complex environments.
## 2. Core Concept: Dynamic Command Manifests
Instead of tracking state in data files (like JSON), the system procedurally builds a **Command Manifest** (`~/.local/state/bootstrap/uninstallers/<tool>.cmds`) as the installation progresses. Every helper action records its inverse command as an independent line in this manifest.
## 3. History & Savepoints (`b fall` and `b rb`)
To allow rolling back multiple installations or returning to a known good state, the system maintains a chronological **History Log** acting as a stack.
**Location:** `~/.local/state/bootstrap/history.log`
### A. Creating a Savepoint (`b fall <name>`)
The `b fall` command simply appends a marker to the history log.
```bash
echo "SAVEPOINT: $name" >> "$HOME/.local/state/bootstrap/history.log"
```
### B. Tracking Installations
Whenever an installation successfully completes, the `b` CLI appends an install marker:
```bash
echo "INSTALL: nvim" >> "$HOME/.local/state/bootstrap/history.log"
```
**Example History Log:**
```text
SAVEPOINT: init
INSTALL: rust
INSTALL: node
SAVEPOINT: dev_setup
INSTALL: yazi
INSTALL: nvim
```
### C. Bare Rollback (`b rb`)
When `b rb` is executed without arguments, it rolls back the single most recent change:
1. Reads the last line of the history log (e.g., `INSTALL: nvim`).
2. Executes the command manifest for `nvim`.
3. Deletes the last line from the history log.
### D. Savepoint Rollback (`b rb <name>`)
When `b rb init` is executed, it rolls back all changes made after that savepoint:
1. Parses the history log from bottom to top.
2. For each `INSTALL: <tool>` encountered, it executes the rollback manifest for `<tool>`.
3. Stops when it reaches `SAVEPOINT: init`.
4. Truncates the history log back to the savepoint.
## 4. Required Abstractions & Helper Modifications
### A. Context Initialization
Before executing an installer script, the `b` CLI initializes the command list:
```bash
export BOOTSTRAP_UNINSTALLER_CMDS="$HOME/.local/state/bootstrap/uninstallers/nvim.cmds"
mkdir -p "$(dirname "$BOOTSTRAP_UNINSTALLER_CMDS")"
touch "$BOOTSTRAP_UNINSTALLER_CMDS"
```
### B. Recording Commands (LIFO Execution)
Rollback steps are safest when executed in reverse order. A helper prepends commands to the top of the manifest.
```bash
add_rollback_cmd() {
local cmd="$1"
sed -i "1i $cmd" "$BOOTSTRAP_UNINSTALLER_CMDS"
}
```
### C. Modifying Existing Helpers
Existing helpers automatically generate their own inverse commands.
- **`pkg_install`:**
```bash
add_rollback_cmd "pkg_remove $pkg"
```
- **`write_env_snippet` / `write_alias_snippet`:**
```bash
add_rollback_cmd "rm -f \"$HOME/.config/bootstrap/env.d/$snippet_name.sh\""
```
### D. New File Tracking Helpers
```bash
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
```
## 5. The Rollback Execution (`b rollback <tool>`)
Execution is line-by-line and fault-tolerant, allowing safe recovery even if a user injects a malformed command.
```bash
log_info "Rolling back..."
while IFS= read -r cmd; do
[ -z "$cmd" ] && continue
log_info "Executing: $cmd"
eval "$cmd" || log_warn "Failed to execute rollback step: $cmd"
done < "$BOOTSTRAP_UNINSTALLER_CMDS"
rm -f "$BOOTSTRAP_UNINSTALLER_CMDS"
log_success "Rollback complete."
```
## 6. Resilience Against User Modifications
Because `b ware <tool>` allows users to modify installation scripts:
1. **Dynamic Adaptation:** The manifest is built *during* execution, adapting to whatever packages the user manually added.
2. **Fault Isolation:** The `eval` loop ensures that a syntax error in one custom rollback step doesn't crash the removal of other tracked packages.
## 7. Handling Shared Dependencies
The `pkg_remove` helper utilizes reference counting via simple text files (e.g., `~/.local/state/bootstrap/packages/curl`).
- **On `pkg_install`**: Append tool name.
- **On `pkg_remove`**: Remove tool name. If empty, proceed with system uninstallation.

View File

@@ -6,20 +6,10 @@
# Antigravity CLI Installer Script (Linux Only)
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -140,27 +130,20 @@ install_agy() {
cp "$extracted_binary" "$BINARY_PATH"
chmod +x "$BINARY_PATH"
rm -rf "$staging_dir"
track_file "$BINARY_PATH"
log_success "Antigravity CLI successfully installed to $BINARY_PATH."
}
configure_shell() {
# Ensure $TARGET_DIR is in PATH for shell configurations if not present
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
local path_content='export PATH="$HOME/.local/bin:$PATH"'
for config_file in "${target_files[@]}"; do
if [ -f "$config_file" ] && ! grep -q '\.local/bin' "$config_file" 2>/dev/null; then
log_info "Adding ~/.local/bin to PATH in $config_file..."
inject_block "$config_file" "local-bin path" "$path_content"
# Source if modified (only for bashrc)
if [ "$config_file" = "$HOME/.bashrc" ]; then
. "$config_file" 2>/dev/null || true
fi
fi
remove_block "$config_file" "local-bin path"
done
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
}
run_handoff() {

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Tool: asciicinema
# DisplayName: asciicinema
# Description: Install asciinema terminal recorder
#
# 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)"
cleanup() {
rm -rf "$TMP_DIR"
}
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)
fi
if [ -z "$latest_tag" ]; then
latest_tag="v3.2.1" # fallback
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
else
log_info "Latest asciinema version found: $latest_tag"
fi
if has_command asciinema; then
local current_version
current_version=$(asciinema --version | head -n1 | awk '{print $2}')
if [[ "$current_version" != v* ]]; then
current_version="v${current_version}"
fi
if [[ "$current_version" == "$latest_tag" ]]; then
log_info "asciinema ${latest_tag} is already installed."
if ! confirm "Reinstall/Upgrade asciinema?"; then
log_info "Skipping asciinema installation."
return
fi
else
if ! confirm "Detecting asciinema ${current_version}. Upgrade to ${latest_tag}?"; then
log_info "Skipping asciinema installation."
return
fi
fi
else
if ! confirm "Install asciinema ${latest_tag}?"; then
log_info "Skipping asciinema installation."
return
fi
fi
# Detect architecture
local arch
arch=$(detect_arch)
local asciinema_arch=""
case "$arch" in
x86_64) asciinema_arch="x86_64-unknown-linux-gnu" ;;
arm64) asciinema_arch="aarch64-unknown-linux-gnu" ;;
*) 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}..."
curl -fsSL "$download_url" -o "$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"
# 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
track_file "/usr/local/bin/asciicinema"
log_success "asciinema ${latest_tag} installed."
}
main() {
install_asciicinema
echo
log_success "asciinema installation complete."
}
main "$@"

View File

@@ -6,20 +6,10 @@
# Bat Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -77,6 +67,7 @@ install_bat() {
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."
@@ -85,31 +76,16 @@ install_bat() {
}
configure_shell() {
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
local content="alias cat='bat --paging=never -p'"
for config_file in "${target_files[@]}"; do
local target_file="$config_file"
if [ "$config_file" = "$HOME/.bashrc" ]; then
# Clean up old block from ~/.bashrc if present to avoid duplication
remove_block "$config_file" "bat alias"
target_file="$HOME/.bash_aliases"
# Ensure the file exists
if [ ! -f "$target_file" ]; then
touch "$target_file"
fi
fi
log_info "Adding bat alias to $target_file..."
inject_block "$target_file" "bat alias" "$content"
# Source if modified (only for bashrc)
if [ "$config_file" = "$HOME/.bashrc" ]; then
log_info "Sourcing $config_file..."
. "$config_file" 2>/dev/null || true
fi
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'"
}
main() {

View File

@@ -6,20 +6,10 @@
# Node.js and NVM Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -61,12 +51,18 @@ install_nvm() {
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
track_dir "$HOME/.nvm"
log_success "NVM source files successfully extracted to $HOME/.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'
@@ -76,16 +72,7 @@ export NVM_DIR="$HOME/.nvm"
EOF
)
for config_file in "${target_files[@]}"; do
log_info "Adding NVM configuration block to $config_file..."
inject_block "$config_file" "nvm setup" "$content"
# Source if modified (only for bashrc)
if [ "$config_file" = "$HOME/.bashrc" ]; then
log_info "Sourcing $config_file..."
. "$config_file" 2>/dev/null || true
fi
done
write_env_snippet "node" "$content"
}
install_node() {

View File

@@ -1,30 +1,20 @@
#!/usr/bin/env bash
# Tool: nvim
# DisplayName: Neovim
# Description: Install Neovim 0.11.7 and configuration
# Description: Install Neovim 0.12.0 and configuration
#
# Neovim Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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.11.7"
NVIM_VERSION="0.12.0"
NVIM_INSTALL_DIR="/opt/nvim"
NVIM_CONFIG_REPO="https://git.adityagupta.dev/sortedcord/editor.git"
NVIM_CONFIG_DIR="$HOME/.config/nvim"
@@ -54,6 +44,10 @@ install_packages() {
"fedora:gcc-c++"
create_fd_symlink
log_info "Installing tree-sitter-cli globally..."
sudo npm install -g tree-sitter-cli
add_rollback_cmd "sudo npm uninstall -g tree-sitter-cli"
}
install_nvim() {
@@ -93,6 +87,9 @@ install_nvim() {
sudo mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
sudo ln -sf "$NVIM_INSTALL_DIR/bin/nvim" /usr/local/bin/nvim
track_dir "$NVIM_INSTALL_DIR"
track_file "/usr/local/bin/nvim"
log_success "Installed:"
nvim --version | head -n1
@@ -109,28 +106,32 @@ install_config() {
log_info "Cloning configuration to $NVIM_CONFIG_DIR..."
git clone "$NVIM_CONFIG_REPO" "$NVIM_CONFIG_DIR"
track_dir "$NVIM_CONFIG_DIR"
log_success "Configuration installed."
}
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
local modified=false
if add_alias_if_missing "$config_file" "vim" "nvim"; then
modified=true
fi
if add_env_if_missing "$config_file" "EDITOR" "nvim"; then
modified=true
fi
# Source if modified (only for bashrc, and not when sourced to prevent recursion)
if [ "$modified" = true ] && [ "$config_file" = "$HOME/.bashrc" ]; then
. "$config_file" 2>/dev/null || true
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"'
}
main() {

View File

@@ -17,20 +17,10 @@
# curl -fsSL https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -183,13 +173,18 @@ install_pnpm() {
}
fi
track_dir "$HOME/.local/share/pnpm"
log_success "pnpm v${version} installed successfully!"
}
# ─── 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.
@@ -204,15 +199,7 @@ esac
EOF
)
for config_file in "${target_files[@]}"; do
log_info "Configuring pnpm in $config_file..."
inject_block "$config_file" "pnpm setup" "$content"
# Source if modified (only for bashrc)
if [ "$config_file" = "$HOME/.bashrc" ]; then
. "$config_file" 2>/dev/null || true
fi
done
write_env_snippet "pnpm" "$content"
}
# ─── Main ─────────────────────────────────────────────────────────────

View File

@@ -6,24 +6,20 @@
# Rust Installer Script (Simplified Local Rustup Init)
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
# Ensure we have curl
install_downloader() {
if ! has_command curl; then
@@ -77,17 +73,10 @@ install_rust() {
local url="https://static.rust-lang.org/rustup/dist/${target}/rustup-init"
local tmpdir
tmpdir="$(make_temp_dir)"
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT
local dest="$tmpdir/rustup-init"
local dest="$TMP_DIR/rustup-init"
log_info "Downloading rustup-init..."
curl -fsSL \"$url\" -o \"$dest\"| curl -fsSL \"$url\" -o \"$dest\"
curl -fsSL "$url" -o "$dest"
chmod +x "$dest"
@@ -96,25 +85,21 @@ install_rust() {
# -y: skip prompts (we already confirmed)
# --no-modify-path: let bootstrap manage the shell paths
"$dest" -y --no-modify-path
add_rollback_cmd "rustup self uninstall -y"
}
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
log_info "Configuring Rust environment in $config_file..."
local content='. "$HOME/.cargo/env"'
inject_block "$config_file" "rust init" "$content"
# Source if modified (only for bashrc)
if [ "$config_file" = "$HOME/.bashrc" ]; then
. "$config_file" 2>/dev/null || true
fi
remove_block "$config_file" "rust init"
done
write_env_snippet "rust" '. "$HOME/.cargo/env"'
}
main() {

View File

@@ -6,20 +6,10 @@
# Starship Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -81,29 +71,22 @@ install_starship() {
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"
}
configure_shell() {
# Add ~/.local/bin to PATH for the current process
export PATH="$HOME/.local/bin:$PATH"
local config_file="$HOME/.bashrc"
if [ -f "$config_file" ]; then
# Ensure ~/.local/bin is in PATH for this file if not already present
if ! grep -q '\.local/bin' "$config_file" 2>/dev/null; then
log_info "Adding ~/.local/bin to PATH in $config_file..."
local path_content='export PATH="$HOME/.local/bin:$PATH"'
inject_block "$config_file" "local-bin path" "$path_content"
fi
# 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
log_info "Adding starship initialization to $config_file..."
local content='eval "$(starship init bash)"'
inject_block "$config_file" "starship init" "$content"
# Source to apply changes in the current context
. "$config_file" 2>/dev/null || true
fi
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
write_env_snippet "starship" 'eval "$(starship init bash)"'
}
main() {

View File

@@ -6,20 +6,10 @@
# uv Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -91,31 +81,23 @@ install_uv() {
cp "$TMP_DIR/uv" "$target_dir/uv"
cp "$TMP_DIR/uvx" "$target_dir/uvx"
chmod +x "$target_dir/uv" "$target_dir/uvx"
track_file "$target_dir/uv"
track_file "$target_dir/uvx"
}
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
# Ensure ~/.local/bin is in PATH for this file if not already present
if ! grep -q '\.local/bin' "$config_file" 2>/dev/null; then
log_info "Adding ~/.local/bin to PATH in $config_file..."
local path_content='export PATH="$HOME/.local/bin:$PATH"'
inject_block "$config_file" "local-bin path" "$path_content"
fi
log_info "Adding uv completion to $config_file..."
local content='eval "$(uv generate-shell-completion bash)"'
inject_block "$config_file" "uv completion" "$content"
# Source to apply changes in the current context
if [ "$config_file" = "$HOME/.bashrc" ]; then
. "$config_file" 2>/dev/null || true
fi
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)"'
}
main() {

View File

@@ -6,20 +6,10 @@
# Yay Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -40,10 +30,10 @@ install_yay() {
fi
local needs_install=false
if ! pacman -Qq git &>/dev/null; then
if ! pkg_check git; then
needs_install=true
fi
if ! pacman -Qq base-devel &>/dev/null && ! pacman -Qg base-devel &>/dev/null; then
if ! pkg_check base-devel && ! pacman -Qg base-devel &>/dev/null; then
needs_install=true
fi
@@ -71,6 +61,7 @@ install_yay() {
log_info "Building and installing yay..."
makepkg -si
add_rollback_cmd "sudo pacman -R --noconfirm yay"
cd "$orig_dir"
log_info "Cleaning up installer directory..."

View File

@@ -6,20 +6,10 @@
# Yazi Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -31,7 +21,11 @@ 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'
@@ -47,16 +41,7 @@ y() {
EOF
)
for config_file in "${target_files[@]}"; do
log_info "Adding yazi wrapper function 'y' to $config_file..."
inject_block "$config_file" "yazi wrapper" "$wrapper_content"
done
# Source ~/.bashrc to make the alias immediately available in the current shell context (if sourced)
if [ -f "$HOME/.bashrc" ]; then
log_info "Sourcing ~/.bashrc..."
. "$HOME/.bashrc" 2>/dev/null || true
fi
write_alias_snippet "yazi" "$wrapper_content"
}
install_yazi() {
@@ -84,7 +69,7 @@ install_yazi() {
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/')
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
@@ -95,6 +80,7 @@ install_yazi() {
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 || \
@@ -116,6 +102,7 @@ install_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

View File

@@ -6,20 +6,10 @@
# Zoxide Installer Script
#
# Run metascript to check if the shell is bash and load libraries
PARENT_DIR="$(dirname "$0")/.."
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
if [ -f "$METASCRIPT_LOCAL" ]; then
. "$METASCRIPT_LOCAL"
else
if command -v curl >/dev/null 2>&1; then
eval "$(curl -fsSL "$METASCRIPT_URL")"
else
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
exit 1
fi
# 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
@@ -50,25 +40,21 @@ 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"
}
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
log_info "Adding zoxide initialization to $config_file..."
local content="eval \"\$(zoxide init --cmd cd bash)\""
inject_block "$config_file" "zoxide init" "$content"
# Source if modified (only for bashrc)
if [ "$config_file" = "$HOME/.bashrc" ]; then
. "$config_file" 2>/dev/null || true
fi
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)"'
}
main() {

View File

@@ -84,3 +84,9 @@ version_lt() {
done
return 1
}
# Export functions and variables for subshells
export _LIB_COMMON_SOURCED=1
export RED GREEN YELLOW BLUE NC
export -f require_bash log_info log_success log_warn log_error confirm has_command make_temp_dir version_lt

View File

@@ -36,18 +36,9 @@ detect_arch() {
esac
}
# Install packages depending on detected distro
# Usage: pkg_install <package_name_arch> <package_name_debian> <package_name_fedora>
# Or simpler: map common packages to their distro equivalents
pkg_install() {
local distro
distro=$(detect_distro)
if [ "$distro" = "unknown" ]; then
log_error "Unsupported distribution. Cannot install packages automatically."
return 1
fi
_resolve_pkg_names() {
local distro="$1"
shift
local pkgs=()
for arg in "$@"; do
# Format can be "pkg" or "arch:pkg_a|debian:pkg_d|fedora:pkg_f"
@@ -69,22 +60,159 @@ pkg_install() {
pkgs+=("$arg")
fi
done
echo "${pkgs[@]}"
}
# Install packages depending on detected distro
# Usage: pkg_install <package_name_arch> <package_name_debian> <package_name_fedora>
# Or simpler: map common packages to their distro equivalents
pkg_install() {
local distro
distro=$(detect_distro)
if [ "$distro" = "unknown" ]; then
log_error "Unsupported distribution. Cannot install packages automatically."
return 1
fi
IFS=' ' read -ra pkgs <<< "$(_resolve_pkg_names "$distro" "$@")"
if [ ${#pkgs[@]} -eq 0 ]; then
return 0
fi
log_info "Installing packages via $distro package manager: ${pkgs[*]}"
local to_install=()
for pkg in "${pkgs[@]}"; do
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
return 0
fi
log_info "Installing packages via $distro package manager: ${to_install[*]}"
case "$distro" in
arch)
sudo pacman -Sy --needed --noconfirm "${pkgs[@]}"
sudo pacman -Sy --needed --noconfirm "${to_install[@]}"
;;
debian)
sudo apt update
sudo apt install -y "${pkgs[@]}"
sudo apt install -y "${to_install[@]}"
;;
fedora)
sudo dnf install -y "${pkgs[@]}"
sudo dnf install -y "${to_install[@]}"
;;
esac
}
# Check if packages are installed
# Returns 0 if all are installed, 1 otherwise
pkg_check() {
local distro
distro=$(detect_distro)
if [ "$distro" = "unknown" ]; then
return 1
fi
IFS=' ' read -ra pkgs <<< "$(_resolve_pkg_names "$distro" "$@")"
if [ ${#pkgs[@]} -eq 0 ]; then
return 0
fi
case "$distro" in
arch)
pacman -Qq "${pkgs[@]}" >/dev/null 2>&1
return $?
;;
debian)
dpkg -s "${pkgs[@]}" >/dev/null 2>&1
return $?
;;
fedora)
rpm -q "${pkgs[@]}" >/dev/null 2>&1
return $?
;;
esac
}
# Remove packages depending on detected distro
pkg_remove() {
local distro
distro=$(detect_distro)
if [ "$distro" = "unknown" ]; then
log_error "Unsupported distribution. Cannot remove packages automatically."
return 1
fi
IFS=' ' read -ra pkgs <<< "$(_resolve_pkg_names "$distro" "$@")"
if [ ${#pkgs[@]} -eq 0 ]; then
return 0
fi
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
fi
to_remove+=("$pkg")
done
if [ ${#to_remove[@]} -eq 0 ]; then
return 0
fi
log_info "Removing packages via $distro package manager: ${to_remove[*]}"
case "$distro" in
arch)
local pac_remove=()
for pkg in "${to_remove[@]}"; do
if pacman -Qq "$pkg" >/dev/null 2>&1; then
pac_remove+=("$pkg")
fi
done
if [ ${#pac_remove[@]} -gt 0 ]; then
sudo pacman -R --noconfirm "${pac_remove[@]}"
fi
;;
debian)
sudo apt remove -y "${to_remove[@]}"
;;
fedora)
sudo dnf remove -y "${to_remove[@]}"
;;
esac
}
# Export functions and variables for subshells
export _LIB_PLATFORM_SOURCED=1
export -f detect_distro detect_arch _resolve_pkg_names pkg_install pkg_check pkg_remove

View File

@@ -2,9 +2,10 @@
declare -A INSTALLERS=(
[agy]="Antigravity CLI"
[asciicinema]="asciinema terminal recorder"
[bat]="Bat (alternative to cat) and configure alias"
[node]="Node.js (LTS) and NVM"
[nvim]="Neovim 0.11.7 and configuration"
[nvim]="Neovim 0.12.0 and configuration"
[pnpm]="pnpm package manager"
[rust]="Rustup and Rust compiler/toolchain"
[starship]="Starship shell prompt"
@@ -16,6 +17,7 @@ declare -A INSTALLERS=(
declare -A INSTALLER_DISPLAYS=(
[agy]="Antigravity"
[asciicinema]="asciicinema"
[bat]="Bat"
[node]="Node"
[nvim]="Neovim"
@@ -28,4 +30,4 @@ declare -A INSTALLER_DISPLAYS=(
[zoxide]="Zoxide"
)
INSTALLER_KEYS=(agy bat node nvim pnpm rust starship uv yay yazi zoxide)
INSTALLER_KEYS=(agy asciicinema bat node nvim pnpm rust starship uv yay yazi zoxide)

130
lib/rollback.sh Normal file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env bash
if [ -n "${_LIB_ROLLBACK_SOURCED:-}" ]; then
return 0
fi
_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"
}
setup_uninstaller_context() {
local tool="$1"
export BOOTSTRAP_CURRENT_TOOL="$tool"
export BOOTSTRAP_UNINSTALLER_CMDS="$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds"
# Ensure fresh manifest for this run
rm -f "$BOOTSTRAP_UNINSTALLER_CMDS"
touch "$BOOTSTRAP_UNINSTALLER_CMDS"
}
add_rollback_cmd() {
local cmd="$1"
if [ -n "${BOOTSTRAP_UNINSTALLER_CMDS:-}" ] && [ -f "$BOOTSTRAP_UNINSTALLER_CMDS" ]; then
# Prepend to the top of the file
sed -i "1i $cmd" "$BOOTSTRAP_UNINSTALLER_CMDS"
fi
}
track_file() {
add_rollback_cmd "sudo rm -f '$1'"
}
track_dir() {
add_rollback_cmd "sudo rm -rf '$1'"
}
create_savepoint() {
local name="$1"
echo "SAVEPOINT: $name" >> "$BOOTSTRAP_HISTORY_LOG"
log_success "Savepoint '$name' created."
}
mark_install_success() {
local tool="$1"
# Only record if we actually have an uninstaller
if [ -f "$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds" ]; then
echo "INSTALL: $tool" >> "$BOOTSTRAP_HISTORY_LOG"
fi
}
execute_rollback() {
local tool="$1"
local manifest="$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds"
if [ ! -f "$manifest" ]; then
log_warn "No rollback manifest found for '$tool'."
return 0
fi
export BOOTSTRAP_CURRENT_TOOL="$tool"
log_info "Rolling back '$tool'..."
while IFS= read -r cmd; do
[ -z "$cmd" ] && continue
log_info "Executing: $cmd"
eval "$cmd" || log_warn "Failed to execute: $cmd"
done < "$manifest"
rm -f "$manifest"
log_success "Rollback of '$tool' complete."
}
rollback_bare() {
if [ ! -s "$BOOTSTRAP_HISTORY_LOG" ]; then
log_info "No history available to rollback."
return 0
fi
local last_line
last_line=$(tail -n 1 "$BOOTSTRAP_HISTORY_LOG")
if [[ "$last_line" == INSTALL:* ]]; then
local tool="${last_line#INSTALL: }"
execute_rollback "$tool"
# Remove the last line efficiently
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
elif [[ "$last_line" == SAVEPOINT:* ]]; then
local sp="${last_line#SAVEPOINT: }"
log_warn "Last action was savepoint '$sp'. Cannot bare-rollback a savepoint."
fi
}
rollback_to_savepoint() {
local target_sp="$1"
if ! grep -q "SAVEPOINT: $target_sp" "$BOOTSTRAP_HISTORY_LOG"; then
log_error "Savepoint '$target_sp' not found in history."
return 1
fi
while [ -s "$BOOTSTRAP_HISTORY_LOG" ]; do
local last_line
last_line=$(tail -n 1 "$BOOTSTRAP_HISTORY_LOG")
if [[ "$last_line" == SAVEPOINT:\ $target_sp ]]; then
log_success "Reached savepoint '$target_sp'."
# Optionally remove the savepoint itself or keep it? Let's keep it.
break
elif [[ "$last_line" == INSTALL:* ]]; then
local tool="${last_line#INSTALL: }"
execute_rollback "$tool"
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
elif [[ "$last_line" == SAVEPOINT:* ]]; then
local sp="${last_line#SAVEPOINT: }"
log_info "Removing intermediate savepoint '$sp'..."
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
else
# Unknown line format, just remove it
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
fi
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

View File

@@ -2,42 +2,37 @@
# Central routing script for bootstrap installers.
# This file is updated automatically by the 'b' command.
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$(dirname "$_LIB_DIR")}"
# Source common library
# Fallback to ~/.config/bootstrap if not found locally
if [ ! -d "$BOOTSTRAP_DIR/lib" ]; then
BOOTSTRAP_DIR="$HOME/.config/bootstrap"
fi
export BOOTSTRAP_DIR
# Source libraries
if [ -f "$BOOTSTRAP_DIR/lib/common.sh" ]; then
. "$BOOTSTRAP_DIR/lib/common.sh"
. "$BOOTSTRAP_DIR/lib/rollback.sh"
. "$BOOTSTRAP_DIR/lib/platform.sh"
. "$BOOTSTRAP_DIR/lib/shell_config.sh"
init_rollback_system
else
# Fallback/Bootstrap case if lib is not installed yet
echo "Error: Bootstrap libraries not found at $BOOTSTRAP_DIR/lib/" >&2
exit 1
fi
require_bash
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
# Source registry
if [ -f "$_SCRIPT_DIR/registry.sh" ]; then
. "$_SCRIPT_DIR/registry.sh"
elif [ -f "$BOOTSTRAP_DIR/lib/registry.sh" ]; then
if [ -f "$BOOTSTRAP_DIR/lib/registry.sh" ]; then
. "$BOOTSTRAP_DIR/lib/registry.sh"
else
# Standalone/remote fallback: download registry
_tmp_registry=$(mktemp)
BOOTSTRAP_BASE_URL="${BOOTSTRAP_BASE_URL:-https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master}"
BOOTSTRAP_FALLBACK_URL="${BOOTSTRAP_FALLBACK_URL:-https://raw.githubusercontent.com/sortedcord/bootstrap/refs/heads/master}"
curl -fsSL "${BOOTSTRAP_BASE_URL}/lib/registry.sh" -o "$_tmp_registry" 2>/dev/null || \
curl -fsSL "${BOOTSTRAP_FALLBACK_URL}/lib/registry.sh" -o "$_tmp_registry" 2>/dev/null
if [ -s "$_tmp_registry" ]; then
. "$_tmp_registry"
else
# Critical fallback
declare -A INSTALLERS
declare -A INSTALLER_DISPLAYS
INSTALLER_KEYS=()
fi
rm -f "$_tmp_registry"
# Critical fallback
declare -A INSTALLERS
declare -A INSTALLER_DISPLAYS
INSTALLER_KEYS=()
fi
# Helper function to run/edit installer scripts
@@ -64,6 +59,13 @@ run_ware() {
# Check for local installer first
local local_installer="$BOOTSTRAP_DIR/installers/install_${tool}.sh"
if [ "$bypass_edit" = "true" ] && [ -f "$local_installer" ]; then
log_info "Running ${display_name} installer..."
bash "$local_installer" "${cmd_args[@]}"
return $?
fi
local temp_script
temp_script=$(mktemp --suffix=".sh" 2>/dev/null || mktemp)
@@ -111,9 +113,14 @@ run_ware() {
# Run the script (edited or unchanged)
log_info "Running ${display_name} installer..."
setup_uninstaller_context "$tool"
bash "$temp_script" "${cmd_args[@]}"
local run_status=$?
if [ "$run_status" -eq 0 ]; then
mark_install_success "$tool"
fi
# Cleanup
rm -f "$temp_script"
return "$run_status"
@@ -193,6 +200,24 @@ for script in "${SCRIPTS[@]}"; do
exit 1
fi
;;
fall)
local savepoint_name="${1:-}"
if [ -z "$savepoint_name" ]; then
log_error "Usage: b fall <savepoint_name>"
exit 1
fi
create_savepoint "$savepoint_name"
exit 0
;;
rb)
local target="${1:-}"
if [ -z "$target" ]; then
rollback_bare
else
rollback_to_savepoint "$target"
fi
exit 0
;;
*)
log_error "Unknown command '$script'."
log_info "Run 'b all' to list all available commands."

View File

@@ -102,6 +102,62 @@ add_env_if_missing() {
return 1 # Not added
}
# Write environment snippet to env.d/
# Usage: write_env_snippet <name> <content>
write_env_snippet() {
local name="$1"
local content="$2"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/env.d"
mkdir -p "$dir"
log_info "Writing environment 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
}
# Write alias snippet to aliases.d/
# Usage: write_alias_snippet <name> <content>
write_alias_snippet() {
local name="$1"
local content="$2"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/aliases.d"
mkdir -p "$dir"
log_info "Writing alias 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 environment snippet from env.d/
# Usage: remove_env_snippet <name>
remove_env_snippet() {
local name="$1"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/env.d"
if [ -f "$dir/${name}.sh" ]; then
log_info "Removing environment snippet '$name'"
rm -f "$dir/${name}.sh"
fi
}
# Remove alias snippet from aliases.d/
# Usage: remove_alias_snippet <name>
remove_alias_snippet() {
local name="$1"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/aliases.d"
if [ -f "$dir/${name}.sh" ]; then
log_info "Removing alias 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
@@ -109,3 +165,9 @@ create_fd_symlink() {
sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd
fi
}
# 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

View File

@@ -13,6 +13,7 @@ Here is a comparison of the size and complexity of using Bootstrap (`to b`) vers
| application | to b | not to b |
| :--- | :--- | :--- |
| **Antigravity CLI (`agy`)** | 197 lines | 239 lines (Official Antigravity install script) |
| **asciicinema (`asciicinema`)** | 99 lines | N/A (Official binary distribution) |
| **Bat (`bat`)** | 155 lines | N/A (Standard package install) |
| **Node.js & NVM (`node`)** | 156 lines | 507 lines (Official NVM install script) |
| **Neovim (`nvim`)** | 178 lines | N/A (Official binary/config distribution) |

View File

@@ -9,41 +9,3 @@ if [ -f "./scripts/generate_registry.sh" ]; then
./scripts/generate_registry.sh
git add lib/registry.sh
fi
VERSION_FILE="VERSION"
if [ ! -f "$VERSION_FILE" ]; then
echo "1.0.0" > "$VERSION_FILE"
git add "$VERSION_FILE"
exit 0
fi
# Check if there are staged changes other than VERSION, documentation (*.md), or installers folder
if ! git diff --cached --name-only | grep -Ev "^($VERSION_FILE$|.*\.md$|^installers/)" | grep -q .; then
# No other files staged, skip version bump
exit 0
fi
# Read current version
current_version=$(cat "$VERSION_FILE" | tr -d '[:space:]')
# Basic regex validation for semantic versioning (X.Y.Z)
if [[ ! "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Current version '$current_version' in $VERSION_FILE is not in X.Y.Z format." >&2
exit 1
fi
# Parse version components
IFS='.' read -r major minor patch <<< "$current_version"
# Increment patch version
patch=$((patch + 1))
new_version="$major.$minor.$patch"
# Write to VERSION file
echo "$new_version" > "$VERSION_FILE"
# Add VERSION file to the commit
git add "$VERSION_FILE"
echo "[pre-commit] Automatically bumped version from $current_version to $new_version"