mirror of
https://github.com/sortedcord/bootstrap.git
synced 2026-07-22 20:22:50 +05:30
Compare commits
32 Commits
v2.2.0
...
5ba08f3d20
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ba08f3d20 | |||
| eefd792b6c | |||
| 4a03e42b9d | |||
| f6df53ae31 | |||
| 7d4e23e309 | |||
| 190f337f12 | |||
| 6e0f951623 | |||
| f9a25ff37e | |||
| 03e9c20e54 | |||
| 2ddd28d4d4 | |||
| db6ec1c1c8 | |||
| ed56ef95a9 | |||
| 671cf7f818 | |||
| f5227569b1 | |||
| 15d3a1a59d | |||
| 83c524441c | |||
| cee345e3f0 | |||
| 0c16640593 | |||
| d5c90d6e85 | |||
| 4c1c7de0b7 | |||
| 29de051b7d | |||
| a4e5bc1175 | |||
| 36c7be07b3 | |||
| 0eaea2c997 | |||
| 4eec27570e | |||
| f5a266ff70 | |||
| c42687a710 | |||
| 7f3ff45f05 | |||
| 780e79364f | |||
| f158c4e913 | |||
| fc4303bc99 | |||
| 6b0d07d70a |
@@ -20,7 +20,7 @@ bootstrap/
|
||||
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
|
||||
│ ├── 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
|
||||
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet, write_completion_snippet
|
||||
│ ├── registry.sh # Dynamically generated installer registry
|
||||
│ └── routes.sh # Central router script
|
||||
├── commands/ # Non-installer commands (help, con, uninstall)
|
||||
@@ -35,36 +35,48 @@ bootstrap/
|
||||
|
||||
When adding a new installer named `<name>`:
|
||||
|
||||
### Step 1: Create the installer script
|
||||
### Step 1: Analyze user request & gather details
|
||||
|
||||
Create `installers/install_<name>.sh` using the template below.
|
||||
When the user asks you to create an installer, they often provide either an official `curl` install script or a link to a `.tar.gz` release.
|
||||
You MUST do the following before writing the script:
|
||||
|
||||
If the user provides an official install or curl script in the prompt:
|
||||
- Read and analyze the script.
|
||||
- Remove redundant parts like macOS and Windows compatibility.
|
||||
- Strip unnecessary shell boilerplate, self-update logic, and other bloat.
|
||||
- Implement only the essential Linux installation logic inside the `install_<name>` function.
|
||||
If the user provides an official install or curl script in the prompt, or a link to one:
|
||||
- Execute the `curl` command (or use `read_url_content` or `read_browser_page`) to fetch the script and analyze what it actually does under the hood.
|
||||
- Do NOT simply execute the official script blindly in your installer.
|
||||
- Re-write its functionality according to the conventions of the bootstrap installer.
|
||||
- Strip away redundant code, OS checks for macOS/Windows (we only target Linux), unnecessary shell configuration logic, and self-update logic.
|
||||
- Implement only the core, essential Linux installation logic inside the `install_<name>` function.
|
||||
- Do NOT add checks for `curl` or attempt to install it. Assume `curl` is installed and available.
|
||||
|
||||
### Step 2: Add metadata comments to the top of your installer script
|
||||
**If the user provides a link to a `.tar.gz` (or `.zip`):**
|
||||
- First, download the archive to a temporary directory and extract it to inspect its contents.
|
||||
- Analyze the extracted folder structure to decide what needs to be installed (e.g., binaries, man pages, completions) and what should be ignored/deleted.
|
||||
- Write the `install_<name>` function to download, extract, and copy only those essential files. (Use `download_file` and temporary directories, see "Resumable Download and Extraction" below).
|
||||
|
||||
At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following three metadata headers:
|
||||
### Step 2: Create the installer script
|
||||
|
||||
### Step 3: Add metadata comments to the top of your installer script
|
||||
|
||||
At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following metadata headers:
|
||||
```bash
|
||||
# Tool: <name>
|
||||
# DisplayName: <displayName>
|
||||
# Description: <description>
|
||||
# Strategy: <strategy> (binary | managed | system)
|
||||
```
|
||||
|
||||
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: Implement Rollback Tracking (Crucial)
|
||||
### Step 4: 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.
|
||||
To ensure the user can seamlessly use `b rb <name>` to uninstall or rollback a tool, all manual modifications must be tracked:
|
||||
- When extracting binaries, copy them to `$BOOTSTRAP_BIN` and use `track_file "$BOOTSTRAP_BIN/binary"`. Never use `sudo` or write to system folders like `/usr/local/bin`.
|
||||
- When creating directories (e.g., in `$BOOTSTRAP_OPT` or `$BOOTSTRAP_RUNTIMES`), use `track_dir` to register them.
|
||||
- When running manual commands (like compiling or registry changes), log their inverses: `add_rollback_cmd "rm -rf ..."` or equivalent cleanup.
|
||||
- When installing packages via `pkg_install`, always register them with `registry_add_sys_deps` for reference-counted rollback tracking.
|
||||
- Note: `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` will automatically track themselves.
|
||||
|
||||
### Step 4: Verify (optional)
|
||||
### Step 5: Verify (optional)
|
||||
|
||||
Verify that the installer works and appears in the help output:
|
||||
- Run `b all` to confirm it appears in the help list.
|
||||
@@ -75,22 +87,15 @@ Verify that the installer works and appears in the help output:
|
||||
|
||||
## Installer Script Template
|
||||
|
||||
Every installer follows this exact boilerplate structure. Copy this and fill in the tool-specific logic:
|
||||
Every installer follows this exact structure. Copy this and fill in the tool-specific logic (notice there are no standalone execution guards or curl checks):
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Tool: <name>
|
||||
# DisplayName: <ToolName>
|
||||
# Description: Short description of what it installs
|
||||
# Strategy: <strategy> (binary | managed | system)
|
||||
#
|
||||
# <ToolName> 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
|
||||
|
||||
@@ -110,14 +115,14 @@ install_<name>() {
|
||||
fi
|
||||
|
||||
# --- Tool-specific installation logic goes here ---
|
||||
# Use pkg_install for distro packages (it automatically handles rollback hooks!):
|
||||
# Use pkg_install for distro packages:
|
||||
# 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!
|
||||
# cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary"
|
||||
# track_file "$BOOTSTRAP_BIN/binary" # Important for rollback!
|
||||
}
|
||||
|
||||
# ─── Shell Configuration (if needed) ─────────────────────────────────
|
||||
@@ -126,6 +131,7 @@ configure_shell() {
|
||||
# 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>'"
|
||||
# write_completion_snippet "<name>" "source <(<command> completion bash)"
|
||||
:
|
||||
}
|
||||
|
||||
@@ -159,7 +165,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/`. |
|
||||
| `download_file <url> <dest>` | Resumable and cached download of `<url>` to `<dest>`. Uses `$BOOTSTRAP_CACHE_DIR/downloads/`. |
|
||||
|
||||
### From `lib/platform.sh`
|
||||
|
||||
@@ -167,7 +173,7 @@ 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. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. Automatically integrates with rollback context and handles package reference counting. |
|
||||
| `pkg_install <pkg>...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. |
|
||||
| `pkg_check <pkg>...` | Returns 0 if packages are installed. Supports identical mapping syntax. |
|
||||
|
||||
### From `lib/rollback.sh`
|
||||
@@ -176,7 +182,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
|---|---|
|
||||
| `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>"`). |
|
||||
| `add_rollback_cmd <cmd>` | Adds a raw bash command to the uninstall manifest (e.g., `add_rollback_cmd "rm -rf <path>"`). |
|
||||
|
||||
### From `lib/shell_config.sh`
|
||||
|
||||
@@ -184,6 +190,15 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
|---|---|
|
||||
| `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. |
|
||||
| `write_completion_snippet <name> <content>` | Creates an isolated `completions.d/` bash completion snippet and registers it for rollback. |
|
||||
|
||||
### From `lib/github.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `github_get_latest_release <repo>` | Fetches the latest release tag (e.g., `v1.0.0`) from the GitHub API. |
|
||||
| `github_get_download_url <repo> <tag> <regex>` | Finds an asset matching a regex in the specified release and prints its URL. |
|
||||
| `github_download_asset <repo> <tag> <regex> <dest>` | Resolves the URL for the matching asset and downloads it directly to `<dest>`. |
|
||||
|
||||
---
|
||||
|
||||
@@ -200,18 +215,14 @@ trap cleanup EXIT
|
||||
### Distro-specific mapping
|
||||
|
||||
```bash
|
||||
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "curl" "git"
|
||||
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "git"
|
||||
```
|
||||
|
||||
### Fetching latest GitHub release tag
|
||||
|
||||
```bash
|
||||
local latest_tag=""
|
||||
if has_command curl; then
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/<owner>/<repo>/releases/latest \
|
||||
| grep '"tag_name":' | head -n1 \
|
||||
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
latest_tag=$(github_get_latest_release "owner/repo")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v1.0.0" # fallback
|
||||
@@ -219,19 +230,18 @@ if [ -z "$latest_tag" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
### Resumable Download and Extraction
|
||||
### GitHub Download and Extraction
|
||||
|
||||
```bash
|
||||
local url="https://github.com/owner/repo/releases/download/${version}/archive.tar.gz"
|
||||
local dest="$TMP_DIR/archive.tar.gz"
|
||||
local archive="$TMP_DIR/archive.tar.gz"
|
||||
|
||||
# Resumable, cached download
|
||||
download_file "$url" "$dest"
|
||||
# Download the asset matching a regex pattern for the specific release tag
|
||||
github_download_asset "owner/repo" "$latest_tag" "app-.*-linux-x86_64\.tar\.gz" "$archive"
|
||||
|
||||
# Extract and install
|
||||
tar -xzf "$dest" -C "$TMP_DIR"
|
||||
sudo cp "$TMP_DIR/binary" /usr/local/bin/binary
|
||||
track_file "/usr/local/bin/binary"
|
||||
tar -xzf "$archive" -C "$TMP_DIR"
|
||||
cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary"
|
||||
track_file "$BOOTSTRAP_BIN/binary"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -240,10 +250,10 @@ track_file "/usr/local/bin/binary"
|
||||
|
||||
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
|
||||
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. **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.
|
||||
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `$BOOTSTRAP_BIN`, you MUST call `track_file`. If you build/install manually (e.g. via `makepkg -si`), you MUST call `add_rollback_cmd "sudo pacman -R --noconfirm <pkg>"` or equivalent.
|
||||
4. **Shell Drop-ins**: Always use `write_env_snippet`, `write_alias_snippet`, or `write_completion_snippet` instead of manually injecting code directly into `~/.bashrc`.
|
||||
5. **No hardcoded paths**: Use `$HOME`, `$BOOTSTRAP_BIN`, `$BOOTSTRAP_OPT`, `$BOOTSTRAP_RUNTIMES`, library functions, and `detect_*` helpers.
|
||||
6. **Error handling**: Use `set -euo pipefail`.
|
||||
7. **No Standalone Execution Guards**: Do NOT add guards to prevent running the installer directly; simplify code paths to focus on clean runs.
|
||||
8. **Clean Official Scripts**: When implementing official curl/install scripts, strip them of bloat, macOS/Windows support, and redundant shell setups. Do not check or install `curl`.
|
||||
9. **No manual shell re-sourcing**: Do NOT manually run `source ~/.bashrc` or print instructions asking the user to run it. Sourcing of the shell configuration is handled automatically by the central router and CLI at the end of the installation.
|
||||
|
||||
37
.gitea/workflows/lint.yml
Normal file
37
.gitea/workflows/lint.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- main
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: production
|
||||
steps:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y git shellcheck
|
||||
|
||||
- name: Checkout Code
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
rm -rf * .git || true
|
||||
git clone --depth 1 $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git .
|
||||
|
||||
- name: Run ShellCheck
|
||||
run: |
|
||||
find . -type f -name '*.sh' | xargs shellcheck
|
||||
31
.github/workflows/lint.yml
vendored
Normal file
31
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run ShellCheck
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y shellcheck
|
||||
find . -type f -name '*.sh' | xargs shellcheck
|
||||
10
b.sh
10
b.sh
@@ -1,3 +1,4 @@
|
||||
# shellcheck shell=bash
|
||||
# >>> bootstrap-cli b function >>>
|
||||
# Shortcut for downloading and running bootstrap/install scripts
|
||||
b() {
|
||||
@@ -19,6 +20,7 @@ b() {
|
||||
_version_lt() {
|
||||
[ "$1" = "$2" ] && return 1
|
||||
local IFS=.
|
||||
# shellcheck disable=SC2206
|
||||
local i ver1=($1) ver2=($2)
|
||||
for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done
|
||||
for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done
|
||||
@@ -41,7 +43,7 @@ b() {
|
||||
|
||||
local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
local local_ver="0.0.0"
|
||||
[ -f "$routes_dir/VERSION" ] && local_ver=$(cat "$routes_dir/VERSION" 2>/dev/null | tr -d '[:space:]')
|
||||
[ -f "$routes_dir/VERSION" ] && local_ver=$(tr -d '[:space:]' < "$routes_dir/VERSION" 2>/dev/null)
|
||||
|
||||
local remote_ver
|
||||
if remote_ver=$(curl -fsSL "$base_url/VERSION" 2>/dev/null); then
|
||||
@@ -71,6 +73,7 @@ b() {
|
||||
# Sourced again in the parent shell after successfully running the command
|
||||
if [ $ret -eq 0 ]; then
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
@@ -86,7 +89,7 @@ _b_completion() {
|
||||
|
||||
# If completing the first argument after 'b'
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
opts="all con gone up ware bware"
|
||||
opts="all con gone up ware bware me trust"
|
||||
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
local installer_keys=""
|
||||
@@ -123,6 +126,7 @@ _b_completion() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2207
|
||||
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
@@ -161,6 +165,7 @@ _b_completion() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2207
|
||||
COMPREPLY=( $(compgen -W "$installer_keys" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
@@ -170,6 +175,7 @@ _b_completion() {
|
||||
# List of directories in ~/.config/ to choose from
|
||||
local config_dirs
|
||||
config_dirs=$(find "$HOME/.config" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null)
|
||||
# shellcheck disable=SC2207
|
||||
COMPREPLY=( $(compgen -W "$config_dirs" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
|
||||
57
bootstrap.sh
57
bootstrap.sh
@@ -8,11 +8,6 @@ if [ -z "${BASH_VERSION:-}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "Error: curl is required to run this script." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect if the script is sourced
|
||||
is_sourced=false
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then
|
||||
@@ -25,7 +20,7 @@ fi
|
||||
|
||||
# Locate or download libraries so that sourced installers can use them
|
||||
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _SCRIPT_DIR="$(pwd)"
|
||||
|
||||
if [ -f "$_SCRIPT_DIR/lib/common.sh" ]; then
|
||||
# Dev/local mode: source directly from repo
|
||||
@@ -41,7 +36,7 @@ else
|
||||
BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR"
|
||||
|
||||
_BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
_LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/json.sh" "lib/plugins.sh")
|
||||
_LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/plugins.sh" "lib/registry_helpers.sh" "lib/github.sh")
|
||||
|
||||
_curl_args=()
|
||||
for _lib in "${_LIBS[@]}"; do
|
||||
@@ -56,6 +51,8 @@ if [ -f "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" ]; then
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/rollback.sh"
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/platform.sh"
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/shell_config.sh"
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/registry_helpers.sh"
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/github.sh"
|
||||
init_rollback_system
|
||||
else
|
||||
echo "Error: Failed to locate or download bootstrap libraries." >&2
|
||||
@@ -68,9 +65,27 @@ install_bootstrap() {
|
||||
[ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc")
|
||||
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
mkdir -p "$routes_dir"
|
||||
mkdir -p "$routes_dir/env.d"
|
||||
mkdir -p "$routes_dir/aliases.d"
|
||||
mkdir -p "$routes_dir/completions.d"
|
||||
|
||||
# Initialize XDG directories
|
||||
mkdir -p "$HOME/.local/share/bootstrap/bin"
|
||||
mkdir -p "$HOME/.local/share/bootstrap/opt"
|
||||
mkdir -p "$HOME/.local/share/bootstrap/runtimes"
|
||||
mkdir -p "$HOME/.local/state/bootstrap/logs"
|
||||
mkdir -p "$HOME/.local/state/bootstrap/rollback"
|
||||
mkdir -p "$HOME/.cache/bootstrap/downloads"
|
||||
mkdir -p "$HOME/.cache/bootstrap/tmp"
|
||||
|
||||
# Create the universal binary PATH snippet
|
||||
cat << 'EOF' > "$routes_dir/env.d/bootstrap-bin.sh"
|
||||
export BOOTSTRAP_BIN="$BOOTSTRAP_BIN"
|
||||
case ":$PATH:" in
|
||||
*":$BOOTSTRAP_BIN:"*) ;;
|
||||
*) export PATH="$BOOTSTRAP_BIN:$PATH" ;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
# List of all files to download/copy
|
||||
local files=(
|
||||
@@ -82,7 +97,8 @@ install_bootstrap() {
|
||||
"lib/rollback.sh"
|
||||
"lib/platform.sh"
|
||||
"lib/shell_config.sh"
|
||||
"lib/json.sh"
|
||||
"lib/registry_helpers.sh"
|
||||
"lib/github.sh"
|
||||
"lib/plugins.sh"
|
||||
"commands/help.sh"
|
||||
"commands/con.sh"
|
||||
@@ -90,6 +106,11 @@ install_bootstrap() {
|
||||
"commands/up.sh"
|
||||
)
|
||||
|
||||
if ! pkg_check jq >/dev/null 2>&1; then
|
||||
log_info "jq is missing. Installing jq..."
|
||||
pkg_install jq
|
||||
fi
|
||||
|
||||
if [ -f "$_SCRIPT_DIR/b.sh" ] && [ -f "$_SCRIPT_DIR/lib/routes.sh" ]; then
|
||||
log_info "Using local files from repository..."
|
||||
for file in "${files[@]}"; do
|
||||
@@ -104,6 +125,12 @@ install_bootstrap() {
|
||||
mkdir -p "$routes_dir/installers"
|
||||
cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/"
|
||||
fi
|
||||
|
||||
# Also copy plugins if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/plugins" ]; then
|
||||
mkdir -p "$routes_dir/plugins"
|
||||
cp -r "$_SCRIPT_DIR/plugins/"* "$routes_dir/plugins/"
|
||||
fi
|
||||
else
|
||||
log_info "Downloading bootstrap scripts..."
|
||||
local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
@@ -139,9 +166,17 @@ install_bootstrap() {
|
||||
|
||||
# >>> bootstrap-cli setup >>>
|
||||
export BOOTSTRAP_DIR="$HOME/.config/bootstrap"
|
||||
export BOOTSTRAP_DATA_DIR="$HOME/.local/share/bootstrap"
|
||||
export BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap"
|
||||
export BOOTSTRAP_CACHE_DIR="$HOME/.cache/bootstrap"
|
||||
export BOOTSTRAP_BIN="$BOOTSTRAP_DATA_DIR/bin"
|
||||
export BOOTSTRAP_OPT="$BOOTSTRAP_DATA_DIR/opt"
|
||||
export BOOTSTRAP_RUNTIMES="$BOOTSTRAP_DATA_DIR/runtimes"
|
||||
|
||||
[ -f "$BOOTSTRAP_DIR/b.sh" ] && . "$BOOTSTRAP_DIR/b.sh"
|
||||
for f in "$BOOTSTRAP_DIR/env.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
for f in "$BOOTSTRAP_DIR/aliases.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
for f in "$BOOTSTRAP_DIR/completions.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
# <<< bootstrap-cli setup <<<
|
||||
EOF
|
||||
|
||||
@@ -241,7 +276,7 @@ EOF
|
||||
# Display centered version of bootstrap in bold
|
||||
_version=""
|
||||
if [ -f "$_version_file" ]; then
|
||||
_version=$(cat "$_version_file" | tr -d '\r\n')
|
||||
_version=$(tr -d '\r\n' < "$_version_file")
|
||||
fi
|
||||
if [ -z "$_version" ]; then
|
||||
_version="0.0.0" # Fallback if VERSION missing
|
||||
@@ -308,6 +343,7 @@ EOF
|
||||
|
||||
# Load the b function immediately in the current subshell
|
||||
if [ -f "$HOME/.config/bootstrap/b.sh" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.config/bootstrap/b.sh"
|
||||
fi
|
||||
|
||||
@@ -320,6 +356,7 @@ else
|
||||
# Sourced mode (e.g., when sourced by installers or manually by user)
|
||||
# Load the b function in the current shell context
|
||||
if [ -f "$HOME/.config/bootstrap/b.sh" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.config/bootstrap/b.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: con
|
||||
# Edits configurations in ~/.config/
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: help
|
||||
# Lists all available bootstrap commands and installers
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: uninstall (gone)
|
||||
# Removes bootstrap CLI and cleans up shell configuration files
|
||||
|
||||
@@ -45,7 +46,6 @@ if [ -f "$HOME/.bash_aliases" ]; then
|
||||
|
||||
# 2. Remove specific aliases added by bootstrap (e.g. vim -> nvim)
|
||||
if grep -q '^alias vim="nvim"$' "$HOME/.bash_aliases" 2>/dev/null; then
|
||||
local tmp_file
|
||||
tmp_file=$(mktemp)
|
||||
sed '/^alias vim="nvim"$/d' "$HOME/.bash_aliases" > "$tmp_file"
|
||||
cat "$tmp_file" > "$HOME/.bash_aliases"
|
||||
@@ -61,7 +61,6 @@ fi
|
||||
|
||||
# If force is false, leave a lightweight 'b back' shortcut function in shell config files
|
||||
if [ "$FORCE" = "false" ]; then
|
||||
local b_back_content
|
||||
b_back_content=$(cat << 'EOF'
|
||||
b() {
|
||||
if [ "${1:-}" = "back" ]; then
|
||||
@@ -78,6 +77,9 @@ EOF
|
||||
fi
|
||||
|
||||
# Remove the installation directory
|
||||
rm -rf "$BOOTSTRAP_DATA_DIR"
|
||||
rm -rf "$BOOTSTRAP_STATE_DIR"
|
||||
rm -rf "$BOOTSTRAP_CACHE_DIR"
|
||||
rm -rf "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
|
||||
|
||||
if [ "$FORCE" = "true" ]; then
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: up
|
||||
# Manually checks for updates and runs the updater if a newer version is found.
|
||||
|
||||
# Source libraries if needed
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
_LIB_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib"
|
||||
# shellcheck source=/dev/null
|
||||
. "$_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
@@ -14,7 +16,7 @@ log_info "Checking for updates..."
|
||||
local_ver="0.0.0"
|
||||
version_file="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/VERSION"
|
||||
if [ -f "$version_file" ]; then
|
||||
local_ver=$(cat "$version_file" | tr -d '[:space:]')
|
||||
local_ver=$(tr -d '[:space:]' < "$version_file")
|
||||
fi
|
||||
|
||||
base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
|
||||
389
docs/installation_strategies.md
Normal file
389
docs/installation_strategies.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Installation Strategies & Installer Lifecycle
|
||||
|
||||
This document explains how Bootstrap categorizes tool installations, the anatomy of an installer script, the full execution lifecycle from invocation to registry recording, and the helper infrastructure available to installers.
|
||||
|
||||
## 1. Installation Strategies
|
||||
|
||||
Every installer declares an explicit **strategy** that describes how it provisions software onto the system. The strategy is recorded in two places:
|
||||
|
||||
1. **At authoring time** — as a `# Strategy:` metadata comment in the installer script header.
|
||||
2. **At runtime** — inside `$BOOTSTRAP_STATE_DIR/registry.json` via `register_tool`.
|
||||
|
||||
### Strategy Types
|
||||
|
||||
| Strategy | Description | Validation (`registry_check`) |
|
||||
| :--- | :--- | :--- |
|
||||
| `binary` | Tool is downloaded as a prebuilt binary (typically from a GitHub release) and placed into `$BOOTSTRAP_BIN`. | Confirms the registered binary path exists and is executable. |
|
||||
| `managed` | Tool is installed through its own dedicated version manager or installer (e.g. NVM for Node.js, `rustup` for Rust). Bootstrap orchestrates the manager but doesn't own the binary directly. | Confirms the tool is available via `command -v`. |
|
||||
| `system` | Tool is installed entirely through the system package manager (`pacman`, `apt`, `dnf`). | Validates recorded system packages using `pkg_check`, falling back to `command -v` when no dependencies are recorded. |
|
||||
|
||||
### Which Strategy to Use
|
||||
|
||||
- **Prefer `binary`** when the upstream project provides standalone Linux binaries on GitHub (e.g. `bat`, `lazygit`, `hyperfine`, `yazi`, `nvim`).
|
||||
- **Use `managed`** when the tool has its own version/runtime manager that must be preserved for updates (e.g. `nvm` for Node.js, `rustup` for Rust, `zoxide` which uses its own install script).
|
||||
- **Use `system`** when the tool is only available through the distro's package manager and there is no upstream binary distribution (e.g. `docker`, `yay`).
|
||||
|
||||
## 2. Installer Script Anatomy
|
||||
|
||||
Every installer lives at `installers/install_<tool>.sh` and follows a strict structure.
|
||||
|
||||
### 2.1 Metadata Header
|
||||
|
||||
The first lines of the file are structured comments parsed by `scripts/generate_registry.sh` to auto-generate `lib/registry.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
# Strategy: binary
|
||||
#
|
||||
```
|
||||
|
||||
| Field | Purpose |
|
||||
| :--- | :--- |
|
||||
| `Tool` | The CLI key used to invoke the installer (`b bat`). Must be lowercase, no spaces. |
|
||||
| `DisplayName` | Human-readable name shown in `b ware` listings and log messages. |
|
||||
| `Description` | One-line description. The word "Install" at the start is stripped automatically by the registry generator. |
|
||||
| `Strategy` | One of `binary`, `managed`, or `system`. |
|
||||
|
||||
### 2.2 Script Body Structure
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
|
||||
# Temporary directory (auto-cleaned)
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# Core installation function
|
||||
install_<tool>() {
|
||||
# 1. Check if already installed, offer reinstall/upgrade
|
||||
# 2. Detect architecture, fetch latest version
|
||||
# 3. Download, extract, install
|
||||
# 4. Track files for rollback
|
||||
# 5. Register in the JSON registry
|
||||
}
|
||||
|
||||
# Optional: shell environment configuration
|
||||
configure_shell() {
|
||||
# Write env/alias/completion snippets
|
||||
}
|
||||
|
||||
main() {
|
||||
install_<tool>
|
||||
configure_shell # if needed
|
||||
|
||||
echo
|
||||
log_success "<Tool> installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
### 2.3 Key Conventions
|
||||
|
||||
- **No standalone execution guards**: Installers are always executed by the Bootstrap runtime, which sources all libraries before running the script. They must never check for or source libraries themselves.
|
||||
- **No `curl` availability checks**: The runtime guarantees `curl` is available.
|
||||
- **No direct `.bashrc` manipulation**: Use `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` to write shell configuration fragments into the snippet directories (`env.d/`, `aliases.d/`, `completions.d/`). The loader sources these automatically.
|
||||
- **Temp directories**: Use `make_temp_dir` (from `lib/common.sh`) for temporary work and clean up via a `trap cleanup EXIT`.
|
||||
|
||||
## 3. Available Helper Functions
|
||||
|
||||
Installers run inside a prepared environment where all Bootstrap libraries have been sourced. The following helper functions are available:
|
||||
|
||||
### 3.1 Common Utilities (`lib/common.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `log_info`, `log_success`, `log_warn`, `log_error` | Coloured logging to stdout/stderr. |
|
||||
| `confirm "<prompt>"` | Interactive yes/no prompt. Reads from `/dev/tty` to support piped execution. |
|
||||
| `has_command <cmd>` | Returns `0` if the command exists on `$PATH`. |
|
||||
| `make_temp_dir` | Creates a `mktemp -d` temporary directory and prints its path. |
|
||||
| `download_file <url> <dest>` | Cached, resumable download. Files are first stored in `$BOOTSTRAP_CACHE_DIR/downloads/` and then copied to `<dest>`. |
|
||||
| `version_lt <v1> <v2>` | Semver comparison. Returns `0` if `v1 < v2`. |
|
||||
|
||||
### 3.2 Platform & Package Management (`lib/platform.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `detect_distro` | Returns `arch`, `debian`, `fedora`, or `unknown`. |
|
||||
| `detect_arch` | Returns `x86_64` or `arm64`. |
|
||||
| `pkg_install <spec>...` | Installs system packages. Supports distro-specific mappings: `"arch:docker\|debian:docker.io\|fedora:docker"`. |
|
||||
| `pkg_check <spec>...` | Returns `0` if all specified packages are installed. |
|
||||
| `pkg_remove <spec>...` | Removes system packages. |
|
||||
|
||||
### 3.3 GitHub Release Helpers (`lib/github.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `github_get_latest_release <owner/repo>` | Queries the GitHub API and prints the `tag_name` of the latest release. |
|
||||
| `github_get_download_url <owner/repo> <tag> <regex>` | Finds the asset matching the regex pattern in the given release and prints its download URL. |
|
||||
| `github_download_asset <owner/repo> <tag> <regex> <dest>` | Resolves and downloads the matching asset to `<dest>`. |
|
||||
|
||||
**Why `github_get_latest_release` is separate from `github_download_asset`**: Asset filenames often embed the version string (e.g. `bat-v0.26.1-x86_64-unknown-linux-gnu.tar.gz`). The installer needs the concrete version *before* constructing the asset pattern and also passes it to `register_tool`.
|
||||
|
||||
### 3.4 Shell Configuration (`lib/shell_config.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `write_env_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/env.d/<name>.sh`. Auto-registers a rollback command to delete the snippet. |
|
||||
| `write_alias_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/aliases.d/<name>.sh`. Auto-registers rollback. |
|
||||
| `write_completion_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/completions.d/<name>.sh`. Auto-registers rollback. |
|
||||
|
||||
### 3.5 Rollback & File Tracking (`lib/rollback.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `track_file <path>` | Records `sudo rm -f '<path>'` in the uninstaller manifest for the current tool. |
|
||||
| `track_dir <path>` | Records `sudo rm -rf '<path>'` in the uninstaller manifest. |
|
||||
| `add_rollback_cmd <cmd>` | Prepends an arbitrary shell command to the manifest (LIFO order). |
|
||||
|
||||
### 3.6 Registry Helpers (`lib/registry_helpers.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `register_tool <tool> <strategy> [version] [source]` | Records the tool in `registry.json` with its strategy, version, source, timestamp, and (for `binary` strategy) its binary path. |
|
||||
| `registry_add_sys_deps <tool> <dep1> <dep2>...` | Appends system package dependency names to the tool's `system_dependencies` array in the registry (used for reference counting during uninstallation). |
|
||||
| `registry_remove_tool <tool>` | Deletes the tool's entire entry from `registry.json`. |
|
||||
| `registry_check <tool>` | Validates that the tool is correctly installed according to its declared strategy. |
|
||||
|
||||
## 4. Execution Lifecycle
|
||||
|
||||
This is the complete flow from when a user types `b <tool>` to when the installation is recorded.
|
||||
|
||||
### 4.1 Invocation
|
||||
|
||||
```
|
||||
User types: b bat
|
||||
│
|
||||
▼
|
||||
b.sh (shell function)
|
||||
│
|
||||
├── Auto-update check (once per 24h)
|
||||
│
|
||||
▼
|
||||
bash routes.sh bat
|
||||
```
|
||||
|
||||
`b.sh` is a shell function sourced from `~/.bashrc`. It delegates to `routes.sh` in a subshell.
|
||||
|
||||
### 4.2 Library Loading
|
||||
|
||||
`routes.sh` sources the core libraries in order:
|
||||
|
||||
1. `lib/common.sh` — Logging, `download_file`, `make_temp_dir`, XDG path variables
|
||||
2. `lib/rollback.sh` — Manifest system, `track_file`, `track_dir`, `init_rollback_system`
|
||||
3. `lib/platform.sh` — Distro/arch detection, `pkg_install`/`pkg_remove`
|
||||
4. `lib/shell_config.sh` — `write_env_snippet`, `write_alias_snippet`, etc.
|
||||
5. `lib/registry.sh` — Auto-generated lookup tables (`INSTALLERS`, `INSTALLER_DISPLAYS`, `INSTALLER_STRATEGIES`, `INSTALLER_KEYS`)
|
||||
6. `lib/registry_helpers.sh` — `register_tool`, `registry_add_sys_deps`, etc.
|
||||
7. `lib/github.sh` — GitHub release API helpers
|
||||
8. `lib/plugins.sh` — Plugin system
|
||||
|
||||
All functions are exported to subshells so the installer script inherits them.
|
||||
|
||||
### 4.3 Routing
|
||||
|
||||
`routes.sh` resolves the command. If the argument matches a key in the `INSTALLERS` registry, it is treated as a tool name and routed to `run_ware`:
|
||||
|
||||
```
|
||||
routes.sh receives "bat"
|
||||
│
|
||||
├── Is it a built-in command (help, rb, fall, con, up, gone, etc.)? → No
|
||||
├── Is it a registered tool key in INSTALLERS? → Yes
|
||||
│
|
||||
▼
|
||||
run_ware "bat" "-y"
|
||||
```
|
||||
|
||||
### 4.4 Installer Resolution
|
||||
|
||||
`run_ware` locates the installer script:
|
||||
|
||||
1. **Local check**: Does `$BOOTSTRAP_DIR/installers/install_bat.sh` exist? If yes, use it directly.
|
||||
2. **Remote download**: If not local, download from the primary Git server (`git.adityagupta.dev`). On failure, fall back to the GitHub mirror.
|
||||
|
||||
### 4.5 Pre-Execution Setup
|
||||
|
||||
Before running the installer, `run_ware` calls `setup_uninstaller_context "bat"`, which:
|
||||
|
||||
- Sets `BOOTSTRAP_CURRENT_TOOL=bat`
|
||||
- Sets `BOOTSTRAP_UNINSTALLER_CMDS=$BOOTSTRAP_STATE_DIR/uninstallers/bat.cmds`
|
||||
- If a manifest already exists from a previous interrupted run, it preserves it (resume support).
|
||||
- Otherwise, creates a fresh empty manifest file.
|
||||
|
||||
Signal traps (`INT`, `TERM`) are installed to catch interruptions.
|
||||
|
||||
### 4.6 Installer Execution
|
||||
|
||||
The installer script runs in a `bash` subshell. During execution:
|
||||
|
||||
- **`track_file`/`track_dir`** calls prepend removal commands to `bat.cmds` (LIFO order).
|
||||
- **`write_env_snippet`/`write_alias_snippet`** automatically record their own rollback commands.
|
||||
- **`pkg_install`** installs system packages. If the tool uses `system` strategy, the installer calls `registry_add_sys_deps` to register these packages for reference counting.
|
||||
- **`register_tool "bat" "binary" "$version" "github:sharkdp/bat"`** writes the tool's metadata to `$BOOTSTRAP_STATE_DIR/registry.json` using thread-safe `flock`.
|
||||
|
||||
### 4.7 Post-Execution
|
||||
|
||||
If the installer exits successfully (`exit 0`), `run_ware`:
|
||||
|
||||
1. Calls `mark_install_success "bat"` — appends `INSTALL: bat` to `$BOOTSTRAP_STATE_DIR/history.log`.
|
||||
2. Re-sources `~/.bashrc` so new env/alias snippets take effect immediately.
|
||||
|
||||
If the installer fails or is interrupted, the user is offered a choice:
|
||||
|
||||
- **Rollback (`r`)**: Executes the manifest line-by-line to undo all recorded changes.
|
||||
- **Keep (`k`)**: Preserves partial changes so the user can resume or debug.
|
||||
|
||||
## 5. The Registry (`registry.json`)
|
||||
|
||||
The JSON registry is the central source of truth for installed tool state. It lives at `$BOOTSTRAP_STATE_DIR/registry.json`.
|
||||
|
||||
### 5.1 Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"bat": {
|
||||
"strategy": "binary",
|
||||
"version": "v0.26.1",
|
||||
"source": "github:sharkdp/bat",
|
||||
"installed_at": "2025-06-28T03:15:00Z",
|
||||
"bin": "/home/user/.local/share/bootstrap/bin/bat"
|
||||
},
|
||||
"docker": {
|
||||
"strategy": "system",
|
||||
"installed_at": "2025-06-28T03:20:00Z",
|
||||
"source": "os-package-manager",
|
||||
"system_dependencies": [
|
||||
"arch:docker|debian:docker.io|fedora:docker"
|
||||
]
|
||||
},
|
||||
"node": {
|
||||
"strategy": "managed",
|
||||
"version": "v0.40.5",
|
||||
"source": "github:nvm-sh/nvm",
|
||||
"installed_at": "2025-06-28T03:25:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Thread Safety
|
||||
|
||||
All mutations go through `registry_set`, which acquires an exclusive file lock (`flock -x`) on `registry.json.lock` before applying a `jq` filter. This prevents race conditions if multiple installers run simultaneously.
|
||||
|
||||
## 6. The Auto-Generated Registry (`lib/registry.sh`)
|
||||
|
||||
`lib/registry.sh` is a **build-time artifact**, not a runtime state file. It is auto-generated by `scripts/generate_registry.sh`, which parses the metadata headers from every `installers/install_*.sh` file and produces four Bash data structures:
|
||||
|
||||
| Variable | Type | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| `INSTALLERS` | `declare -A` | Maps tool key → description |
|
||||
| `INSTALLER_DISPLAYS` | `declare -A` | Maps tool key → human-readable display name |
|
||||
| `INSTALLER_STRATEGIES` | `declare -A` | Maps tool key → strategy (`binary`/`managed`/`system`) |
|
||||
| `INSTALLER_KEYS` | Array | Sorted list of all tool keys |
|
||||
|
||||
These are used by `routes.sh` for command routing, by `b ware` (with no arguments) for listing available tools, and by `b help` for the help output.
|
||||
|
||||
**Important**: `lib/registry.sh` must be regenerated whenever an installer is added, removed, or has its metadata header changed. Run:
|
||||
|
||||
```bash
|
||||
./scripts/generate_registry.sh
|
||||
```
|
||||
|
||||
## 7. Uninstallation Flow
|
||||
|
||||
### 7.1 Named Uninstall (`b rb <tool>`)
|
||||
|
||||
```
|
||||
b rb bat
|
||||
│
|
||||
▼
|
||||
uninstall_tool "bat"
|
||||
│
|
||||
├── 1. execute_rollback "bat"
|
||||
│ → Reads bat.cmds line-by-line (LIFO)
|
||||
│ → Executes: rm binary, rm env snippet, rm alias snippet, etc.
|
||||
│ → Deletes bat.cmds
|
||||
│
|
||||
├── 2. Reference-count system dependencies
|
||||
│ → For each dep in registry_get_sys_deps("bat"):
|
||||
│ → Count how many OTHER tools also list this dep
|
||||
│ → If count == 0: pkg_remove the dep
|
||||
│ → If count > 0: keep it
|
||||
│
|
||||
├── 3. registry_remove_tool "bat"
|
||||
│ → Deletes .tools.bat from registry.json
|
||||
│
|
||||
└── 4. Remove "INSTALL: bat" from history.log
|
||||
```
|
||||
|
||||
### 7.2 Multi-Tool Uninstall (`b rb bat,yazi,zoxide`)
|
||||
|
||||
The argument is split on commas and `uninstall_tool` is called for each tool sequentially.
|
||||
|
||||
### 7.3 Bare Rollback (`b rb`)
|
||||
|
||||
Reads the last line of `history.log`. If it is `INSTALL: <tool>`, calls `uninstall_tool` for that tool.
|
||||
|
||||
### 7.4 Savepoint Rollback (`b rb <savepoint>`)
|
||||
|
||||
If the argument doesn't match a registered tool, it is treated as a savepoint name. The system walks `history.log` backwards, uninstalling each tool encountered until it reaches the matching `SAVEPOINT:` entry.
|
||||
|
||||
## 8. Strategy-Specific Patterns
|
||||
|
||||
### 8.1 Binary Strategy Example (Bat)
|
||||
|
||||
```bash
|
||||
# Detect arch, fetch latest tag from GitHub
|
||||
latest_tag=$(github_get_latest_release "sharkdp/bat")
|
||||
|
||||
# Download the matching release asset
|
||||
github_download_asset "sharkdp/bat" "$latest_tag" "bat-${latest_tag}-${target}\.tar\.gz" "$archive"
|
||||
|
||||
# Extract, copy binary to $BOOTSTRAP_BIN, make executable
|
||||
cp "$extract_dir/bat" "$BOOTSTRAP_BIN/bat"
|
||||
chmod +x "$BOOTSTRAP_BIN/bat"
|
||||
track_file "$BOOTSTRAP_BIN/bat"
|
||||
|
||||
# Register
|
||||
register_tool "bat" "binary" "$latest_tag" "github:sharkdp/bat"
|
||||
```
|
||||
|
||||
### 8.2 Managed Strategy Example (Node.js via NVM)
|
||||
|
||||
```bash
|
||||
# Download and extract NVM into $BOOTSTRAP_RUNTIMES/nvm
|
||||
download_file "$nvm_url" "$TMP_DIR/nvm.tar.gz"
|
||||
tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$BOOTSTRAP_RUNTIMES/nvm" --strip-components=1
|
||||
track_dir "$BOOTSTRAP_RUNTIMES/nvm"
|
||||
|
||||
# Write env snippet so NVM loads on shell startup
|
||||
write_env_snippet "node" "$content"
|
||||
|
||||
# Use NVM to install Node LTS
|
||||
nvm install --lts
|
||||
|
||||
# Register (NVM manages the actual node binary)
|
||||
register_tool "node" "managed" "$latest_tag" "github:nvm-sh/nvm"
|
||||
```
|
||||
|
||||
### 8.3 System Strategy Example (Docker)
|
||||
|
||||
```bash
|
||||
# Install via system package manager
|
||||
pkg_install "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Register the dependency for reference counting
|
||||
registry_add_sys_deps "docker" "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Additional system configuration (groups, systemd)
|
||||
sudo usermod -aG docker "$USER"
|
||||
add_rollback_cmd "sudo gpasswd -d $USER docker || true"
|
||||
|
||||
# Register
|
||||
register_tool "docker" "system" "" "os-package-manager"
|
||||
```
|
||||
@@ -3,8 +3,10 @@
|
||||
## 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.
|
||||
## 2. Core Concept: Procedural Manifests & JSON Registry
|
||||
The system uses a hybrid approach:
|
||||
1. **Procedural Command Manifests**: As the installation progresses, a LIFO script manifest (`$BOOTSTRAP_STATE_DIR/uninstallers/<tool>.cmds`) is built by prepending the inverse commands for each helper action (files, directories, env/alias snippets).
|
||||
2. **Centralized JSON Registry**: Metadata, strategy, and system-level dependencies are tracked in a thread-safe `$BOOTSTRAP_STATE_DIR/registry.json` using `jq`. During uninstallation, this registry is used to reference-count and safely remove shared system dependencies.
|
||||
|
||||
## 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.
|
||||
@@ -33,17 +35,19 @@ INSTALL: nvim
|
||||
```
|
||||
|
||||
### C. Bare Rollback (`b rb`)
|
||||
When `b rb` is executed without arguments, it rolls back the single most recent change:
|
||||
When `b rb` is executed without arguments, it rolls back the single most recent installation:
|
||||
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.
|
||||
2. Runs `uninstall_tool nvim` which executes `nvim.cmds` and cleans up registry entries.
|
||||
|
||||
### D. Savepoint Rollback (`b rb <name>`)
|
||||
When `b rb init` is executed, it rolls back all changes made after that savepoint:
|
||||
### D. Named Rollback (`b rb <tool1>,<tool2>...`)
|
||||
Users can uninstall specific tools by name (e.g., `b rb nvim` or `b rb nvim,yazi`). The system runs the corresponding uninstaller manifests, reference-counts and removes any orphaned system dependencies, and cleans up the history log and registry.
|
||||
|
||||
### E. Savepoint Rollback (`b rb <savepoint>`)
|
||||
If the argument does not match an installed tool in the registry, it is treated as a 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.
|
||||
2. For each `INSTALL: <tool>` encountered, it runs `uninstall_tool <tool>`.
|
||||
3. Stops when it reaches the specified `SAVEPOINT: <name>`.
|
||||
4. Truncates the history log back to that savepoint.
|
||||
|
||||
## 4. Required Abstractions & Helper Modifications
|
||||
|
||||
@@ -64,22 +68,18 @@ add_rollback_cmd() {
|
||||
}
|
||||
```
|
||||
|
||||
### C. Modifying Existing Helpers
|
||||
Existing helpers automatically generate their own inverse commands.
|
||||
- **`pkg_install`:**
|
||||
### C. Helper Operations
|
||||
Helper functions record their inverse actions directly to the manifest:
|
||||
- **`write_env_snippet` / `write_alias_snippet` / `write_completion_snippet`:**
|
||||
```bash
|
||||
add_rollback_cmd "pkg_remove $pkg"
|
||||
add_rollback_cmd "rm -f \"\$BOOTSTRAP_DIR/env.d/\$snippet_name.sh\""
|
||||
```
|
||||
- **`write_env_snippet` / `write_alias_snippet`:**
|
||||
- **`track_file` / `track_dir`**:
|
||||
```bash
|
||||
add_rollback_cmd "rm -f \"$HOME/.config/bootstrap/env.d/$snippet_name.sh\""
|
||||
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
|
||||
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
|
||||
```
|
||||
|
||||
### D. New File Tracking Helpers
|
||||
```bash
|
||||
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
|
||||
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
|
||||
```
|
||||
Note: distro packages are not tracked via `add_rollback_cmd`. Instead, installers declare them as system dependencies (see below).
|
||||
|
||||
## 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.
|
||||
@@ -102,9 +102,11 @@ Because `b ware <tool>` allows users to modify installation scripts:
|
||||
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.
|
||||
System dependencies installed via `pkg_install` must be registered via `registry_add_sys_deps <tool> <dep1> <dep2>...`.
|
||||
When a tool is uninstalled:
|
||||
1. The registry is checked to see if any other installed tool still lists those dependencies.
|
||||
2. If the reference count drops to `0`, the dependency is automatically removed using `pkg_remove`.
|
||||
3. If other tools still depend on it, it is kept.
|
||||
|
||||
## 8. Fault Tolerance, Resumability, and Interrupted Installations
|
||||
|
||||
|
||||
@@ -2,21 +2,16 @@
|
||||
# Tool: agy
|
||||
# DisplayName: Antigravity
|
||||
# Description: Install Antigravity CLI
|
||||
# Strategy: binary
|
||||
#
|
||||
# Antigravity CLI Installer Script (Linux Only)
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Constants
|
||||
DOWNLOAD_BASE_URL="https://antigravity-cli-auto-updater-974169037036.us-central1.run.app"
|
||||
TARGET_DIR="$HOME/.local/bin"
|
||||
TARGET_DIR="$BOOTSTRAP_BIN"
|
||||
BINARY_PATH="$TARGET_DIR/agy"
|
||||
|
||||
install_agy() {
|
||||
@@ -55,19 +50,12 @@ install_agy() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# POSIX-compliant JSON parser (no jq dependencies)
|
||||
parse_json_key() {
|
||||
local payload="$1"
|
||||
local key="$2"
|
||||
echo "$payload" | sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
|
||||
}
|
||||
|
||||
local version
|
||||
local url
|
||||
local sha512
|
||||
version=$(parse_json_key "$manifest_json" "version")
|
||||
url=$(parse_json_key "$manifest_json" "url")
|
||||
sha512=$(parse_json_key "$manifest_json" "sha512")
|
||||
version=$(echo "$manifest_json" | jq -r '.version // empty')
|
||||
url=$(echo "$manifest_json" | jq -r '.url // empty')
|
||||
sha512=$(echo "$manifest_json" | jq -r '.sha512 // empty')
|
||||
|
||||
if [ -z "$url" ] || [ -z "$sha512" ]; then
|
||||
log_error "Failed to parse release manifest."
|
||||
@@ -134,16 +122,11 @@ install_agy() {
|
||||
track_file "$BINARY_PATH"
|
||||
|
||||
log_success "Antigravity CLI successfully installed to $BINARY_PATH."
|
||||
register_tool "agy" "binary" "" "github:sortedcord/agy"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "local-bin path"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
}
|
||||
|
||||
run_handoff() {
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
# Tool: asciicinema
|
||||
# DisplayName: asciicinema
|
||||
# Description: Install asciinema terminal recorder
|
||||
# Strategy: binary
|
||||
#
|
||||
# asciinema Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -21,12 +16,9 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
install_asciicinema() {
|
||||
local latest_tag=""
|
||||
if has_command curl; then
|
||||
log_info "Fetching latest asciinema version from GitHub..."
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/asciinema/asciinema/releases/latest \
|
||||
| grep '"tag_name":' | head -n1 \
|
||||
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
latest_tag=$(github_get_latest_release "asciinema/asciinema")
|
||||
fi
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
@@ -73,22 +65,21 @@ install_asciicinema() {
|
||||
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
local download_url="https://github.com/asciinema/asciinema/releases/download/${latest_tag}/asciinema-${asciinema_arch}"
|
||||
|
||||
log_info "Downloading asciinema ${latest_tag} for ${arch}..."
|
||||
download_file "$download_url" "$TMP_DIR/asciinema"
|
||||
github_download_asset "asciinema/asciinema" "$latest_tag" "asciinema-${asciinema_arch}" "$TMP_DIR/asciinema"
|
||||
|
||||
log_info "Installing asciinema to /usr/local/bin..."
|
||||
sudo cp "$TMP_DIR/asciinema" /usr/local/bin/asciinema
|
||||
sudo chmod +x /usr/local/bin/asciinema
|
||||
track_file "/usr/local/bin/asciinema"
|
||||
log_info "Installing asciinema to $BOOTSTRAP_BIN..."
|
||||
cp "$TMP_DIR/asciinema" "$BOOTSTRAP_BIN/asciinema"
|
||||
chmod +x "$BOOTSTRAP_BIN/asciinema"
|
||||
track_file "$BOOTSTRAP_BIN/asciinema"
|
||||
|
||||
# Create compatibility symlink matching the installer name spelling
|
||||
log_info "Creating compatibility symlink for asciicinema..."
|
||||
sudo ln -sf /usr/local/bin/asciinema /usr/local/bin/asciicinema
|
||||
ln -sf "$BOOTSTRAP_BIN/asciinema" /usr/local/bin/asciicinema
|
||||
track_file "/usr/local/bin/asciicinema"
|
||||
|
||||
log_success "asciinema ${latest_tag} installed."
|
||||
register_tool "asciicinema" "binary" "$latest_tag" "github:asciinema/asciinema"
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
# Strategy: binary
|
||||
#
|
||||
# Bat Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -21,69 +16,54 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
install_bat() {
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
if [ "$distro" = "arch" ]; then
|
||||
log_info "Arch Linux detected"
|
||||
log_info "Installing Bat..."
|
||||
pkg_install bat
|
||||
|
||||
elif [ "$distro" = "fedora" ]; then
|
||||
log_info "Fedora detected"
|
||||
log_info "Installing Bat..."
|
||||
pkg_install bat
|
||||
|
||||
elif [ "$distro" = "debian" ]; then
|
||||
log_info "Debian/Ubuntu detected"
|
||||
|
||||
pkg_install curl
|
||||
|
||||
log_info "Fetching latest Bat version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/sharkdp/bat/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.26.1"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
else
|
||||
log_info "Latest Bat version found: $latest_tag"
|
||||
if has_command bat; then
|
||||
if ! confirm "Bat is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Bat installation."
|
||||
return
|
||||
fi
|
||||
|
||||
# Remove leading 'v' for file name version
|
||||
local version="${latest_tag#v}"
|
||||
|
||||
# Detect architecture mapping
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
local deb_arch="amd64"
|
||||
if [ "$arch" = "arm64" ]; then
|
||||
deb_arch="arm64"
|
||||
fi
|
||||
|
||||
local deb_url="https://github.com/sharkdp/bat/releases/download/${latest_tag}/bat_${version}_${deb_arch}.deb"
|
||||
log_info "Downloading Bat from ${deb_url}..."
|
||||
download_file "$deb_url" "$TMP_DIR/bat.deb"
|
||||
|
||||
log_info "Installing Bat package..."
|
||||
sudo apt install -y "$TMP_DIR/bat.deb"
|
||||
add_rollback_cmd "sudo apt remove -y bat"
|
||||
|
||||
else
|
||||
log_error "Unsupported distribution."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
local target=""
|
||||
case "$arch" in
|
||||
x86_64) target="x86_64-unknown-linux-gnu" ;;
|
||||
arm64) target="aarch64-unknown-linux-gnu" ;;
|
||||
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
log_info "Fetching latest Bat version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "sharkdp/bat")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.26.1"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
else
|
||||
log_info "Latest Bat version found: $latest_tag"
|
||||
fi
|
||||
|
||||
log_info "Downloading Bat ${latest_tag}..."
|
||||
local archive="$TMP_DIR/bat.tar.gz"
|
||||
github_download_asset "sharkdp/bat" "$latest_tag" "bat-${latest_tag}-${target}\.tar\.gz" "$archive"
|
||||
|
||||
log_info "Extracting Bat binary..."
|
||||
tar -xzf "$archive" -C "$TMP_DIR"
|
||||
|
||||
local extract_dir="$TMP_DIR/bat-${latest_tag}-${target}"
|
||||
|
||||
local target_dir="$BOOTSTRAP_BIN"
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
log_info "Installing Bat to $target_dir/bat..."
|
||||
cp "$extract_dir/bat" "$target_dir/bat"
|
||||
chmod +x "$target_dir/bat"
|
||||
track_file "$target_dir/bat"
|
||||
|
||||
register_tool "bat" "binary" "$latest_tag" "github:sharkdp/bat"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "bat alias"
|
||||
done
|
||||
if [ -f "$HOME/.bash_aliases" ]; then
|
||||
remove_block "$HOME/.bash_aliases" "bat alias"
|
||||
fi
|
||||
|
||||
write_alias_snippet "bat" "alias cat='bat --paging=never -p'"
|
||||
}
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
# Tool: docker
|
||||
# DisplayName: Docker
|
||||
# Description: Container runtime and orchestration platform
|
||||
# Strategy: system
|
||||
#
|
||||
# Docker Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Installation Logic ──────────────────────────────────────────────
|
||||
@@ -29,8 +24,9 @@ install_docker() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use pkg_install for distro packages (it automatically handles rollback hooks for the packages!)
|
||||
# Use pkg_install for distro packages and explicitly register them for reference-counted rollback
|
||||
pkg_install "arch:docker|debian:docker.io|fedora:docker"
|
||||
registry_add_sys_deps "docker" "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Ensure docker group exists (some distros might not create it immediately)
|
||||
if ! getent group docker >/dev/null 2>&1; then
|
||||
@@ -58,6 +54,7 @@ install_docker() {
|
||||
sudo systemctl enable --now docker.service || true
|
||||
sudo systemctl enable --now containerd.service || true
|
||||
fi
|
||||
register_tool "docker" "system" "" "os-package-manager"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
103
installers/install_hyperfine.sh
Normal file
103
installers/install_hyperfine.sh
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: hyperfine
|
||||
# DisplayName: Hyperfine
|
||||
# Description: Command-line benchmarking tool
|
||||
# Strategy: binary
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_hyperfine() {
|
||||
if has_command hyperfine; then
|
||||
if ! confirm "Hyperfine is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Hyperfine installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install Hyperfine?"; then
|
||||
log_info "Skipping Hyperfine installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
local raw_arch
|
||||
raw_arch=$(detect_arch)
|
||||
local arch=""
|
||||
case "$raw_arch" in
|
||||
x86_64) arch="x86_64" ;;
|
||||
arm64) arch="aarch64" ;;
|
||||
*) log_error "Unsupported Linux architecture: $raw_arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
log_info "Fetching latest Hyperfine version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "sharkdp/hyperfine")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v1.20.0"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
else
|
||||
log_info "Latest Hyperfine version found: $latest_tag"
|
||||
fi
|
||||
|
||||
local archive="$TMP_DIR/hyperfine.tar.gz"
|
||||
github_download_asset "sharkdp/hyperfine" "$latest_tag" "hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz" "$archive"
|
||||
|
||||
# Extract the archive
|
||||
log_info "Extracting Hyperfine archive..."
|
||||
tar -xzf "$archive" -C "$TMP_DIR"
|
||||
|
||||
local extract_dir="$TMP_DIR/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu"
|
||||
if [ ! -d "$extract_dir" ]; then
|
||||
# Handle case where directory name might differ
|
||||
extract_dir=$(find "$TMP_DIR" -maxdepth 1 -type d -name "hyperfine-*" | head -n1)
|
||||
fi
|
||||
|
||||
if [ -z "$extract_dir" ] || [ ! -d "$extract_dir" ]; then
|
||||
log_error "Failed to locate extracted Hyperfine directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install binary to $BOOTSTRAP_BIN
|
||||
local target_dir="$BOOTSTRAP_BIN"
|
||||
mkdir -p "$target_dir"
|
||||
log_info "Installing Hyperfine to $target_dir/hyperfine..."
|
||||
cp "$extract_dir/hyperfine" "$target_dir/hyperfine"
|
||||
chmod +x "$target_dir/hyperfine"
|
||||
track_file "$target_dir/hyperfine"
|
||||
|
||||
# Install man page if present
|
||||
if [ -f "$extract_dir/hyperfine.1" ]; then
|
||||
local man_dir="$HOME/.local/share/man/man1"
|
||||
mkdir -p "$man_dir"
|
||||
log_info "Installing man page to $man_dir/hyperfine.1..."
|
||||
cp "$extract_dir/hyperfine.1" "$man_dir/hyperfine.1"
|
||||
track_file "$man_dir/hyperfine.1"
|
||||
fi
|
||||
|
||||
# Install autocomplete if present
|
||||
if [ -f "$extract_dir/autocomplete/hyperfine.bash" ]; then
|
||||
log_info "Installing bash completions..."
|
||||
local comp_content
|
||||
comp_content=$(cat "$extract_dir/autocomplete/hyperfine.bash")
|
||||
write_completion_snippet "hyperfine" "$comp_content"
|
||||
fi
|
||||
|
||||
register_tool "hyperfine" "binary" "$latest_tag" "github:sharkdp/hyperfine"
|
||||
}
|
||||
|
||||
main() {
|
||||
install_hyperfine
|
||||
|
||||
echo
|
||||
log_success "Hyperfine installation complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
72
installers/install_lazygit.sh
Executable file
72
installers/install_lazygit.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: lazygit
|
||||
# DisplayName: lazygit
|
||||
# Description: Simple terminal UI for git commands
|
||||
# Strategy: binary
|
||||
#
|
||||
# lazygit Installer Script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Installation Logic ──────────────────────────────────────────────
|
||||
|
||||
install_lazygit() {
|
||||
if has_command lazygit; then
|
||||
if ! confirm "lazygit is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping lazygit installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install lazygit?"; then
|
||||
log_info "Skipping lazygit installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "jesseduffield/lazygit")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.62.2" # fallback
|
||||
log_warn "Failed to fetch latest version. Falling back to: $latest_tag"
|
||||
fi
|
||||
|
||||
local version="${latest_tag#v}"
|
||||
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
local arch_str="x86_64"
|
||||
if [ "$arch" = "arm64" ]; then
|
||||
arch_str="arm64"
|
||||
fi
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
local dest="$TMP_DIR/lazygit.tar.gz"
|
||||
|
||||
log_info "Downloading lazygit ${latest_tag}..."
|
||||
github_download_asset "jesseduffield/lazygit" "$latest_tag" "lazygit_${version}_linux_${arch_str}\.tar\.gz" "$dest"
|
||||
|
||||
log_info "Extracting..."
|
||||
tar -xzf "$dest" -C "$TMP_DIR"
|
||||
|
||||
mkdir -p "$BOOTSTRAP_BIN"
|
||||
cp "$TMP_DIR/lazygit" "$HOME/.local/bin/lazygit"
|
||||
chmod +x "$HOME/.local/bin/lazygit"
|
||||
track_file "$HOME/.local/bin/lazygit"
|
||||
register_tool "lazygit" "binary" "$latest_tag" "github:jesseduffield/lazygit"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
install_lazygit
|
||||
|
||||
echo
|
||||
log_success "lazygit installation complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -2,16 +2,11 @@
|
||||
# Tool: node
|
||||
# DisplayName: Node
|
||||
# Description: Install Node.js (LTS) and NVM
|
||||
# Strategy: managed
|
||||
#
|
||||
# Node.js and NVM Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -21,7 +16,7 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
install_nvm() {
|
||||
if has_command nvm || [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||
if has_command nvm || [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then
|
||||
log_info "NVM is already installed."
|
||||
fi
|
||||
|
||||
@@ -29,12 +24,13 @@ install_nvm() {
|
||||
if ! has_command tar; then
|
||||
log_info "tar not found. Installing tar..."
|
||||
pkg_install tar
|
||||
registry_add_sys_deps "node" "tar"
|
||||
fi
|
||||
|
||||
# Try to fetch the latest version of NVM from GitHub API
|
||||
log_info "Fetching the latest NVM version..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
latest_tag=$(github_get_latest_release "nvm-sh/nvm")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.40.5" # Fallback version if API request fails
|
||||
@@ -47,25 +43,20 @@ install_nvm() {
|
||||
log_info "Downloading NVM from $nvm_url..."
|
||||
download_file "$nvm_url" "$TMP_DIR/nvm.tar.gz"
|
||||
|
||||
log_info "Extracting NVM archive directly to $HOME/.nvm (stripping versioned subfolder to keep config generic)..."
|
||||
mkdir -p "$HOME/.nvm"
|
||||
tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
|
||||
log_info "Extracting NVM archive directly to $BOOTSTRAP_RUNTIMES/nvm (stripping versioned subfolder to keep config generic)..."
|
||||
mkdir -p "$BOOTSTRAP_RUNTIMES/nvm"
|
||||
tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$BOOTSTRAP_RUNTIMES/nvm" --strip-components=1
|
||||
|
||||
track_dir "$HOME/.nvm"
|
||||
track_dir "$BOOTSTRAP_RUNTIMES/nvm"
|
||||
|
||||
log_success "NVM source files successfully extracted to $HOME/.nvm."
|
||||
log_success "NVM source files successfully extracted to $BOOTSTRAP_RUNTIMES/nvm."
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "nvm setup"
|
||||
done
|
||||
|
||||
local content
|
||||
content=$(cat << 'EOF'
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
export NVM_DIR="$BOOTSTRAP_RUNTIMES/nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load NVM
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # Load NVM bash completion
|
||||
EOF
|
||||
@@ -76,10 +67,11 @@ EOF
|
||||
|
||||
install_node() {
|
||||
# Ensure NVM is loaded in this script context
|
||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||
if [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then
|
||||
# Temporarily disable nounset as nvm.sh does not support set -u
|
||||
set +u
|
||||
. "$HOME/.nvm/nvm.sh"
|
||||
# shellcheck source=/dev/null
|
||||
. "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh"
|
||||
else
|
||||
log_error "Could not load NVM to install Node.js."
|
||||
return 1
|
||||
@@ -95,6 +87,7 @@ install_node() {
|
||||
nvm alias default 'lts/*'
|
||||
log_success "Node.js installed successfully!"
|
||||
set -u
|
||||
register_tool "node" "managed" "$latest_tag" "github:nvm-sh/nvm"
|
||||
}
|
||||
|
||||
main() {
|
||||
@@ -106,7 +99,7 @@ main() {
|
||||
if has_command node; then
|
||||
log_success "Node.js (via NVM) installation and configuration complete."
|
||||
log_info "Installed Node version: $(node --version)"
|
||||
log_info "Installed NVM version: $(nvm --version 2>/dev/null || cat "$HOME/.nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")"
|
||||
log_info "Installed NVM version: $(nvm --version 2>/dev/null || grep '"version":' "$BOOTSTRAP_RUNTIMES/nvm/package.json" | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")"
|
||||
else
|
||||
log_success "Installation complete."
|
||||
fi
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# Tool: nvim
|
||||
# DisplayName: Neovim
|
||||
# Description: Install Neovim 0.12.0 and configuration
|
||||
# Strategy: binary
|
||||
#
|
||||
# Neovim Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NVIM_VERSION="0.12.0"
|
||||
NVIM_INSTALL_DIR="/opt/nvim"
|
||||
NVIM_INSTALL_DIR="$BOOTSTRAP_OPT/nvim"
|
||||
NVIM_BIN_DIR="$BOOTSTRAP_BIN"
|
||||
NVIM_CONFIG_REPO="https://git.adityagupta.dev/sortedcord/editor.git"
|
||||
NVIM_CONFIG_DIR="$HOME/.config/nvim"
|
||||
|
||||
@@ -33,7 +30,18 @@ check_config_dir() {
|
||||
install_packages() {
|
||||
log_info "Detecting distribution and installing dependencies..."
|
||||
pkg_install \
|
||||
git tar curl unzip ripgrep fzf nodejs npm xclip wl-clipboard \
|
||||
git tar unzip ripgrep fzf nodejs npm xclip wl-clipboard \
|
||||
"arch:fd|debian:fd-find|fedora:fd-find" \
|
||||
"arch:cmake|debian:cmake|fedora:cmake" \
|
||||
"arch:make|debian:build-essential|fedora:make" \
|
||||
"arch:gcc|debian:build-essential|fedora:gcc" \
|
||||
"arch:python|debian:python3|fedora:python3" \
|
||||
"debian:python3-pip|fedora:python3-pip" \
|
||||
"debian:python3-venv" \
|
||||
"fedora:gcc-c++"
|
||||
|
||||
registry_add_sys_deps "nvim" \
|
||||
git tar unzip ripgrep fzf nodejs npm xclip wl-clipboard \
|
||||
"arch:fd|debian:fd-find|fedora:fd-find" \
|
||||
"arch:cmake|debian:cmake|fedora:cmake" \
|
||||
"arch:make|debian:build-essential|fedora:make" \
|
||||
@@ -76,23 +84,23 @@ install_nvim() {
|
||||
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
local nvim_url="https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-${nvim_arch}.tar.gz"
|
||||
|
||||
log_info "Downloading Neovim v${NVIM_VERSION} for ${arch}..."
|
||||
download_file "$nvim_url" "$TMP_DIR/nvim.tar.gz"
|
||||
github_download_asset "neovim/neovim" "v${NVIM_VERSION}" "nvim-${nvim_arch}\.tar\.gz" "$TMP_DIR/nvim.tar.gz"
|
||||
|
||||
tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR"
|
||||
|
||||
sudo rm -rf "$NVIM_INSTALL_DIR"
|
||||
sudo mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
|
||||
rm -rf "$NVIM_INSTALL_DIR"
|
||||
mkdir -p "$(dirname "$NVIM_INSTALL_DIR")"
|
||||
mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
|
||||
|
||||
sudo ln -sf "$NVIM_INSTALL_DIR/bin/nvim" /usr/local/bin/nvim
|
||||
ln -sf "$NVIM_INSTALL_DIR/bin/nvim" "$NVIM_BIN_DIR/nvim"
|
||||
|
||||
track_dir "$NVIM_INSTALL_DIR"
|
||||
track_file "/usr/local/bin/nvim"
|
||||
track_file "$NVIM_BIN_DIR/nvim"
|
||||
|
||||
log_success "Installed:"
|
||||
nvim --version | head -n1
|
||||
register_tool "nvim" "binary" "$NVIM_VERSION" "github:neovim/neovim"
|
||||
}
|
||||
|
||||
install_config() {
|
||||
@@ -111,24 +119,6 @@ install_config() {
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Clean up legacy inline edits from bashrc and bash_aliases
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
if [ -f "$config_file" ]; then
|
||||
local tmp_file
|
||||
tmp_file=$(mktemp)
|
||||
sed '/^export EDITOR="nvim"/d' "$config_file" > "$tmp_file"
|
||||
cat "$tmp_file" > "$config_file"
|
||||
rm -f "$tmp_file"
|
||||
fi
|
||||
done
|
||||
if [ -f "$HOME/.bash_aliases" ]; then
|
||||
local tmp_file
|
||||
tmp_file=$(mktemp)
|
||||
sed '/^alias vim="nvim"/d' "$HOME/.bash_aliases" > "$tmp_file"
|
||||
cat "$tmp_file" > "$HOME/.bash_aliases"
|
||||
rm -f "$tmp_file"
|
||||
fi
|
||||
|
||||
write_alias_snippet "nvim" 'alias vim="nvim"'
|
||||
write_env_snippet "nvim" 'export EDITOR="nvim"'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Tool: pnpm
|
||||
# DisplayName: Pnpm
|
||||
# Description: Install pnpm package manager
|
||||
# Strategy: binary
|
||||
#
|
||||
# pnpm Installer Script
|
||||
#
|
||||
@@ -17,12 +18,6 @@
|
||||
# curl -fsSL https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -127,14 +122,17 @@ install_pnpm() {
|
||||
}
|
||||
libc_suffix="$(detect_libc_suffix)"
|
||||
|
||||
# Fetch the latest version from the npm registry, or use PNPM_VERSION if set
|
||||
# Fetch the latest version from GitHub, or use PNPM_VERSION if set
|
||||
if [ -z "${PNPM_VERSION:-}" ]; then
|
||||
log_info "Fetching latest pnpm version from npm registry..."
|
||||
version_json="$(download "https://registry.npmjs.org/@pnpm/exe")" || {
|
||||
log_error "Failed to fetch pnpm version info from npm registry."
|
||||
log_info "Fetching latest pnpm version from GitHub..."
|
||||
local tag
|
||||
tag=$(github_get_latest_release "pnpm/pnpm")
|
||||
if [ -n "$tag" ]; then
|
||||
version="${tag#v}"
|
||||
else
|
||||
log_error "Failed to fetch pnpm version info from GitHub."
|
||||
return 1
|
||||
}
|
||||
version="$(echo "$version_json" | grep -o '"latest":[[:space:]]*"[0-9.]*"' | grep -o '[0-9.]*')"
|
||||
fi
|
||||
else
|
||||
version="${PNPM_VERSION}"
|
||||
fi
|
||||
@@ -151,7 +149,7 @@ install_pnpm() {
|
||||
|
||||
if [ "$major_version" -ge 11 ]; then
|
||||
# v11+: distributed as tarballs containing the binary and dist/ directory
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}.tar.gz" "$TMP_DIR/pnpm.tar.gz" || {
|
||||
github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}\.tar\.gz" "$TMP_DIR/pnpm.tar.gz" || {
|
||||
log_error "Failed to download pnpm tarball."
|
||||
return 1
|
||||
}
|
||||
@@ -166,7 +164,7 @@ install_pnpm() {
|
||||
}
|
||||
else
|
||||
# Older versions: distributed as a single executable binary
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}" "$TMP_DIR/pnpm" || {
|
||||
github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}" "$TMP_DIR/pnpm" || {
|
||||
log_error "Failed to download pnpm binary."
|
||||
return 1
|
||||
}
|
||||
@@ -179,23 +177,19 @@ install_pnpm() {
|
||||
|
||||
track_dir "$HOME/.local/share/pnpm"
|
||||
log_success "pnpm v${version} installed successfully!"
|
||||
register_tool "pnpm" "binary" "$version" "github:pnpm/pnpm"
|
||||
}
|
||||
|
||||
# ─── Shell Configuration ─────────────────────────────────────────────
|
||||
|
||||
configure_shell() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "pnpm setup"
|
||||
done
|
||||
|
||||
# pnpm's `setup --force` configures PNPM_HOME and PATH automatically,
|
||||
# but we also add an env block to ensure PNPM_HOME is set consistently.
|
||||
local content
|
||||
content=$(cat << 'EOF'
|
||||
# pnpm
|
||||
export PNPM_HOME="$HOME/.local/share/pnpm"
|
||||
export PNPM_HOME="$BOOTSTRAP_RUNTIMES/pnpm"
|
||||
case ":$PATH:" in
|
||||
*":$PNPM_HOME:"*) ;;
|
||||
*) export PATH="$PNPM_HOME:$PATH" ;;
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# Tool: rust
|
||||
# DisplayName: Rust
|
||||
# Description: Install Rustup and Rust compiler/toolchain
|
||||
# Strategy: managed
|
||||
#
|
||||
# Rust Installer Script (Simplified Local Rustup Init)
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -20,14 +16,6 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Ensure we have curl
|
||||
install_downloader() {
|
||||
if ! has_command curl; then
|
||||
log_info "curl not found. Installing curl..."
|
||||
pkg_install curl
|
||||
fi
|
||||
}
|
||||
|
||||
detect_target_triple() {
|
||||
local ostype
|
||||
ostype="$(uname -s)"
|
||||
@@ -61,11 +49,14 @@ detect_target_triple() {
|
||||
}
|
||||
|
||||
install_rust() {
|
||||
if has_command rustup || [ -f "$HOME/.cargo/bin/rustup" ]; then
|
||||
export CARGO_HOME="$BOOTSTRAP_RUNTIMES/cargo"
|
||||
export RUSTUP_HOME="$BOOTSTRAP_RUNTIMES/rustup"
|
||||
|
||||
if has_command rustup || [ -f "$BOOTSTRAP_RUNTIMES/cargo/bin/rustup" ]; then
|
||||
log_info "Rust (rustup) is already installed."
|
||||
fi
|
||||
|
||||
install_downloader
|
||||
|
||||
|
||||
local target
|
||||
target=$(detect_target_triple)
|
||||
@@ -87,19 +78,19 @@ install_rust() {
|
||||
"$dest" -y --no-modify-path
|
||||
|
||||
add_rollback_cmd "rustup self uninstall -y"
|
||||
register_tool "rust" "managed" "" "rustup"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Add ~/.cargo/bin to PATH for the current process
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "rust init"
|
||||
done
|
||||
|
||||
write_env_snippet "rust" '. "$HOME/.cargo/env"'
|
||||
local snippet_content=$(cat << 'EOF'
|
||||
export CARGO_HOME="$BOOTSTRAP_RUNTIMES/cargo"
|
||||
export RUSTUP_HOME="$BOOTSTRAP_RUNTIMES/rustup"
|
||||
. "$CARGO_HOME/env"
|
||||
EOF
|
||||
)
|
||||
write_env_snippet "rust" "$snippet_content"
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# Tool: starship
|
||||
# DisplayName: Starship
|
||||
# Description: Install Starship shell prompt
|
||||
# Strategy: binary
|
||||
#
|
||||
# Starship Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -25,12 +21,6 @@ install_starship() {
|
||||
log_info "Starship is already installed."
|
||||
fi
|
||||
|
||||
# Ensure curl is installed
|
||||
if ! has_command curl; then
|
||||
log_info "curl not found. Installing curl..."
|
||||
pkg_install curl
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
local raw_arch
|
||||
raw_arch=$(detect_arch)
|
||||
@@ -45,47 +35,33 @@ install_starship() {
|
||||
|
||||
log_info "Fetching latest Starship version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/starship/starship/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
|
||||
local download_url
|
||||
if [ -n "$latest_tag" ]; then
|
||||
log_info "Latest Starship version found: $latest_tag"
|
||||
download_url="https://github.com/starship/starship/releases/download/${latest_tag}/starship-${target}.tar.gz"
|
||||
else
|
||||
latest_tag=$(github_get_latest_release "starship/starship")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="latest"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to downloading latest release directly."
|
||||
download_url="https://github.com/starship/starship/releases/latest/download/starship-${target}.tar.gz"
|
||||
fi
|
||||
|
||||
log_info "Downloading Starship from ${download_url}..."
|
||||
log_info "Downloading Starship ${latest_tag}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
download_file "$download_url" "$archive"
|
||||
github_download_asset "starship/starship" "$latest_tag" "starship-${target}\.tar\.gz" "$archive"
|
||||
|
||||
# Extract the binary
|
||||
log_info "Extracting Starship binary..."
|
||||
tar -xzf "$archive" -C "$TMP_DIR"
|
||||
|
||||
# Install to ~/.local/bin
|
||||
local target_dir="$HOME/.local/bin"
|
||||
local target_dir="$BOOTSTRAP_BIN"
|
||||
mkdir -p "$target_dir"
|
||||
log_info "Installing Starship to $target_dir/starship..."
|
||||
cp "$TMP_DIR/starship" "$target_dir/starship"
|
||||
chmod +x "$target_dir/starship"
|
||||
track_file "$target_dir/starship"
|
||||
register_tool "starship" "binary" "$latest_tag" "github:starship/starship"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Add ~/.local/bin to PATH for the current process
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "local-bin path"
|
||||
remove_block "$config_file" "starship init"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
write_env_snippet "starship" 'eval "$(starship init bash)"'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# Tool: uv
|
||||
# DisplayName: uv
|
||||
# Description: Fast Python package installer and resolver
|
||||
# Strategy: binary
|
||||
#
|
||||
# uv Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -28,12 +24,6 @@ install_uv() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure curl is installed
|
||||
if ! has_command curl; then
|
||||
log_info "curl not found. Installing curl..."
|
||||
pkg_install curl
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
local raw_arch
|
||||
raw_arch=$(detect_arch)
|
||||
@@ -54,28 +44,22 @@ install_uv() {
|
||||
|
||||
log_info "Fetching latest uv version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/astral-sh/uv/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
latest_tag=$(github_get_latest_release "astral-sh/uv")
|
||||
|
||||
local download_url
|
||||
if [ -n "$latest_tag" ]; then
|
||||
log_info "Latest uv version found: $latest_tag"
|
||||
download_url="https://github.com/astral-sh/uv/releases/download/${latest_tag}/uv-${target}.tar.gz"
|
||||
else
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="latest"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to downloading latest release directly."
|
||||
download_url="https://github.com/astral-sh/uv/releases/latest/download/uv-${target}.tar.gz"
|
||||
fi
|
||||
|
||||
log_info "Downloading uv from ${download_url}..."
|
||||
log_info "Downloading uv ${latest_tag}..."
|
||||
local archive="$TMP_DIR/uv.tar.gz"
|
||||
download_file "$download_url" "$archive"
|
||||
github_download_asset "astral-sh/uv" "$latest_tag" "uv-${target}\.tar\.gz" "$archive"
|
||||
|
||||
# Extract the binaries
|
||||
log_info "Extracting uv binaries..."
|
||||
tar -xzf "$archive" --strip-components 1 -C "$TMP_DIR"
|
||||
|
||||
# Install to ~/.local/bin
|
||||
local target_dir="$HOME/.local/bin"
|
||||
local target_dir="$BOOTSTRAP_BIN"
|
||||
mkdir -p "$target_dir"
|
||||
log_info "Installing uv and uvx to $target_dir..."
|
||||
cp "$TMP_DIR/uv" "$target_dir/uv"
|
||||
@@ -83,20 +67,12 @@ install_uv() {
|
||||
chmod +x "$target_dir/uv" "$target_dir/uvx"
|
||||
track_file "$target_dir/uv"
|
||||
track_file "$target_dir/uvx"
|
||||
register_tool "uv" "binary" "$latest_tag" "github:astral-sh/uv"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Add ~/.local/bin to PATH for the current process
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "local-bin path"
|
||||
remove_block "$config_file" "uv completion"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
write_env_snippet "uv" 'eval "$(uv generate-shell-completion bash)"'
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
# Tool: yay
|
||||
# DisplayName: Yay
|
||||
# Description: Install Yay AUR helper
|
||||
# Strategy: system
|
||||
#
|
||||
# Yay Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Installation Logic ──────────────────────────────────────────────
|
||||
@@ -43,6 +38,7 @@ install_yay() {
|
||||
else
|
||||
log_info "Dependencies (git and base-devel) are already present. Skipping package installation."
|
||||
fi
|
||||
registry_add_sys_deps "yay" "git" "base-devel"
|
||||
|
||||
log_info "Cloning yay-bin repository..."
|
||||
local clone_dir
|
||||
@@ -66,6 +62,7 @@ install_yay() {
|
||||
cd "$orig_dir"
|
||||
log_info "Cleaning up installer directory..."
|
||||
rm -rf "$clone_dir"
|
||||
register_tool "yay" "system" "" "aur:yay-bin"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
# Tool: yazi
|
||||
# DisplayName: Yazi
|
||||
# Description: Install Yazi terminal file manager and dependencies
|
||||
# Strategy: binary
|
||||
#
|
||||
# Yazi Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -21,11 +16,6 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
add_y_wrapper() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "yazi wrapper"
|
||||
done
|
||||
|
||||
local wrapper_content
|
||||
wrapper_content=$(cat << 'EOF'
|
||||
@@ -40,77 +30,69 @@ y() {
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
write_alias_snippet "yazi" "$wrapper_content"
|
||||
}
|
||||
|
||||
install_yazi() {
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
if [ "$distro" = "arch" ]; then
|
||||
log_info "Arch Linux detected"
|
||||
if has_command yazi; then
|
||||
log_info "Yazi is already installed."
|
||||
if has_command yazi; then
|
||||
if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Yazi installation."
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Installing Yazi..."
|
||||
pkg_install yazi
|
||||
log_info "Installing dependencies subsequently..."
|
||||
pkg_install ffmpeg 7zip jq poppler fd ripgrep fzf zoxide resvg imagemagick
|
||||
|
||||
elif [ "$distro" = "debian" ]; then
|
||||
log_info "Debian/Ubuntu detected"
|
||||
if has_command yazi; then
|
||||
log_info "Yazi is already installed."
|
||||
fi
|
||||
|
||||
pkg_install curl git
|
||||
|
||||
log_info "Fetching latest Yazi version from GitHub..."
|
||||
local latest_tag
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/sxyazi/yazi/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v26.5.6"
|
||||
fi
|
||||
|
||||
local deb_url="https://github.com/sxyazi/yazi/releases/download/${latest_tag}/yazi-x86_64-unknown-linux-gnu.deb"
|
||||
log_info "Downloading Yazi ${latest_tag} from ${deb_url}..."
|
||||
download_file "$deb_url" "$TMP_DIR/yazi.deb"
|
||||
|
||||
log_info "Installing Yazi package..."
|
||||
sudo apt install -y "$TMP_DIR/yazi.deb"
|
||||
add_rollback_cmd "sudo apt remove -y yazi"
|
||||
|
||||
log_info "Installing dependencies subsequently..."
|
||||
pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick 7zip || \
|
||||
pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick p7zip-full
|
||||
|
||||
create_fd_symlink
|
||||
|
||||
elif [ "$distro" = "fedora" ]; then
|
||||
log_info "Fedora detected"
|
||||
if has_command yazi; then
|
||||
log_info "Yazi is already installed."
|
||||
fi
|
||||
|
||||
log_info "Installing dnf-plugins-core..."
|
||||
pkg_install dnf-plugins-core
|
||||
|
||||
log_info "Enabling lihaohong/yazi copr repo..."
|
||||
sudo dnf copr enable -y lihaohong/yazi
|
||||
|
||||
log_info "Installing Yazi (without weak dependencies first)..."
|
||||
sudo dnf install -y yazi --setopt=install_weak_deps=False
|
||||
add_rollback_cmd "sudo dnf remove -y yazi"
|
||||
|
||||
log_info "Installing weak dependencies subsequently..."
|
||||
pkg_install yazi
|
||||
|
||||
else
|
||||
log_error "Unsupported distribution."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure required extraction tools are installed
|
||||
if ! has_command unzip; then
|
||||
log_info "unzip not found. Installing unzip..."
|
||||
pkg_install unzip
|
||||
registry_add_sys_deps "yazi" "unzip"
|
||||
fi
|
||||
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
local target=""
|
||||
case "$arch" in
|
||||
x86_64) target="x86_64-unknown-linux-gnu" ;;
|
||||
arm64) target="aarch64-unknown-linux-gnu" ;;
|
||||
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
log_info "Fetching latest Yazi version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "sxyazi/yazi")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.3.3"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
fi
|
||||
|
||||
log_info "Downloading Yazi ${latest_tag}..."
|
||||
local archive="$TMP_DIR/yazi.zip"
|
||||
github_download_asset "sxyazi/yazi" "$latest_tag" "yazi-${target}\.zip" "$archive"
|
||||
|
||||
log_info "Extracting Yazi binaries..."
|
||||
unzip -q "$archive" -d "$TMP_DIR"
|
||||
|
||||
local extract_dir="$TMP_DIR/yazi-${target}"
|
||||
local target_dir="$BOOTSTRAP_BIN"
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
log_info "Installing Yazi to $target_dir..."
|
||||
cp "$extract_dir/yazi" "$target_dir/yazi"
|
||||
cp "$extract_dir/ya" "$target_dir/ya"
|
||||
chmod +x "$target_dir/yazi" "$target_dir/ya"
|
||||
track_file "$target_dir/yazi"
|
||||
track_file "$target_dir/ya"
|
||||
|
||||
log_info "Installing system dependencies for Yazi..."
|
||||
pkg_install ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find"
|
||||
|
||||
create_fd_symlink
|
||||
|
||||
register_tool "yazi" "binary" "$latest_tag" "github:sxyazi/yazi"
|
||||
|
||||
# Add the system dependencies to the registry for uninstallation tracking
|
||||
registry_add_sys_deps "yazi" ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find"
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# Tool: zoxide
|
||||
# DisplayName: Zoxide
|
||||
# Description: Install Zoxide directory jumper
|
||||
# Strategy: managed
|
||||
#
|
||||
# Zoxide Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
install_curl() {
|
||||
if ! has_command curl; then
|
||||
log_info "curl not found. Installing curl..."
|
||||
pkg_install curl
|
||||
fi
|
||||
}
|
||||
|
||||
install_fzf() {
|
||||
if has_command fzf; then
|
||||
@@ -29,6 +19,7 @@ install_fzf() {
|
||||
|
||||
log_info "fzf not found. Installing fzf..."
|
||||
pkg_install fzf
|
||||
registry_add_sys_deps "zoxide" "fzf"
|
||||
}
|
||||
|
||||
install_zoxide() {
|
||||
@@ -36,24 +27,17 @@ install_zoxide() {
|
||||
log_info "Zoxide is already installed."
|
||||
fi
|
||||
|
||||
install_curl
|
||||
|
||||
|
||||
log_info "Downloading and running the official zoxide installer..."
|
||||
curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh
|
||||
track_file "$HOME/.local/bin/zoxide"
|
||||
register_tool "zoxide" "managed" "" "github:ajeetdsouza/zoxide"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Add ~/.local/bin to PATH for the current process
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "zoxide init"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
write_env_snippet "zoxide" 'eval "$(zoxide init --cmd cd bash)"'
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,15 @@ if [ -n "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
fi
|
||||
_LIB_COMMON_SOURCED=1
|
||||
|
||||
# Export global environment paths with default fallbacks
|
||||
export BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
|
||||
export BOOTSTRAP_DATA_DIR="${BOOTSTRAP_DATA_DIR:-$HOME/.local/share/bootstrap}"
|
||||
export BOOTSTRAP_STATE_DIR="${BOOTSTRAP_STATE_DIR:-$HOME/.local/state/bootstrap}"
|
||||
export BOOTSTRAP_CACHE_DIR="${BOOTSTRAP_CACHE_DIR:-$HOME/.cache/bootstrap}"
|
||||
export BOOTSTRAP_BIN="${BOOTSTRAP_BIN:-$BOOTSTRAP_DATA_DIR/bin}"
|
||||
export BOOTSTRAP_OPT="${BOOTSTRAP_OPT:-$BOOTSTRAP_DATA_DIR/opt}"
|
||||
export BOOTSTRAP_RUNTIMES="${BOOTSTRAP_RUNTIMES:-$BOOTSTRAP_DATA_DIR/runtimes}"
|
||||
|
||||
# Ensure running in Bash
|
||||
require_bash() {
|
||||
if [ -z "${BASH_VERSION:-}" ]; then
|
||||
@@ -72,6 +81,7 @@ make_temp_dir() {
|
||||
version_lt() {
|
||||
[ "$1" = "$2" ] && return 1
|
||||
local IFS=.
|
||||
# shellcheck disable=SC2206
|
||||
local i ver1=($1) ver2=($2)
|
||||
for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done
|
||||
for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done
|
||||
@@ -88,7 +98,7 @@ version_lt() {
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local cache_dir="$HOME/.local/state/bootstrap/cache"
|
||||
local cache_dir="$BOOTSTRAP_CACHE_DIR/downloads"
|
||||
|
||||
mkdir -p "$cache_dir"
|
||||
|
||||
|
||||
58
lib/github.sh
Normal file
58
lib/github.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# GitHub API helper functions for Bootstrap installers
|
||||
|
||||
# Usage: github_get_latest_release <owner/repo>
|
||||
# Prints the tag_name of the latest release.
|
||||
|
||||
# Installers still use this function instead of just directly invoking download_asset function:
|
||||
# - Asset names often contain the version
|
||||
# - Installers may compare the latest tag from github against the locally installed version before doing any work.
|
||||
# - We need concrete version string so we can pass it to the reigster_tool function.
|
||||
github_get_latest_release() {
|
||||
local repo="$1"
|
||||
local tag
|
||||
tag=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" | jq -r '.tag_name // empty')
|
||||
echo "$tag"
|
||||
}
|
||||
|
||||
# Usage: github_get_download_url <owner/repo> <tag> <regex_pattern>
|
||||
# Finds the asset matching the regex pattern in the specified release tag and prints its download URL.
|
||||
github_get_download_url() {
|
||||
local repo="$1"
|
||||
local tag="$2"
|
||||
local pattern="$3"
|
||||
|
||||
# If the tag is exactly 'latest', fetch the latest release asset list
|
||||
local endpoint
|
||||
if [ "$tag" = "latest" ]; then
|
||||
endpoint="https://api.github.com/repos/$repo/releases/latest"
|
||||
else
|
||||
endpoint="https://api.github.com/repos/$repo/releases/tags/$tag"
|
||||
fi
|
||||
|
||||
local url
|
||||
url=$(curl -fsSL "$endpoint" | jq -r --arg regex "$pattern" '.assets[] | select(.name | test($regex; "i")) | .browser_download_url' | head -n1)
|
||||
echo "$url"
|
||||
}
|
||||
|
||||
# Usage: github_download_asset <owner/repo> <tag> <regex_pattern> <dest_file>
|
||||
# Resolves the URL for the matching asset and downloads it to dest_file.
|
||||
github_download_asset() {
|
||||
local repo="$1"
|
||||
local tag="$2"
|
||||
local pattern="$3"
|
||||
local dest="$4"
|
||||
|
||||
local url
|
||||
url=$(github_get_download_url "$repo" "$tag" "$pattern")
|
||||
|
||||
if [ -z "$url" ]; then
|
||||
log_error "Could not find asset matching regex '$pattern' for $repo@$tag"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Downloading $url ..."
|
||||
download_file "$url" "$dest"
|
||||
}
|
||||
|
||||
export -f github_get_latest_release github_get_download_url github_download_asset
|
||||
68
lib/json.sh
68
lib/json.sh
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# generic JSON parser in pure bash and awk.
|
||||
# reads JSON from stdin and outputs a flattened list of key-value pairs.
|
||||
# example input: {"plugins": {"my_plugin": {"version": "1.0", "arr": [1, 2]}}}
|
||||
# example output:
|
||||
# plugins.my_plugin.version="1.0"
|
||||
# plugins.my_plugin.arr[0]=1
|
||||
# plugins.my_plugin.arr[1]=2
|
||||
|
||||
# pardon my french
|
||||
parse_json() {
|
||||
# Tokenize the JSON using grep
|
||||
grep -oE '"([^"\\]|\\.)*"|true|false|null|[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?|[][}{:,]' | \
|
||||
awk '
|
||||
BEGIN {
|
||||
depth=0;
|
||||
key=""
|
||||
}
|
||||
{
|
||||
token = $0
|
||||
if (token == "{") {
|
||||
depth++
|
||||
is_key[depth] = 1
|
||||
array_idx[depth] = ""
|
||||
} else if (token == "}") {
|
||||
delete path[depth]
|
||||
delete array_idx[depth]
|
||||
depth--
|
||||
} else if (token == "[") {
|
||||
depth++
|
||||
is_key[depth] = 0
|
||||
array_idx[depth] = 0
|
||||
} else if (token == "]") {
|
||||
delete array_idx[depth]
|
||||
delete path[depth]
|
||||
depth--
|
||||
} else if (token == ":") {
|
||||
is_key[depth] = 0
|
||||
} else if (token == ",") {
|
||||
if (array_idx[depth] != "") {
|
||||
array_idx[depth]++
|
||||
} else {
|
||||
is_key[depth] = 1
|
||||
}
|
||||
} else {
|
||||
if (is_key[depth] == 1) {
|
||||
# Remove quotes from the key
|
||||
gsub(/^"|"$/, "", token)
|
||||
path[depth] = token
|
||||
} else {
|
||||
# It is a value
|
||||
p = ""
|
||||
for (i=1; i<=depth; i++) {
|
||||
if (array_idx[i] != "") {
|
||||
p = p "[" array_idx[i] "]"
|
||||
} else if (path[i] != "") {
|
||||
p = p "." path[i]
|
||||
}
|
||||
}
|
||||
# Remove leading dot
|
||||
sub(/^\./, "", p)
|
||||
print p "=" token
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
@@ -11,6 +11,7 @@ if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
# Assumes common.sh is in the same directory as platform.sh
|
||||
# We resolve the directory of the current script
|
||||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
. "$_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
@@ -86,18 +87,7 @@ pkg_install() {
|
||||
if ! pkg_check "$pkg"; then
|
||||
to_install+=("$pkg")
|
||||
fi
|
||||
|
||||
# Reference counting logic
|
||||
if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then
|
||||
local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg"
|
||||
if ! grep -q "^${BOOTSTRAP_CURRENT_TOOL}$" "$ref_file" 2>/dev/null; then
|
||||
echo "$BOOTSTRAP_CURRENT_TOOL" >> "$ref_file"
|
||||
# Register rollback command
|
||||
if type add_rollback_cmd >/dev/null 2>&1; then
|
||||
add_rollback_cmd "pkg_remove $pkg"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
if [ ${#to_install[@]} -eq 0 ]; then
|
||||
@@ -169,21 +159,15 @@ pkg_remove() {
|
||||
|
||||
local to_remove=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then
|
||||
local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg"
|
||||
if [ -f "$ref_file" ]; then
|
||||
# Remove this tool from the reference file
|
||||
sed -i "/^${BOOTSTRAP_CURRENT_TOOL}$/d" "$ref_file"
|
||||
if [ -s "$ref_file" ]; then
|
||||
log_info "Skipping removal of '$pkg'; it is required by other tools."
|
||||
continue
|
||||
else
|
||||
rm -f "$ref_file"
|
||||
fi
|
||||
fi
|
||||
local is_installed=0
|
||||
if pkg_check "$pkg"; then
|
||||
is_installed=1
|
||||
fi
|
||||
|
||||
|
||||
to_remove+=("$pkg")
|
||||
if [ "$is_installed" -eq 1 ]; then
|
||||
to_remove+=("$pkg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#to_remove[@]} -eq 0 ]; then
|
||||
|
||||
@@ -1,50 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -f "$BOOTSTRAP_DIR/lib/json.sh" ]; then
|
||||
. "$BOOTSTRAP_DIR/lib/json.sh"
|
||||
fi
|
||||
|
||||
# Parses a plugin manifest using the generic json parser and outputs bash array assignments
|
||||
# Parses a plugin manifest using jq and outputs bash array assignments
|
||||
parse_plugin_manifest() {
|
||||
# The generic parser outputs lines like:
|
||||
# plugins.myplugin.version="1.0"
|
||||
# plugins.myplugin.url="https://..."
|
||||
# We want to extract myplugin and the keys to build:
|
||||
# PLUGIN_VERSIONS["myplugin"]="1.0"
|
||||
# PLUGIN_URLS["myplugin"]="https://..."
|
||||
|
||||
parse_json | awk -F'=' '
|
||||
{
|
||||
path = $1
|
||||
val = $2
|
||||
|
||||
# Remove quotes around value for bash array assignment
|
||||
gsub(/^"|"$/, "", val)
|
||||
|
||||
# Match paths starting with "plugins."
|
||||
if (match(path, /^plugins\./)) {
|
||||
rest = substr(path, RLENGTH + 1)
|
||||
# Find the last dot to separate plugin name from the property key
|
||||
last_dot = 0
|
||||
for (i=length(rest); i>0; i--) {
|
||||
if (substr(rest, i, 1) == ".") {
|
||||
last_dot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (last_dot > 0) {
|
||||
plugin_name = substr(rest, 1, last_dot - 1)
|
||||
prop = substr(rest, last_dot + 1)
|
||||
if (prop == "version") {
|
||||
print "PLUGIN_VERSIONS[\"" plugin_name "\"]=\"" val "\""
|
||||
} else if (prop == "url") {
|
||||
print "PLUGIN_URLS[\"" plugin_name "\"]=\"" val "\""
|
||||
} else if (prop == "bootstrap") {
|
||||
print "PLUGIN_BOOTSTRAP_VERSIONS[\"" plugin_name "\"]=\"" val "\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
jq -r '
|
||||
.plugins | to_entries[] |
|
||||
(if .value.version then "PLUGIN_VERSIONS[\"" + .key + "\"]=\"" + .value.version + "\"" else empty end),
|
||||
(if .value.url then "PLUGIN_URLS[\"" + .key + "\"]=\"" + .value.url + "\"" else empty end),
|
||||
(if .value.bootstrap then "PLUGIN_BOOTSTRAP_VERSIONS[\"" + .key + "\"]=\"" + .value.bootstrap + "\"" else empty end)
|
||||
'
|
||||
}
|
||||
|
||||
# Ensures that the plugin sources file exists, initializing it with the official repository by default
|
||||
@@ -94,7 +57,7 @@ EOF
|
||||
|
||||
for temp_file in "${temp_manifests[@]}"; do
|
||||
if [ -s "$temp_file" ]; then
|
||||
cat "$temp_file" | parse_plugin_manifest >> "$cache_file"
|
||||
parse_plugin_manifest < "$temp_file" >> "$cache_file"
|
||||
fi
|
||||
rm -f "$temp_file"
|
||||
done
|
||||
@@ -168,7 +131,7 @@ run_plugin() {
|
||||
if [ -n "$compat_ver" ]; then
|
||||
local current_ver="0.0.0"
|
||||
if [ -f "$BOOTSTRAP_DIR/VERSION" ]; then
|
||||
current_ver=$(cat "$BOOTSTRAP_DIR/VERSION" | tr -d '[:space:]')
|
||||
current_ver=$(tr -d '[:space:]' < "$BOOTSTRAP_DIR/VERSION")
|
||||
fi
|
||||
if version_lt "$compat_ver" "$current_ver"; then
|
||||
log_warn "Plugin '$plugin_name' is only tested up to bootstrap version $compat_ver (current: $current_ver). It may be incompatible."
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# shellcheck shell=bash
|
||||
# shellcheck disable=SC2034
|
||||
# This file is auto-generated by scripts/generate_registry.sh. Do not edit manually.
|
||||
|
||||
declare -A INSTALLERS=(
|
||||
@@ -5,6 +7,8 @@ declare -A INSTALLERS=(
|
||||
[asciicinema]="asciinema terminal recorder"
|
||||
[bat]="Bat (alternative to cat) and configure alias"
|
||||
[docker]="Container runtime and orchestration platform"
|
||||
[hyperfine]="Command-line benchmarking tool"
|
||||
[lazygit]="Simple terminal UI for git commands"
|
||||
[node]="Node.js (LTS) and NVM"
|
||||
[nvim]="Neovim 0.12.0 and configuration"
|
||||
[pnpm]="pnpm package manager"
|
||||
@@ -21,6 +25,8 @@ declare -A INSTALLER_DISPLAYS=(
|
||||
[asciicinema]="asciicinema"
|
||||
[bat]="Bat"
|
||||
[docker]="Docker"
|
||||
[hyperfine]="Hyperfine"
|
||||
[lazygit]="lazygit"
|
||||
[node]="Node"
|
||||
[nvim]="Neovim"
|
||||
[pnpm]="Pnpm"
|
||||
@@ -32,4 +38,22 @@ declare -A INSTALLER_DISPLAYS=(
|
||||
[zoxide]="Zoxide"
|
||||
)
|
||||
|
||||
INSTALLER_KEYS=(agy asciicinema bat docker node nvim pnpm rust starship uv yay yazi zoxide)
|
||||
declare -A INSTALLER_STRATEGIES=(
|
||||
[agy]="binary"
|
||||
[asciicinema]="binary"
|
||||
[bat]="binary"
|
||||
[docker]="system"
|
||||
[hyperfine]="binary"
|
||||
[lazygit]="binary"
|
||||
[node]="managed"
|
||||
[nvim]="binary"
|
||||
[pnpm]="binary"
|
||||
[rust]="managed"
|
||||
[starship]="binary"
|
||||
[uv]="binary"
|
||||
[yay]="system"
|
||||
[yazi]="binary"
|
||||
[zoxide]="managed"
|
||||
)
|
||||
|
||||
INSTALLER_KEYS=(agy asciicinema bat docker hyperfine lazygit node nvim pnpm rust starship uv yay yazi zoxide)
|
||||
|
||||
133
lib/registry_helpers.sh
Normal file
133
lib/registry_helpers.sh
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# Registry management helpers for Bootstrap
|
||||
|
||||
# Ensures the registry file exists
|
||||
ensure_registry() {
|
||||
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
|
||||
if [ ! -f "$registry_file" ]; then
|
||||
mkdir -p "$(dirname "$registry_file")"
|
||||
echo '{"tools": {}}' > "$registry_file"
|
||||
fi
|
||||
echo "$registry_file"
|
||||
}
|
||||
|
||||
# Safely applies a jq filter to the registry using a file lock
|
||||
registry_set() {
|
||||
local jq_filter="$1"
|
||||
shift
|
||||
local registry_file
|
||||
registry_file=$(ensure_registry)
|
||||
local lock_file="${registry_file}.lock"
|
||||
|
||||
(
|
||||
flock -x 200
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
# Apply jq filter with any additional arguments passed in
|
||||
jq "$@" "$jq_filter" "$registry_file" > "$temp_file" && mv "$temp_file" "$registry_file"
|
||||
) 200>"$lock_file"
|
||||
}
|
||||
|
||||
# Usage: register_tool <tool_name> <strategy> [version] [source]
|
||||
register_tool() {
|
||||
local tool="$1"
|
||||
local strategy="$2"
|
||||
local version="${3:-}"
|
||||
local source="${4:-}"
|
||||
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
local bindir="$BOOTSTRAP_BIN"
|
||||
|
||||
local filter='if .tools == null then .tools = {} else . end |
|
||||
.tools[$tool].strategy = $strategy |
|
||||
.tools[$tool].installed_at = $timestamp |
|
||||
(if $version != "" then .tools[$tool].version = $version else . end) |
|
||||
(if $source != "" then .tools[$tool].source = $source else . end) |
|
||||
(if $strategy == "binary" then .tools[$tool].bin = ($bindir + "/" + $tool) else . end)'
|
||||
|
||||
registry_set "$filter" \
|
||||
--arg tool "$tool" \
|
||||
--arg strategy "$strategy" \
|
||||
--arg version "$version" \
|
||||
--arg source "$source" \
|
||||
--arg timestamp "$timestamp" \
|
||||
--arg bindir "$bindir"
|
||||
}
|
||||
|
||||
# Usage: registry_add_sys_deps <tool_name> <dep1> <dep2>...
|
||||
registry_add_sys_deps() {
|
||||
local tool="$1"
|
||||
shift
|
||||
if [ $# -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local deps_json
|
||||
deps_json=$(printf '%s\n' "$@" | jq -R . | jq -s .)
|
||||
|
||||
local filter='if .tools == null then .tools = {} else . end |
|
||||
.tools[$tool].system_dependencies = ((.tools[$tool].system_dependencies // []) + $deps | unique)'
|
||||
|
||||
registry_set "$filter" --arg tool "$tool" --argjson deps "$deps_json"
|
||||
}
|
||||
|
||||
# Usage: registry_remove_tool <tool_name>
|
||||
registry_remove_tool() {
|
||||
local tool="$1"
|
||||
registry_set 'del(.tools[$tool])' --arg tool "$tool"
|
||||
}
|
||||
|
||||
# Usage: registry_get_sys_deps <tool_name>
|
||||
registry_get_sys_deps() {
|
||||
local tool="$1"
|
||||
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
|
||||
if [ -f "$registry_file" ]; then
|
||||
jq -r --arg tool "$tool" '.tools[$tool].system_dependencies[]? // empty' "$registry_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage: registry_check <tool_name>
|
||||
# Validates that a tool is actually installed according to its strategy
|
||||
registry_check() {
|
||||
local tool="$1"
|
||||
local registry_file
|
||||
registry_file=$(ensure_registry)
|
||||
|
||||
local strategy
|
||||
strategy=$(jq -r --arg tool "$tool" '.tools[$tool].strategy // empty' "$registry_file")
|
||||
|
||||
if [ -z "$strategy" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$strategy" = "binary" ]; then
|
||||
local bin_path
|
||||
bin_path=$(jq -r --arg tool "$tool" '.tools[$tool].bin // empty' "$registry_file")
|
||||
if [ -n "$bin_path" ] && [ -x "$bin_path" ]; then
|
||||
return 0
|
||||
fi
|
||||
elif [ "$strategy" = "managed" ]; then
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
elif [ "$strategy" = "system" ]; then
|
||||
local deps=()
|
||||
while IFS= read -r dep; do
|
||||
[ -n "$dep" ] && deps+=("$dep")
|
||||
done < <(registry_get_sys_deps "$tool")
|
||||
|
||||
if [ ${#deps[@]} -eq 0 ]; then
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
if pkg_check "${deps[@]}"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
export -f ensure_registry registry_set register_tool registry_add_sys_deps registry_remove_tool registry_get_sys_deps registry_check
|
||||
@@ -8,11 +8,11 @@ _LIB_ROLLBACK_SOURCED=1
|
||||
BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap"
|
||||
BOOTSTRAP_HISTORY_LOG="$BOOTSTRAP_STATE_DIR/history.log"
|
||||
BOOTSTRAP_UNINSTALLERS_DIR="$BOOTSTRAP_STATE_DIR/uninstallers"
|
||||
BOOTSTRAP_PACKAGES_DIR="$BOOTSTRAP_STATE_DIR/packages"
|
||||
|
||||
|
||||
init_rollback_system() {
|
||||
mkdir -p "$BOOTSTRAP_UNINSTALLERS_DIR"
|
||||
mkdir -p "$BOOTSTRAP_PACKAGES_DIR"
|
||||
|
||||
touch "$BOOTSTRAP_HISTORY_LOG"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ track_dir() {
|
||||
|
||||
create_savepoint() {
|
||||
local name="$1"
|
||||
|
||||
# Prevent savepoints from having the same name as a tool
|
||||
if [ -n "${INSTALLERS[$name]:-}" ]; then
|
||||
log_error "Cannot create savepoint named '$name' because it conflicts with a tool name."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "SAVEPOINT: $name" >> "$BOOTSTRAP_HISTORY_LOG"
|
||||
log_success "Savepoint '$name' created."
|
||||
}
|
||||
@@ -84,6 +91,41 @@ execute_rollback() {
|
||||
log_success "Rollback of '$tool' complete."
|
||||
}
|
||||
|
||||
|
||||
uninstall_tool() {
|
||||
local tool="$1"
|
||||
|
||||
# 1. Execute the rollback manifest to remove files/dirs/env/aliases
|
||||
execute_rollback "$tool"
|
||||
|
||||
# 2. Reference counting and cleanup of system dependencies
|
||||
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
|
||||
if [ -f "$registry_file" ] && jq -e --arg tool "$tool" '.tools | has($tool)' "$registry_file" >/dev/null; then
|
||||
while IFS= read -r dep; do
|
||||
[ -z "$dep" ] && continue
|
||||
local other_users
|
||||
other_users=$(jq -r --arg tool "$tool" --arg dep "$dep" '
|
||||
.tools | to_entries | map(select(.key != $tool and (.value.system_dependencies | type == "array") and (.value.system_dependencies | index($dep)))) | length
|
||||
' "$registry_file")
|
||||
|
||||
if [ "$other_users" -eq 0 ]; then
|
||||
log_info "System dependency '$dep' is no longer required by any registered tool. Removing..."
|
||||
pkg_remove "$dep"
|
||||
else
|
||||
log_info "Keeping system dependency '$dep' (required by other tools)"
|
||||
fi
|
||||
done < <(registry_get_sys_deps "$tool")
|
||||
|
||||
# Remove from registry
|
||||
registry_remove_tool "$tool"
|
||||
fi
|
||||
|
||||
# 3. Remove the tool from history.log
|
||||
if [ -f "$BOOTSTRAP_HISTORY_LOG" ]; then
|
||||
sed -i "/^INSTALL: ${tool}$/d" "$BOOTSTRAP_HISTORY_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
rollback_bare() {
|
||||
if [ ! -s "$BOOTSTRAP_HISTORY_LOG" ]; then
|
||||
log_info "No history available to rollback."
|
||||
@@ -95,9 +137,7 @@ rollback_bare() {
|
||||
|
||||
if [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
execute_rollback "$tool"
|
||||
# Remove the last line efficiently
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
uninstall_tool "$tool"
|
||||
elif [[ "$last_line" == SAVEPOINT:* ]]; then
|
||||
local sp="${last_line#SAVEPOINT: }"
|
||||
log_warn "Last action was savepoint '$sp'. Cannot bare-rollback a savepoint."
|
||||
@@ -122,8 +162,7 @@ rollback_to_savepoint() {
|
||||
break
|
||||
elif [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
execute_rollback "$tool"
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
uninstall_tool "$tool"
|
||||
elif [[ "$last_line" == SAVEPOINT:* ]]; then
|
||||
local sp="${last_line#SAVEPOINT: }"
|
||||
log_info "Removing intermediate savepoint '$sp'..."
|
||||
@@ -135,4 +174,4 @@ rollback_to_savepoint() {
|
||||
done
|
||||
}
|
||||
|
||||
export -f init_rollback_system setup_uninstaller_context add_rollback_cmd track_file track_dir create_savepoint mark_install_success execute_rollback rollback_bare rollback_to_savepoint
|
||||
export -f init_rollback_system setup_uninstaller_context add_rollback_cmd track_file track_dir create_savepoint mark_install_success execute_rollback uninstall_tool rollback_bare rollback_to_savepoint
|
||||
|
||||
@@ -216,6 +216,14 @@ for script in "${SCRIPTS[@]}"; do
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
me)
|
||||
run_plugin "auth" "me" "$@"
|
||||
exit $?
|
||||
;;
|
||||
trust)
|
||||
run_plugin "auth" "trust" "$@"
|
||||
exit $?
|
||||
;;
|
||||
con)
|
||||
if [ -f "$BOOTSTRAP_DIR/commands/con.sh" ]; then
|
||||
. "$BOOTSTRAP_DIR/commands/con.sh" "$@"
|
||||
@@ -262,7 +270,7 @@ for script in "${SCRIPTS[@]}"; do
|
||||
fi
|
||||
;;
|
||||
fall)
|
||||
local savepoint_name="${1:-}"
|
||||
savepoint_name="${1:-}"
|
||||
if [ -z "$savepoint_name" ]; then
|
||||
log_error "Usage: b fall <savepoint_name>"
|
||||
exit 1
|
||||
@@ -271,11 +279,25 @@ for script in "${SCRIPTS[@]}"; do
|
||||
exit 0
|
||||
;;
|
||||
rb)
|
||||
local target="${1:-}"
|
||||
target="${1:-}"
|
||||
if [ -z "$target" ]; then
|
||||
rollback_bare
|
||||
else
|
||||
rollback_to_savepoint "$target"
|
||||
# Split comma-separated targets
|
||||
IFS=',' read -ra TARGETS <<< "$target"
|
||||
|
||||
if [ ${#TARGETS[@]} -gt 1 ]; then
|
||||
for t in "${TARGETS[@]}"; do
|
||||
uninstall_tool "$t"
|
||||
done
|
||||
else
|
||||
registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
|
||||
if [ -f "$registry_file" ] && jq -e --arg t "$target" '.tools | has($t)' "$registry_file" >/dev/null; then
|
||||
uninstall_tool "$target"
|
||||
else
|
||||
rollback_to_savepoint "$target"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
|
||||
@@ -9,6 +9,7 @@ _LIB_SHELL_CONFIG_SOURCED=1
|
||||
# Source common utilities if not already loaded
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
. "$_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
@@ -158,6 +159,34 @@ remove_alias_snippet() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Write completion snippet to completions.d/
|
||||
# Usage: write_completion_snippet <name> <content>
|
||||
write_completion_snippet() {
|
||||
local name="$1"
|
||||
local content="$2"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
|
||||
|
||||
mkdir -p "$dir"
|
||||
log_info "Writing completion 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 completion snippet from completions.d/
|
||||
# Usage: remove_completion_snippet <name>
|
||||
remove_completion_snippet() {
|
||||
local name="$1"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
|
||||
|
||||
if [ -f "$dir/${name}.sh" ]; then
|
||||
log_info "Removing completion 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
|
||||
@@ -170,14 +199,11 @@ create_fd_symlink() {
|
||||
source_bashrc() {
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
log_info "Re-sourcing ~/.bashrc..."
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.bashrc"
|
||||
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 source_bashrc
|
||||
|
||||
|
||||
|
||||
|
||||
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 write_completion_snippet remove_completion_snippet source_bashrc
|
||||
|
||||
12
plugins.json
12
plugins.json
@@ -1,21 +1,27 @@
|
||||
{
|
||||
"plugins": {
|
||||
"auth": {
|
||||
"version": "1.0.0",
|
||||
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/auth.sh",
|
||||
"bootstrap": "2.2.0",
|
||||
"description": "Client Authentication and Provisioning Plugin"
|
||||
},
|
||||
"weather": {
|
||||
"version": "1.0.0",
|
||||
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/weather.sh",
|
||||
"bootstrap": "2.1.0",
|
||||
"bootstrap": "2.2.0",
|
||||
"description": "Show weather forecast for your location"
|
||||
},
|
||||
"sysinfo": {
|
||||
"version": "1.0.0",
|
||||
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/sysinfo.sh",
|
||||
"bootstrap": "2.1.0",
|
||||
"bootstrap": "2.2.0",
|
||||
"description": "Show system information and hardware statistics"
|
||||
},
|
||||
"todo": {
|
||||
"version": "1.0.0",
|
||||
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/todo.sh",
|
||||
"bootstrap": "2.1.0",
|
||||
"bootstrap": "2.2.0",
|
||||
"description": "A simple command-line todo list manager"
|
||||
}
|
||||
}
|
||||
|
||||
242
plugins/auth.sh
Normal file
242
plugins/auth.sh
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Authentication & Provisioning Plugin for Bootstrap CLI
|
||||
# Handles requester (b me) and approver (b trust) flows.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure dependencies are met
|
||||
pkg_install "arch:openssh|debian:openssh-client|fedora:openssh-clients" "curl" "jq" "age"
|
||||
|
||||
|
||||
# Ensure public key exists next to private key for ssh-keygen -Y sign
|
||||
ensure_pubkey_exists() {
|
||||
local priv_key="$1"
|
||||
local pub_key="${priv_key}.pub"
|
||||
if [ ! -f "$pub_key" ]; then
|
||||
ssh-keygen -y -f "$priv_key" > "$pub_key"
|
||||
fi
|
||||
}
|
||||
|
||||
COMMAND="${1:-}"
|
||||
if [ -z "$COMMAND" ]; then
|
||||
echo "Usage: b auth <me|trust> [args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
|
||||
# Defaults
|
||||
SERVER_URL="https://b.adityagupta.dev/auth"
|
||||
KEY_DIR="$HOME/.config/bootstrap-client"
|
||||
POLL_INTERVAL=5
|
||||
ADMIN_KEY="$HOME/.ssh/id_ed25519"
|
||||
USER_CODE=""
|
||||
|
||||
if [ "$COMMAND" = "trust" ]; then
|
||||
if [ $# -lt 1 ]; then
|
||||
log_error "user_code is required for trust."
|
||||
echo "Usage: b trust <user_code> [--server <server_url>] [--admin-key <path>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
USER_CODE="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Parse remaining arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server)
|
||||
SERVER_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--key-dir)
|
||||
KEY_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
POLL_INTERVAL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--admin-key)
|
||||
ADMIN_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$COMMAND" = "me" ]; then
|
||||
mkdir -p "$KEY_DIR"
|
||||
local_key="$KEY_DIR/id_ed25519"
|
||||
|
||||
if [ ! -f "$local_key" ]; then
|
||||
log_info "Generating local Ed25519 key pair under $KEY_DIR..."
|
||||
ssh-keygen -t ed25519 -N "" -f "$local_key" >/dev/null
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$local_key"
|
||||
pub_key=$(cat "${local_key}.pub")
|
||||
hostname=$(hostname 2>/dev/null || uname -n)
|
||||
os=$(uname -s 2>/dev/null || echo "linux")
|
||||
|
||||
# Safely construct JSON payload
|
||||
json_payload=$(jq -n \
|
||||
--arg hn "$hostname" \
|
||||
--arg os "$os" \
|
||||
--arg pk "$pub_key" \
|
||||
'{hostname: $hn, os: $os, public_key: $pk}')
|
||||
|
||||
log_info "Registering device with $SERVER_URL..."
|
||||
|
||||
register_response=$(curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$json_payload" \
|
||||
"$SERVER_URL/api/register")
|
||||
|
||||
user_code=$(echo "$register_response" | jq -r '.user_code // empty')
|
||||
challenge_nonce=$(echo "$register_response" | jq -r '.challenge_nonce // empty')
|
||||
|
||||
if [ -z "$user_code" ] || [ -z "$challenge_nonce" ]; then
|
||||
log_error "Failed to retrieve registration codes from server response."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
log_success "Device registration initiated successfully!"
|
||||
echo "Please authorize this device on your administrator machine using:"
|
||||
echo " b trust $user_code --server $SERVER_URL"
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Verification Code: $user_code"
|
||||
echo "--------------------------------------------------------"
|
||||
log_info "Waiting for administrator approval (polling every ${POLL_INTERVAL}s)..."
|
||||
|
||||
# Prepare challenge poll file signing
|
||||
temp_nonce_file=$(mktemp)
|
||||
temp_sig_file="${temp_nonce_file}.sig"
|
||||
echo -n "$challenge_nonce" > "$temp_nonce_file"
|
||||
|
||||
# Ensure cleanup of temp files
|
||||
cleanup() {
|
||||
rm -f "$temp_nonce_file" "$temp_sig_file"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
rm -f "$temp_sig_file"
|
||||
|
||||
# Sign challenge nonce
|
||||
if ! ssh-keygen -Y sign -f "$local_key" -n "bootstrap" "$temp_nonce_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing of challenge nonce failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get raw base64 from armored signature file
|
||||
signature_b64=$(grep -v '^-' "$temp_sig_file" | tr -d '\n')
|
||||
|
||||
poll_payload=$(jq -n \
|
||||
--arg uc "$user_code" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, signature: $sig}')
|
||||
|
||||
poll_out=$(mktemp)
|
||||
http_code=$(curl -s -o "$poll_out" -w "%{http_code}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$poll_payload" \
|
||||
"$SERVER_URL/api/challenge/poll")
|
||||
|
||||
poll_body=$(cat "$poll_out")
|
||||
rm -f "$poll_out"
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
enc_secrets=$(echo "$poll_body" | jq -r '.encrypted_secrets // empty')
|
||||
if [ -n "$enc_secrets" ] && [ "$enc_secrets" != "null" ]; then
|
||||
log_success "Device approved by administrator! Decrypting secrets payload..."
|
||||
|
||||
decrypted_file="$KEY_DIR/secrets.decrypted"
|
||||
if echo "$enc_secrets" | base64 -d | age --decrypt -i "$local_key" > "$decrypted_file" 2>/dev/null; then
|
||||
log_success "Secrets successfully provisioned and written to: $decrypted_file"
|
||||
cat "$decrypted_file"
|
||||
break
|
||||
else
|
||||
log_error "Decryption using age failed. Please ensure the private key has not been altered."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
|
||||
elif [ "$COMMAND" = "trust" ]; then
|
||||
if [ ! -f "$ADMIN_KEY" ]; then
|
||||
log_error "Admin private key not found at: $ADMIN_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$ADMIN_KEY"
|
||||
|
||||
log_info "Fetching pending device details for user code: $USER_CODE"
|
||||
pending_response=$(curl -fsSL "$SERVER_URL/api/pending/$USER_CODE")
|
||||
|
||||
requester_pub_key=$(echo "$pending_response" | jq -r '.public_key // empty')
|
||||
if [ -z "$requester_pub_key" ]; then
|
||||
log_error "No pending registration found for code '$USER_CODE'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Pending Device Public Key:"
|
||||
echo "$requester_pub_key"
|
||||
echo "--------------------------------------------------------"
|
||||
|
||||
# Prompt for confirmation (read from tty to support pipeline scenarios)
|
||||
read -r -p "Do you trust and approve this device? [y/N]: " confirm_choice </dev/tty || confirm_choice="N"
|
||||
if [[ ! "$confirm_choice" =~ ^[Yy]$ ]]; then
|
||||
log_warn "Approval aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate signature of the requester's public key
|
||||
temp_pubkey_file=$(mktemp)
|
||||
temp_pubkey_sig_file="${temp_pubkey_file}.sig"
|
||||
echo -n "$requester_pub_key" > "$temp_pubkey_file"
|
||||
|
||||
# Cleanup trap
|
||||
cleanup_trust() {
|
||||
rm -f "$temp_pubkey_file" "$temp_pubkey_sig_file"
|
||||
}
|
||||
trap cleanup_trust EXIT INT TERM
|
||||
|
||||
if ! ssh-keygen -Y sign -f "$ADMIN_KEY" -n "bootstrap" "$temp_pubkey_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing using administrator key failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
signature_b64=$(grep -v '^-' "$temp_pubkey_sig_file" | tr -d '\n')
|
||||
|
||||
# Get fingerprint
|
||||
admin_pubkey_str=$(ssh-keygen -y -f "$ADMIN_KEY")
|
||||
temp_admin_pub=$(mktemp)
|
||||
echo "$admin_pubkey_str" > "$temp_admin_pub"
|
||||
approver_fingerprint=$(ssh-keygen -lf "$temp_admin_pub" | awk '{print $2}')
|
||||
rm -f "$temp_admin_pub"
|
||||
|
||||
# Prepare payload
|
||||
approve_payload=$(jq -n \
|
||||
--arg uc "$USER_CODE" \
|
||||
--arg fp "$approver_fingerprint" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, approver_public_key_fingerprint: $fp, signature: $sig}')
|
||||
|
||||
log_info "Submitting cryptographic approval to server..."
|
||||
curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$approve_payload" \
|
||||
"$SERVER_URL/api/approve"
|
||||
|
||||
log_success "Device with code $USER_CODE has been approved."
|
||||
fi
|
||||
@@ -98,7 +98,7 @@ main() {
|
||||
log_success "Removed task #$task_num."
|
||||
;;
|
||||
clear)
|
||||
> "$TODO_FILE"
|
||||
true > "$TODO_FILE"
|
||||
log_success "Cleared all tasks from your todo list."
|
||||
;;
|
||||
--help|-h)
|
||||
|
||||
51
readme.md
51
readme.md
@@ -79,7 +79,7 @@ It automatically fuzzy-finds the folder in case there is no exact match. Also, i
|
||||
|
||||
### 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.
|
||||
Bootstrap CLI features a robust rollback and uninstallation system driven by procedural manifests and a centralized JSON registry.
|
||||
|
||||
To safely uninstall the very last tool you installed (including wiping its shell paths and aliases):
|
||||
|
||||
@@ -87,11 +87,21 @@ To safely uninstall the very last tool you installed (including wiping its shell
|
||||
b rb
|
||||
```
|
||||
|
||||
To uninstall specific tools by name (supporting single or comma-separated lists of tools):
|
||||
|
||||
```bash
|
||||
b rb nvim
|
||||
b rb bat,yazi,zoxide
|
||||
```
|
||||
|
||||
Shared system package dependencies are reference-counted in the registry and will be automatically cleaned up only when the last tool depending on them is removed.
|
||||
|
||||
To create a named savepoint before experimenting with your setup:
|
||||
|
||||
```bash
|
||||
b fall pre_dev_setup
|
||||
```
|
||||
*(Note: Savepoint names cannot conflict with the names of any available tools).*
|
||||
|
||||
To completely roll back all installations made after that savepoint, restoring your system back to that exact state:
|
||||
|
||||
@@ -109,6 +119,30 @@ b up
|
||||
b up --force
|
||||
```
|
||||
|
||||
### Client Authentication and Provisioning (`b me` and `b trust`)
|
||||
|
||||
Bootstrap CLI supports a secure, cryptographic client onboarding and secrets provisioning flow. This allows you to securely register new requester devices and authorize them via an administrator device.
|
||||
|
||||
For the complete protocol specification, including the underlying REST API and cryptographic details, see the [Client Authentication and Provisioning Specification](docs/client_spec_auth.md).
|
||||
|
||||
#### 1. Device Registration (`b me`)
|
||||
To initiate registration on a new, unprovisioned machine, run:
|
||||
|
||||
```bash
|
||||
b me [--server <server_url>] [--key-dir <dir>] [--poll-interval <seconds>]
|
||||
```
|
||||
|
||||
This generates a local SSH Ed25519 key pair, registers your device in a pending state with the authentication server, displays a short `user_code`, and starts polling for administrator approval. Once approved, the encrypted payload containing secrets is retrieved, decrypted locally via `age`, and saved to `<key-dir>/secrets.decrypted`.
|
||||
|
||||
#### 2. Request Approval (`b trust`)
|
||||
To authorize a pending device, run this command on an already provisioned administrator machine:
|
||||
|
||||
```bash
|
||||
b trust <user_code> [--server <server_url>] [--admin-key <path_to_admin_private_key>]
|
||||
```
|
||||
|
||||
This retrieves the pending device's public key, prompts the administrator for confirmation, cryptographically signs the public key using the admin key, and submits the approval signature back to the authentication server.
|
||||
|
||||
## Plugins (`b <plugin_name>`)
|
||||
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike installers (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a subshell**.
|
||||
@@ -177,6 +211,20 @@ b gone -f
|
||||
|
||||
Then reload your shell configuration or run `unset -f b` to clear the function definition from your current terminal session.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
Bootstrap isolates all of its components and installed software inside XDG-compliant directories. You can override these variables in your shell environment, but they default to the following:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| `BOOTSTRAP_DIR` | `~/.config/bootstrap` | Core configuration, libraries, shell snippets (`env.d`, `aliases.d`, `completions.d`), and local installers |
|
||||
| `BOOTSTRAP_DATA_DIR` | `~/.local/share/bootstrap` | Active installation bin files, optional components, and runtime installations |
|
||||
| `BOOTSTRAP_STATE_DIR` | `~/.local/state/bootstrap` | Stateful registry, logs, and rollback/uninstall manifests |
|
||||
| `BOOTSTRAP_CACHE_DIR` | `~/.cache/bootstrap` | Downloaded files cache and temporary workspace files |
|
||||
| `BOOTSTRAP_BIN` | `$BOOTSTRAP_DATA_DIR/bin` | Target directory for all installed tools' binaries |
|
||||
| `BOOTSTRAP_OPT` | `$BOOTSTRAP_DATA_DIR/opt` | Destination for complex, multi-file software distributions |
|
||||
| `BOOTSTRAP_RUNTIMES` | `$BOOTSTRAP_DATA_DIR/runtimes` | Workspace for programming language runtime managers (e.g. NVM) |
|
||||
|
||||
## Development
|
||||
|
||||
If you are developing this tool locally:
|
||||
@@ -201,7 +249,6 @@ The scripts are intentionally straightforward Bash scripts that can be inspected
|
||||
|
||||
Potential additions include:
|
||||
|
||||
* RSA based authentication
|
||||
* Development environment bootstrap
|
||||
* Workstation setup
|
||||
* Server provisioning
|
||||
|
||||
@@ -13,6 +13,7 @@ echo "==> Generating registry.sh..."
|
||||
# Temporary arrays
|
||||
declare -A tools_desc
|
||||
declare -A tools_disp
|
||||
declare -A tools_strat
|
||||
keys=()
|
||||
|
||||
for f in "$INSTALLERS_DIR"/install_*.sh; do
|
||||
@@ -20,35 +21,46 @@ for f in "$INSTALLERS_DIR"/install_*.sh; do
|
||||
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
|
||||
disp_name=$(grep -E "^# DisplayName:" "$f" | head -n1 | sed -E 's/^# DisplayName:\s*//I')
|
||||
desc=$(grep -E "^# Description:" "$f" | head -n1 | sed -E 's/^# Description:\s*//I' | sed -E 's/^Install\s+//I')
|
||||
strat=$(grep -E "^# Strategy:" "$f" | head -n1 | sed -E 's/^# Strategy:\s*//I')
|
||||
|
||||
if [ -n "$tool" ]; then
|
||||
tools_desc["$tool"]="$desc"
|
||||
tools_disp["$tool"]="${disp_name:-$tool}"
|
||||
tools_strat["$tool"]="${strat:-unknown}"
|
||||
keys+=("$tool")
|
||||
fi
|
||||
done
|
||||
|
||||
# Sort keys alphabetically
|
||||
sorted_keys=($(printf '%s\n' "${keys[@]}" | sort))
|
||||
mapfile -t sorted_keys < <(printf '%s\n' "${keys[@]}" | sort)
|
||||
|
||||
{
|
||||
echo "# shellcheck shell=bash"
|
||||
echo "# shellcheck disable=SC2034"
|
||||
echo "# This file is auto-generated by scripts/generate_registry.sh. Do not edit manually."
|
||||
echo ""
|
||||
echo "declare -A INSTALLERS=("
|
||||
for k in "${sorted_keys[@]}"; do
|
||||
# Escape any double quotes in description
|
||||
escaped_desc=$(echo "${tools_desc[$k]}" | sed 's/"/\\"/g')
|
||||
escaped_desc="${tools_desc[$k]//\"/\\\"}"
|
||||
echo " [$k]=\"$escaped_desc\""
|
||||
done
|
||||
echo ")"
|
||||
echo ""
|
||||
echo "declare -A INSTALLER_DISPLAYS=("
|
||||
for k in "${sorted_keys[@]}"; do
|
||||
escaped_disp=$(echo "${tools_disp[$k]}" | sed 's/"/\\"/g')
|
||||
escaped_disp="${tools_disp[$k]//\"/\\\"}"
|
||||
echo " [$k]=\"$escaped_disp\""
|
||||
done
|
||||
echo ")"
|
||||
echo ""
|
||||
echo "declare -A INSTALLER_STRATEGIES=("
|
||||
for k in "${sorted_keys[@]}"; do
|
||||
escaped_strat="${tools_strat[$k]//\"/\\\"}"
|
||||
echo " [$k]=\"$escaped_strat\""
|
||||
done
|
||||
echo ")"
|
||||
echo ""
|
||||
# Format keys output as space-separated list in array declaration format
|
||||
echo "INSTALLER_KEYS=(${sorted_keys[*]})"
|
||||
} > "$REGISTRY_FILE"
|
||||
|
||||
Reference in New Issue
Block a user