Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53e98c7542 | |||
| 02d3c9241c | |||
| c88839d3e0 | |||
| c5e11891a8 | |||
| 393868610f | |||
| 368dea1bbd | |||
| b31a326ca1 | |||
| dc73804416 | |||
| f118d66ec1 | |||
| 0486755771 | |||
| 725e3879d8 | |||
| 234112f304 | |||
| 6fde048250 | |||
| 9ce16a1f2b | |||
| 57a11e16a3 | |||
| b66fb4a354 | |||
| a56bee0b9c | |||
| 7e48f01cca |
@@ -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.
|
||||
@@ -77,20 +86,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
|
||||
@@ -111,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 ─────────────────────────────────────────────────────────────
|
||||
@@ -172,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`
|
||||
|
||||
@@ -179,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. |
|
||||
|
||||
---
|
||||
|
||||
@@ -204,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
|
||||
@@ -243,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"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -255,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 sourcing `bootstrap.sh`.
|
||||
7. **Metascript boilerplate**: The first 22 lines of every installer are identical — always copy them verbatim.
|
||||
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.
|
||||
6. **Error handling**: Use `set -euo pipefail` after the guard block.
|
||||
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.
|
||||
8. **Clean Official Scripts**: When implementing official curl/install scripts provided in the prompt, strip them of bloat, macOS/Windows support, and redundant shell setups before writing the script.
|
||||
|
||||
61
bootstrap.sh
61
bootstrap.sh
@@ -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
|
||||
|
||||
133
docs/rollback_design.md
Normal file
133
docs/rollback_design.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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
|
||||
@@ -106,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
|
||||
@@ -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() {
|
||||
|
||||
101
installers/install_asciicinema.sh
Normal file
101
installers/install_asciicinema.sh
Normal 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}..."
|
||||
download_file "$download_url" "$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 "$@"
|
||||
@@ -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
|
||||
@@ -73,10 +63,11 @@ 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"
|
||||
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() {
|
||||
|
||||
@@ -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
|
||||
@@ -55,18 +45,23 @@ 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"
|
||||
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 +71,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() {
|
||||
|
||||
@@ -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() {
|
||||
@@ -85,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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -44,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() {
|
||||
@@ -157,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
|
||||
}
|
||||
@@ -172,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
|
||||
}
|
||||
@@ -183,13 +177,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 +203,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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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\"
|
||||
download_file "$url" "$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() {
|
||||
|
||||
@@ -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
|
||||
@@ -69,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..."
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -78,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..."
|
||||
@@ -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() {
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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,17 +69,18 @@ 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
|
||||
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -84,3 +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 download_file
|
||||
|
||||
160
lib/platform.sh
160
lib/platform.sh
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
138
lib/rollback.sh
Normal file
138
lib/rollback.sh
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/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"
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
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
|
||||
114
lib/routes.sh
114
lib/routes.sh
@@ -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,57 @@ 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=$?
|
||||
|
||||
# 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
|
||||
rm -f "$temp_script"
|
||||
return "$run_status"
|
||||
@@ -193,6 +243,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."
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
23
readme.md
23
readme.md
@@ -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) |
|
||||
@@ -76,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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user