Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53e98c7542 | |||
| 02d3c9241c | |||
| c88839d3e0 | |||
| c5e11891a8 |
@@ -18,8 +18,9 @@ bootstrap/
|
||||
├── installers/ # Individual installer scripts (install_<name>.sh)
|
||||
├── lib/ # Shared libraries and router sourced by all installers
|
||||
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
|
||||
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install()
|
||||
│ ├── shell_config.sh # get_shell_configs(), inject_block(), remove_block(), add_alias_if_missing(), add_env_if_missing()
|
||||
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install(), pkg_check(), pkg_remove()
|
||||
│ ├── rollback.sh # Rollback tracking (track_file, track_dir, add_rollback_cmd)
|
||||
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet
|
||||
│ ├── registry.sh # Dynamically generated installer registry
|
||||
│ └── routes.sh # Central router script
|
||||
├── commands/ # Non-installer commands (help, con, uninstall)
|
||||
@@ -55,7 +56,15 @@ At the top of your new installer script, right below `#!/usr/bin/env bash`, add
|
||||
|
||||
The central router `lib/routes.sh` and autocomplete function in `b.sh` will dynamically parse this metadata from all `install_*.sh` scripts to register the installer and keys automatically! No manual edits to `lib/routes.sh` or `b.sh` are required.
|
||||
|
||||
### Step 3: Verify (optional)
|
||||
### 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 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.
|
||||
|
||||
### Step 4: Verify (optional)
|
||||
|
||||
Verify that the installer works and appears in the help output:
|
||||
- Run `b all` to confirm it appears in the help list.
|
||||
@@ -101,35 +110,23 @@ install_<name>() {
|
||||
fi
|
||||
|
||||
# --- Tool-specific installation logic goes here ---
|
||||
# Use pkg_install for distro packages:
|
||||
# pkg_install <package>
|
||||
# Use detect_distro for distro-specific logic:
|
||||
# local distro; distro=$(detect_distro)
|
||||
# Use detect_arch for arch-specific logic:
|
||||
# local arch; arch=$(detect_arch)
|
||||
# For GitHub releases, use curl pattern (see bat installer for reference)
|
||||
# Use pkg_install for distro packages (it automatically handles rollback hooks!):
|
||||
# pkg_install "arch:<pkg_a>|debian:<pkg_d>|fedora:<pkg_f>"
|
||||
|
||||
# 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!
|
||||
}
|
||||
|
||||
# ─── Shell Configuration (if needed) ─────────────────────────────────
|
||||
|
||||
configure_shell() {
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
log_info "Configuring <ToolName> in $config_file..."
|
||||
|
||||
# Use inject_block to add shell init/aliases/env vars:
|
||||
# inject_block "$config_file" "<name> init" "<content>"
|
||||
# Use add_alias_if_missing for simple aliases:
|
||||
# add_alias_if_missing "$config_file" "<alias>" "<value>"
|
||||
# Use add_env_if_missing for environment variables:
|
||||
# add_env_if_missing "$config_file" "VAR_NAME" "value"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
# Use drop-in snippets for shell configuration (they auto-rollback)
|
||||
# write_env_snippet "<name>" "export VAR_NAME=value\neval \"\$(<name> init bash)\""
|
||||
# write_alias_snippet "<name>" "alias <name>='<command>'"
|
||||
:
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
@@ -162,6 +159,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
| `confirm "prompt"` | Interactive yes/no prompt, returns 0 for yes |
|
||||
| `has_command <cmd>` | Check if a command exists (returns 0/1) |
|
||||
| `make_temp_dir` | Create and echo a temp directory path |
|
||||
| `download_file <url> <dest>` | Resumable and cached download of `<url>` to `<dest>`. Uses `~/.local/state/bootstrap/cache/`. |
|
||||
|
||||
### From `lib/platform.sh`
|
||||
|
||||
@@ -169,18 +167,23 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
|---|---|
|
||||
| `detect_distro` | Echoes `arch`, `debian`, `fedora`, or `unknown` |
|
||||
| `detect_arch` | Echoes `x86_64` or `arm64` |
|
||||
| `pkg_install <pkg>...` | Install packages via the system package manager. Supports distro-specific mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"` |
|
||||
| `pkg_install <pkg>...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. Automatically integrates with rollback context and handles package reference counting. |
|
||||
| `pkg_check <pkg>...` | Returns 0 if packages are installed. Supports identical mapping syntax. |
|
||||
|
||||
### From `lib/rollback.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `track_file <path>` | Registers a file for deletion during `b rb` rollback. |
|
||||
| `track_dir <path>` | Registers a directory for recursive deletion during rollback. |
|
||||
| `add_rollback_cmd <cmd>` | Adds a raw bash command to the uninstall manifest (e.g., `add_rollback_cmd "sudo npm uninstall -g <pkg>"`). |
|
||||
|
||||
### From `lib/shell_config.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `get_shell_configs` | Space-separated list of existing RC files (`~/.bashrc`) |
|
||||
| `inject_block <file> <name> <content>` | Idempotently inject a named block into a config file (removes old block first) |
|
||||
| `remove_block <file> <name>` | Remove a named block from a config file |
|
||||
| `add_alias_if_missing <file> <alias> <value>` | Add an alias line if not already present |
|
||||
| `add_env_if_missing <file> <var> <value>` | Add an `export VAR="value"` line if not already present |
|
||||
| `create_fd_symlink` | Symlink `fdfind` → `fd` on Debian/Ubuntu |
|
||||
| `write_env_snippet <name> <content>` | Creates an isolated `env.d/` shell drop-in snippet and registers it for rollback. |
|
||||
| `write_alias_snippet <name> <content>` | Creates an isolated `aliases.d/` shell drop-in snippet and registers it for rollback. |
|
||||
|
||||
---
|
||||
|
||||
@@ -194,27 +197,10 @@ cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
```
|
||||
|
||||
### Distro-specific installation (e.g., GitHub .deb for Debian, pacman for Arch)
|
||||
### Distro-specific mapping
|
||||
|
||||
```bash
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
case "$distro" in
|
||||
arch)
|
||||
pkg_install <package>
|
||||
;;
|
||||
debian)
|
||||
# Download .deb from GitHub releases
|
||||
;;
|
||||
fedora)
|
||||
pkg_install <package>
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported distribution."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "curl" "git"
|
||||
```
|
||||
|
||||
### Fetching latest GitHub release tag
|
||||
@@ -233,11 +219,19 @@ if [ -z "$latest_tag" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
### Shell block injection (idempotent)
|
||||
### Resumable Download and Extraction
|
||||
|
||||
```bash
|
||||
# Block name should be unique and descriptive
|
||||
inject_block "$config_file" "<tool> init" 'eval "$(tool init bash)"'
|
||||
local url="https://github.com/owner/repo/releases/download/${version}/archive.tar.gz"
|
||||
local dest="$TMP_DIR/archive.tar.gz"
|
||||
|
||||
# Resumable, cached download
|
||||
download_file "$url" "$dest"
|
||||
|
||||
# Extract and install
|
||||
tar -xzf "$dest" -C "$TMP_DIR"
|
||||
sudo cp "$TMP_DIR/binary" /usr/local/bin/binary
|
||||
track_file "/usr/local/bin/binary"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -245,11 +239,10 @@ inject_block "$config_file" "<tool> init" 'eval "$(tool init bash)"'
|
||||
## Rules & Conventions
|
||||
|
||||
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
|
||||
2. **Registry generation**: The registry in `lib/registry.sh` is automatically generated by `scripts/generate_registry.sh` (run automatically on commit by the git pre-commit hook).
|
||||
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.
|
||||
2. **Confirmation prompts**: Always ask before installing. Check if already installed first.
|
||||
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `~/.local/bin/`, you MUST call `track_file`. If you run `makepkg`, you MUST call `add_rollback_cmd` for `pacman -R`.
|
||||
4. **Shell Drop-ins**: Always use `write_env_snippet` or `write_alias_snippet` instead of manually injecting code directly into `~/.bashrc`.
|
||||
5. **No hardcoded paths**: Use `$HOME`, library functions, and `detect_*` helpers.
|
||||
6. **Error handling**: Use `set -euo pipefail` after the guard block.
|
||||
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.
|
||||
8. **`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.
|
||||
8. **Clean Official Scripts**: When implementing official curl/install scripts provided in the prompt, strip them of bloat, macOS/Windows support, and redundant shell setups before writing the script.
|
||||
|
||||
@@ -105,3 +105,29 @@ Because `b ware <tool>` allows users to modify installation scripts:
|
||||
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.
|
||||
|
||||
## 8. Fault Tolerance, Resumability, and Interrupted Installations
|
||||
|
||||
To handle failures during installation (e.g., network drops, script errors, or user cancellation via `Ctrl+C`), the CLI incorporates a transactional approach that balances **automatic rollback** and **resumability**:
|
||||
|
||||
### A. The Interruption Trap & Prompt
|
||||
When running an installer, the central router (`lib/routes.sh`) traps `SIGINT` and `SIGTERM` signals. If the installation fails or is interrupted:
|
||||
1. The trap catches the event and stops execution.
|
||||
2. The user is prompted interactively:
|
||||
- **Rollback (r)**: Invokes `execute_rollback <tool>` immediately to clean up all partial modifications.
|
||||
- **Keep (k)**: Preserves the partial changes and leaves the `.cmds` manifest intact.
|
||||
3. In non-interactive environments (e.g., CI/CD or scripts), the CLI defaults to **automatic rollback** to keep the system clean.
|
||||
|
||||
### B. Resuming via Preserved Manifests
|
||||
If the user chooses to **keep** the partial state and runs `b <tool>` again:
|
||||
1. `setup_uninstaller_context` detects that a manifest already exists and that the tool was *not* successfully installed (no `INSTALL: <tool>` in the history log).
|
||||
2. It **preserves** the existing manifest instead of wiping it.
|
||||
3. As the script runs again from the top, new rollback commands are prepended to the existing manifest, maintaining the correct LIFO order without losing the tracking of previously completed steps.
|
||||
|
||||
### C. Resumable Downloads (Caching Layer)
|
||||
To make rerunning an interrupted script fast and efficient, installers use `download_file <url> <dest>` instead of raw `curl`:
|
||||
1. It downloads the payload to a central cache directory: `~/.local/state/bootstrap/cache/`.
|
||||
2. It uses `curl -C -` to continue the download from the byte offset where it was interrupted.
|
||||
3. Once completed, it copies the cached file to the installer's temp directory.
|
||||
4. Distro package manager commands (`pkg_install`) and shell snippets (`write_env_snippet`) are naturally idempotent, allowing the script to breeze through already completed steps in milliseconds and resume exactly where the heavy work failed.
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ install_agy() {
|
||||
fi
|
||||
|
||||
log_info "Downloading release package..."
|
||||
curl -fsSL "$url" -o "$staging_payload"
|
||||
download_file "$url" "$staging_payload"
|
||||
|
||||
# Verify SHA512 Checksum
|
||||
local actual_hash
|
||||
|
||||
@@ -76,7 +76,7 @@ install_asciicinema() {
|
||||
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"
|
||||
download_file "$download_url" "$TMP_DIR/asciinema"
|
||||
|
||||
log_info "Installing asciinema to /usr/local/bin..."
|
||||
sudo cp "$TMP_DIR/asciinema" /usr/local/bin/asciinema
|
||||
|
||||
@@ -63,7 +63,7 @@ install_bat() {
|
||||
|
||||
local deb_url="https://github.com/sharkdp/bat/releases/download/${latest_tag}/bat_${version}_${deb_arch}.deb"
|
||||
log_info "Downloading Bat from ${deb_url}..."
|
||||
curl -fsSL "$deb_url" -o "$TMP_DIR/bat.deb"
|
||||
download_file "$deb_url" "$TMP_DIR/bat.deb"
|
||||
|
||||
log_info "Installing Bat package..."
|
||||
sudo apt install -y "$TMP_DIR/bat.deb"
|
||||
|
||||
@@ -45,8 +45,7 @@ install_nvm() {
|
||||
|
||||
local nvm_url="https://github.com/nvm-sh/nvm/archive/refs/tags/${latest_tag}.tar.gz"
|
||||
log_info "Downloading NVM from $nvm_url..."
|
||||
|
||||
curl -fsSL "$nvm_url" -o "$TMP_DIR/nvm.tar.gz"
|
||||
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"
|
||||
|
||||
@@ -79,7 +79,7 @@ install_nvim() {
|
||||
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}..."
|
||||
curl -fsSL "$nvim_url" -o "$TMP_DIR/nvim.tar.gz"
|
||||
download_file "$nvim_url" "$TMP_DIR/nvim.tar.gz"
|
||||
|
||||
tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR"
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ trap cleanup EXIT
|
||||
# ─── Helper Functions ─────────────────────────────────────────────────
|
||||
|
||||
download() {
|
||||
if [ -n "${2:-}" ]; then
|
||||
download_file "$1" "$2"
|
||||
else
|
||||
curl -fsSL "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
is_glibc_compatible() {
|
||||
@@ -147,7 +151,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" || {
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}.tar.gz" "$TMP_DIR/pnpm.tar.gz" || {
|
||||
log_error "Failed to download pnpm tarball."
|
||||
return 1
|
||||
}
|
||||
@@ -162,7 +166,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" || {
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}" "$TMP_DIR/pnpm" || {
|
||||
log_error "Failed to download pnpm binary."
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ install_rust() {
|
||||
local dest="$TMP_DIR/rustup-init"
|
||||
|
||||
log_info "Downloading rustup-init..."
|
||||
curl -fsSL "$url" -o "$dest"
|
||||
download_file "$url" "$dest"
|
||||
|
||||
chmod +x "$dest"
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ install_starship() {
|
||||
|
||||
log_info "Downloading Starship from ${download_url}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
curl -fsSL "$download_url" -o "$archive"
|
||||
download_file "$download_url" "$archive"
|
||||
|
||||
# Extract the binary
|
||||
log_info "Extracting Starship binary..."
|
||||
|
||||
@@ -68,7 +68,7 @@ install_uv() {
|
||||
|
||||
log_info "Downloading uv from ${download_url}..."
|
||||
local archive="$TMP_DIR/uv.tar.gz"
|
||||
curl -fsSL "$download_url" -o "$archive"
|
||||
download_file "$download_url" "$archive"
|
||||
|
||||
# Extract the binaries
|
||||
log_info "Extracting uv binaries..."
|
||||
|
||||
@@ -76,7 +76,7 @@ install_yazi() {
|
||||
|
||||
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}..."
|
||||
curl -fsSL "$deb_url" -o "$TMP_DIR/yazi.deb"
|
||||
download_file "$deb_url" "$TMP_DIR/yazi.deb"
|
||||
|
||||
log_info "Installing Yazi package..."
|
||||
sudo apt install -y "$TMP_DIR/yazi.deb"
|
||||
|
||||
@@ -84,9 +84,46 @@ version_lt() {
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# Cached and resumable download helper
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local cache_dir="$HOME/.local/state/bootstrap/cache"
|
||||
|
||||
mkdir -p "$cache_dir"
|
||||
|
||||
local safe_name
|
||||
if has_command md5sum; then
|
||||
safe_name=$(echo -n "$url" | md5sum | cut -d' ' -f1)
|
||||
elif has_command shasum; then
|
||||
safe_name=$(echo -n "$url" | shasum | cut -d' ' -f1)
|
||||
else
|
||||
safe_name=$(echo -n "$url" | tr -c '[:alnum:]_.-' '_')
|
||||
fi
|
||||
|
||||
local base_name
|
||||
base_name=$(basename "$url")
|
||||
local cache_file="$cache_dir/${safe_name}_${base_name}"
|
||||
|
||||
log_info "Downloading $base_name (resumable)..."
|
||||
if ! curl -fL -C - "$url" -o "$cache_file"; then
|
||||
local exit_code=$?
|
||||
# Exit code 33: HTTP server doesn't support ranges/resuming
|
||||
# Exit code 36: Bad download resume offset
|
||||
if [ $exit_code -eq 33 ] || [ $exit_code -eq 36 ]; then
|
||||
log_warn "Server does not support resuming. Retrying from scratch..."
|
||||
rm -f "$cache_file"
|
||||
curl -fL "$url" -o "$cache_file" || return 1
|
||||
else
|
||||
return $exit_code
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$cache_file" "$dest"
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
export -f require_bash log_info log_success log_warn log_error confirm has_command make_temp_dir version_lt download_file
|
||||
|
||||
@@ -20,8 +20,16 @@ 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"
|
||||
|
||||
# If a manifest already exists and the tool is NOT marked as successfully installed
|
||||
# in history.log, we treat this as a resumed run. We preserve the manifest so
|
||||
# that new commands are prepended to the existing ones.
|
||||
if [ -f "$BOOTSTRAP_UNINSTALLER_CMDS" ] && ! grep -q "^INSTALL: $tool$" "$BOOTSTRAP_HISTORY_LOG" 2>/dev/null; then
|
||||
log_info "Resuming installation of '$tool'. Preserving existing rollback manifest."
|
||||
else
|
||||
# Fresh installation or reinstall, start with a clean slate
|
||||
rm -f "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
fi
|
||||
touch "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
}
|
||||
|
||||
|
||||
@@ -114,11 +114,54 @@ run_ware() {
|
||||
# Run the script (edited or unchanged)
|
||||
log_info "Running ${display_name} installer..."
|
||||
setup_uninstaller_context "$tool"
|
||||
|
||||
# Set trap for signals to intercept interruption and allow user to choose rollback/keep
|
||||
local interrupted=false
|
||||
trap 'interrupted=true' INT TERM
|
||||
|
||||
bash "$temp_script" "${cmd_args[@]}"
|
||||
local run_status=$?
|
||||
|
||||
if [ "$run_status" -eq 0 ]; then
|
||||
# Restore default traps
|
||||
trap - INT TERM
|
||||
|
||||
if [ "$run_status" -eq 0 ] && [ "$interrupted" = "false" ]; then
|
||||
mark_install_success "$tool"
|
||||
else
|
||||
echo
|
||||
if [ "$interrupted" = "true" ]; then
|
||||
log_error "Installation of ${display_name} was interrupted."
|
||||
run_status=130
|
||||
else
|
||||
log_error "Installation of ${display_name} failed with exit code $run_status."
|
||||
fi
|
||||
|
||||
local choice=""
|
||||
if [ -t 0 ]; then
|
||||
while true; do
|
||||
read -r -p "Would you like to [r]ollback partial changes, or [k]eep them to resume/debug later? (r/k): " choice </dev/tty || choice="r"
|
||||
case "$choice" in
|
||||
[Rr]*)
|
||||
execute_rollback "$tool"
|
||||
run_status=1
|
||||
break
|
||||
;;
|
||||
[Kk]*)
|
||||
log_info "Keeping partial changes. Run 'b ${tool}' again to resume."
|
||||
run_status=1
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Please enter 'r' or 'k'."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
# Non-interactive environment, default to safe rollback
|
||||
log_warn "Non-interactive shell detected. Defaulting to automatic rollback to keep system clean."
|
||||
execute_rollback "$tool"
|
||||
run_status=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
|
||||
22
readme.md
22
readme.md
@@ -77,6 +77,28 @@ b con i3
|
||||
|
||||
It automatically fuzzy-finds the folder in case there is no exact match. Also, in case there is only a singular config file in that folder, then it will directly open that file.
|
||||
|
||||
### Rollbacks and Savepoints (`b rb` and `b fall`)
|
||||
|
||||
Bootstrap CLI features a powerful, procedural rollback system. It strictly tracks every extracted binary, configuration snippet, and package manager transaction to ensure your environment stays clean.
|
||||
|
||||
To safely uninstall the very last tool you installed (including wiping its shell paths and aliases):
|
||||
|
||||
```bash
|
||||
b rb
|
||||
```
|
||||
|
||||
To create a named savepoint before experimenting with your setup:
|
||||
|
||||
```bash
|
||||
b fall pre_dev_setup
|
||||
```
|
||||
|
||||
To completely roll back all installations made after that savepoint, restoring your system back to that exact state:
|
||||
|
||||
```bash
|
||||
b rb pre_dev_setup
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
To check for updates and update the tool manually:
|
||||
|
||||
Reference in New Issue
Block a user