mirror of
https://github.com/sortedcord/bootstrap.git
synced 2026-07-22 20:22:50 +05:30
Compare commits
29 Commits
feat/auth-
...
2fa8f6695f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fa8f6695f | |||
| f2c9a4b104 | |||
| 6afaf372f2 | |||
| 506083b85a | |||
| 6e63e54f1e | |||
| 5ba08f3d20 | |||
| eefd792b6c | |||
| 4a03e42b9d | |||
| f6df53ae31 | |||
| 7d4e23e309 | |||
| 190f337f12 | |||
| 6e0f951623 | |||
| f9a25ff37e | |||
| 03e9c20e54 | |||
| 2ddd28d4d4 | |||
| 83c524441c | |||
| cee345e3f0 | |||
| 0c16640593 | |||
| d5c90d6e85 | |||
| 4c1c7de0b7 | |||
| 29de051b7d | |||
| a4e5bc1175 | |||
| 36c7be07b3 | |||
| 0eaea2c997 | |||
| 4eec27570e | |||
| f5a266ff70 | |||
| c42687a710 | |||
| 7f3ff45f05 | |||
| 780e79364f |
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: add_installer
|
||||
description: Add a new installer script to the bootstrap CLI project. Use this skill whenever the user asks to create a new installer, add a new tool/package to bootstrap, or register a new `b <name>` command.
|
||||
name: add_tool
|
||||
description: Add a new tool definition script to the bootstrap CLI project. Use this skill whenever the user asks to create a new tool, add a new tool/package to bootstrap, or register a new `b <name>` command.
|
||||
---
|
||||
|
||||
# Add a New Installer to Bootstrap CLI
|
||||
# Add a New Tool to Bootstrap CLI
|
||||
|
||||
This skill provides everything needed to add a new installer to the bootstrap project without reading the entire codebase.
|
||||
This skill provides everything needed to add a new tool to the bootstrap project without reading the entire codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -15,15 +15,15 @@ Bootstrap CLI (`b`) is a bash-based tool installer and system bootstrapper. User
|
||||
|
||||
```
|
||||
bootstrap/
|
||||
├── installers/ # Individual installer scripts (install_<name>.sh)
|
||||
├── lib/ # Shared libraries and router sourced by all installers
|
||||
├── tools/ # Individual tool directories (tools/<name>/tool.sh)
|
||||
├── lib/ # Shared libraries and router sourced by all tools
|
||||
│ ├── 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, write_completion_snippet
|
||||
│ ├── registry.sh # Dynamically generated installer registry
|
||||
│ ├── registry.sh # Dynamically generated tool registry
|
||||
│ └── routes.sh # Central router script
|
||||
├── commands/ # Non-installer commands (help, con, uninstall)
|
||||
├── commands/ # Non-tool commands (help, con, uninstall)
|
||||
├── assets/ # Assets (logo art, etc.)
|
||||
│ └── pixel_art.ansi
|
||||
├── bootstrap.sh # Metascript for environment setup + library loading
|
||||
@@ -33,73 +33,71 @@ bootstrap/
|
||||
|
||||
## Step-by-Step Checklist
|
||||
|
||||
When adding a new installer named `<name>`:
|
||||
When adding a new tool named `<name>`:
|
||||
|
||||
### Step 1: Analyze user request & gather details
|
||||
|
||||
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:
|
||||
When the user asks you to add a tool, 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 tool script:
|
||||
|
||||
**If the user provides a `curl` command or link to an install script:**
|
||||
- Execute the `curl` command (or use `read_url_content`) to fetch the script and analyze what it actually does under the hood.
|
||||
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), and unnecessary shell configuration logic.
|
||||
- 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.
|
||||
|
||||
**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).
|
||||
- Write the `install_<name>` function to download, extract, and copy only those essential files. (Use `download_file` and temporary directories, see "GitHub Download and Extraction" below).
|
||||
|
||||
### Step 2: Create the installer script
|
||||
### Step 2: Create the tool script
|
||||
|
||||
### Step 3: Add metadata comments to the top of your installer script
|
||||
Create `tools/<name>/tool.sh` using the template below.
|
||||
|
||||
At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following three metadata headers:
|
||||
### Step 3: Add metadata comments to the top of your tool script
|
||||
|
||||
At the top of your new tool 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.
|
||||
The central router `lib/routes.sh` and autocomplete function in `b.sh` will dynamically parse this metadata from all `tools/*/tool.sh` scripts to register the tool and keys automatically! No manual edits to `lib/routes.sh` or `b.sh` are required.
|
||||
|
||||
### 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`, `write_alias_snippet`, and `write_completion_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 5: Verify (optional)
|
||||
|
||||
Verify that the installer works and appears in the help output:
|
||||
Verify that the tool works and appears in the help output:
|
||||
- Run `b all` to confirm it appears in the help list.
|
||||
- Run `b ware <name> -y` to test direct installation.
|
||||
- Run `b ware <name>` to test the interactive editing flow.
|
||||
|
||||
---
|
||||
|
||||
## Installer Script Template
|
||||
## Tool Script Template
|
||||
|
||||
Every installer follows this exact boilerplate structure. Copy this and fill in the tool-specific logic:
|
||||
Every tool 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
|
||||
|
||||
@@ -119,14 +117,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) ─────────────────────────────────
|
||||
@@ -156,7 +154,7 @@ main "$@"
|
||||
|
||||
## Available Library Functions
|
||||
|
||||
These are pre-loaded by `bootstrap.sh` — no need to source them manually in installers.
|
||||
These are pre-loaded by `bootstrap.sh` — no need to source them manually in tool scripts.
|
||||
|
||||
### From `lib/common.sh`
|
||||
|
||||
@@ -169,7 +167,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`
|
||||
|
||||
@@ -177,7 +175,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`
|
||||
@@ -186,7 +184,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`
|
||||
|
||||
@@ -196,6 +194,14 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
| `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>`. |
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
@@ -211,18 +217,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
|
||||
@@ -230,31 +232,30 @@ 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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules & Conventions
|
||||
|
||||
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
|
||||
1. **File naming**: Always `tools/<name>/tool.sh`.
|
||||
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`.
|
||||
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`, 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.
|
||||
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.
|
||||
@@ -20,11 +20,11 @@ When the user asks to "cut a release", "bump the version", or "tag a new version
|
||||
git log $(git describe --tags --abbrev=0)..HEAD --oneline
|
||||
```
|
||||
2. **Determine the bump level** using semantic versioning rules:
|
||||
- Skip any commits that only touch `installers/` or docs — those don't warrant a bump.
|
||||
- Skip any commits that only touch `tools/` or docs — those don't warrant a bump.
|
||||
- If any commit has `BREAKING CHANGE` or `!:` → **major**
|
||||
- If any core-CLI commit has `feat:` → **minor**
|
||||
- Otherwise (only `fix:`, `refactor:`, etc. in core-CLI) → **patch**
|
||||
- If *all* commits are installer-only or docs-only → inform the user no release is needed.
|
||||
- If *all* commits are tool-only or docs-only → inform the user no release is needed.
|
||||
3. **Formulate a verbose, structured description of the changes** based on the analyzed commits. Group the changes into logical sections (such as "Breaking Changes & Major Features:" and "Other Updates:") and list the corresponding commit messages or summaries. Example format:
|
||||
```text
|
||||
Breaking Changes & Major Features:
|
||||
|
||||
16
b.sh
16
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
|
||||
@@ -90,8 +93,8 @@ _b_completion() {
|
||||
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
local installer_keys=""
|
||||
if [ -d "$routes_dir/installers" ]; then
|
||||
for f in "$routes_dir/installers"/install_*.sh; do
|
||||
if [ -d "$routes_dir/tools" ]; then
|
||||
for f in "$routes_dir/tools"/*/tool.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
local tool
|
||||
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
|
||||
@@ -123,6 +126,7 @@ _b_completion() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2207
|
||||
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
@@ -131,8 +135,8 @@ _b_completion() {
|
||||
if [ "$COMP_CWORD" -eq 2 ] && { [ "$prev" = "ware" ] || [ "$prev" = "bware" ]; }; then
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
local installer_keys=""
|
||||
if [ -d "$routes_dir/installers" ]; then
|
||||
for f in "$routes_dir/installers"/install_*.sh; do
|
||||
if [ -d "$routes_dir/tools" ]; then
|
||||
for f in "$routes_dir/tools"/*/tool.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
local tool
|
||||
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
|
||||
@@ -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,11 +65,28 @@ 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=(
|
||||
"VERSION"
|
||||
@@ -83,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"
|
||||
@@ -91,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
|
||||
@@ -100,10 +120,10 @@ install_bootstrap() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Also copy installers if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/installers" ]; then
|
||||
mkdir -p "$routes_dir/installers"
|
||||
cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/"
|
||||
# Also copy tools if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/tools" ]; then
|
||||
mkdir -p "$routes_dir/tools"
|
||||
cp -r "$_SCRIPT_DIR/tools/"* "$routes_dir/tools/"
|
||||
fi
|
||||
|
||||
# Also copy plugins if they exist locally
|
||||
@@ -146,6 +166,13 @@ install_bootstrap() {
|
||||
|
||||
# >>> bootstrap-cli setup >>>
|
||||
export BOOTSTRAP_DIR="$HOME/.config/bootstrap"
|
||||
export BOOTSTRAP_DATA_DIR="$HOME/.local/share/bootstrap"
|
||||
export BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap"
|
||||
export BOOTSTRAP_CACHE_DIR="$HOME/.cache/bootstrap"
|
||||
export BOOTSTRAP_BIN="$BOOTSTRAP_DATA_DIR/bin"
|
||||
export BOOTSTRAP_OPT="$BOOTSTRAP_DATA_DIR/opt"
|
||||
export BOOTSTRAP_RUNTIMES="$BOOTSTRAP_DATA_DIR/runtimes"
|
||||
|
||||
[ -f "$BOOTSTRAP_DIR/b.sh" ] && . "$BOOTSTRAP_DIR/b.sh"
|
||||
for f in "$BOOTSTRAP_DIR/env.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
for f in "$BOOTSTRAP_DIR/aliases.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
@@ -249,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
|
||||
@@ -316,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
|
||||
|
||||
@@ -328,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,12 +1,13 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: help
|
||||
# Lists all available bootstrap commands and installers
|
||||
# Lists all available bootstrap commands and tools
|
||||
|
||||
echo "Available bootstrap commands:"
|
||||
# Non-installers first (aligned to 6 chars width)
|
||||
# Non-tools first (aligned to 6 chars width)
|
||||
printf " %-6s - %s\n" "all" "List all available commands"
|
||||
printf " %-6s - %s\n" "con" "Edit config (e.g. b con nvim)"
|
||||
printf " %-6s - %s\n" "up" "Check for updates and update Bootstrap CLI"
|
||||
printf " %-6s - %s\n" "ware" "Edit and run an installer (e.g. b ware nvim)"
|
||||
printf " %-6s - %s\n" "ware" "Edit and run a tool (e.g. b ware nvim)"
|
||||
printf " %-6s - %s\n" "gone" "Uninstall Bootstrap CLI helper"
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,162 +1,165 @@
|
||||
# Client Authentication and Provisioning Specification
|
||||
# Client Onboarding & Secrets Provisioning
|
||||
|
||||
This document defines the interface and protocol flow for the client application interacting with the bootstrap authentication server. It serves as a guide for implementing any client, particularly the dual mode bash script client.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
The bootstrapping process establishes trust between a new client device, an existing administrator device, and the authentication server. The authentication server uses Ed25519 public key cryptography for authentication and `age` for secure payload encryption.
|
||||
|
||||
### Dual Mode Client
|
||||
|
||||
The client application operates in one of two modes:
|
||||
1. **Requester Mode**: Used by a new, unprovisioned device to request access and receive secrets.
|
||||
2. **Approver Mode**: Used by an already provisioned administrator device to authorize pending requests.
|
||||
This wiki page describes the design, cryptography, and protocol flow of the **Bootstrap Client Onboarding and Secrets Provisioning System**. It explains how a new client device establishes secure trust with an authentication server through an existing administrator device.
|
||||
|
||||
---
|
||||
|
||||
## The Authentication and Provisioning Flow
|
||||
## Overview & Architecture
|
||||
|
||||
The complete flow consists of five sequential phases.
|
||||
The onboarding process leverages public-key cryptography to securely distribute configurations or secrets (encrypted using `age`) to newly registered nodes.
|
||||
|
||||
### Phase 1: Administrator Bootstrapping
|
||||
1. The server starts up. If the `ADMIN_PUBLIC_KEY` environment variable is set, the server automatically registers and approves this public key as an administrator device in the database.
|
||||
2. The administrator client on the admin machine is configured to use the corresponding private key.
|
||||
The trust model consists of three entities:
|
||||
1. **Requester Node (New Client)**: An unprovisioned machine requesting configuration secrets. It generates a local Ed25519 key pair for identification.
|
||||
2. **Approver Node (Administrator)**: A trusted administrator machine that already possesses an approved Ed25519 private key.
|
||||
3. **Authentication Server**: The central coordinator that manages pending requests, validates administrator signatures, and serves `age`-encrypted payloads. The backend server codebase is hosted in the [bootstrap-auth-server](https://github.com/sortedcord/bootstrap-auth-server) repository.
|
||||
|
||||
### Phase 2: Client Request Initiation (Requester Mode)
|
||||
1. The new device runs the client in requester mode:
|
||||
```bash
|
||||
b me (optional: --server <server_url>) [default auth server is https://b.adityagupta.dev/auth]
|
||||
```
|
||||
2. The client script generates an Ed25519 key pair locally.
|
||||
3. The client sends a `POST /api/register` request containing its generated public key.
|
||||
4. The server registers the device in a `pending` state and returns:
|
||||
- A short, human readable `user_code` (e.g. 4 to 8 characters).
|
||||
- A unique `device_id`.
|
||||
5. The client script displays the `user_code` to the operator and begins polling the challenge endpoint:
|
||||
```
|
||||
GET /api/challenge/poll?device_id=<device_id>
|
||||
```
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor Admin
|
||||
participant Requester as Requester Client (b me)
|
||||
participant Server as Auth Server
|
||||
participant Approver as Approver Client (b trust)
|
||||
|
||||
### Phase 3: Administrator Approval (Approver Mode)
|
||||
1. The operator reads the `user_code` from the requesting device's terminal.
|
||||
2. On the administrator device, the operator runs the client in approver mode:
|
||||
```bash
|
||||
b trust <user_code> [--server <server_url>]
|
||||
```
|
||||
3. The administrator client queries the server to retrieve the pending registration details:
|
||||
```
|
||||
GET /api/pending/<user_code>
|
||||
```
|
||||
4. The server returns the pending `device_id` and the requester's `public_key`.
|
||||
5. The administrator client displays these details. The operator confirms the request.
|
||||
6. The administrator client signs a payload containing the requester's `device_id` and the approval action using its Ed25519 administrator private key.
|
||||
7. The administrator client submits the signature and its public key to the server:
|
||||
```
|
||||
POST /api/approve
|
||||
```
|
||||
8. The server verifies the administrator's signature against the registered administrator public keys. If valid, the server transitions the requester device state from `pending` to `approved`.
|
||||
|
||||
### Phase 4: Payload Provisioning
|
||||
1. Once the requester device is approved, the server generates the secret payload (or retrieves it from a secure source).
|
||||
2. The server encrypts the payload using the requester's public key via `age`.
|
||||
3. The server stores this encrypted payload as a challenge response.
|
||||
|
||||
### Phase 5: Challenge Completion and Retrieval
|
||||
1. The next poll from the requester client to `GET /api/challenge/poll?device_id=<device_id>` succeeds.
|
||||
2. The server returns the `age` encrypted payload.
|
||||
3. The requester client decrypts the payload using its local private key.
|
||||
4. The secrets are successfully provisioned.
|
||||
Requester->>Server: POST /api/register (Host, OS, Public Key)
|
||||
Server-->>Requester: 200 OK (user_code, challenge_nonce)
|
||||
Note over Requester: Displays user_code & begins polling...
|
||||
|
||||
Admin->>Approver: Exec: b trust <user_code>
|
||||
Approver->>Server: GET /api/pending/<user_code>
|
||||
Server-->>Approver: 200 OK (Requester Public Key)
|
||||
|
||||
Note over Approver: Admin confirms public key fingerprint
|
||||
Approver->>Server: POST /api/approve (user_code, Admin Signature & Fingerprint)
|
||||
Note over Server: Server verifies signature against admin list
|
||||
|
||||
Requester->>Server: POST /api/challenge/poll (user_code, Signature of Nonce)
|
||||
Server-->>Requester: 200 OK (Encrypted Secrets Payload)
|
||||
|
||||
Note over Requester: Decrypts payload using local Ed25519 private key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints Reference
|
||||
## Cryptographic Mechanics
|
||||
|
||||
### 1. Register Device
|
||||
- **Endpoint**: `POST /api/register`
|
||||
- **Request Body**:
|
||||
To maintain security without complex dependencies, the system uses native Unix tools:
|
||||
|
||||
* **Authentication (Ed25519)**: SSH Ed25519 key pairs generated by `ssh-keygen` are used for signing. Payloads are signed using `ssh-keygen -Y sign` with a fixed namespace of `bootstrap`.
|
||||
* **Encryption (`age`)**: Payloads are encrypted specifically to the requester's SSH Ed25519 public key. The client decrypts the payload using its private key:
|
||||
```bash
|
||||
age -d -i ~/.config/bootstrap-client/id_ed25519
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Protocol Flow
|
||||
|
||||
### Phase 1: Administrator Registration
|
||||
The server registers administrator public keys at startup (e.g., via the `ADMIN_PUBLIC_KEY` environment variable). The administrator client on the admin machine must be configured to use the corresponding private key.
|
||||
|
||||
### Phase 2: Client Request (`b me`)
|
||||
1. The requester executes `b me`.
|
||||
2. It generates an Ed25519 key pair (`id_ed25519`/`id_ed25519.pub`) in `~/.config/bootstrap-client/`.
|
||||
3. It sends a registration request to the server containing its hostname, OS, and public key.
|
||||
4. The server registers the device as `pending`, generating a short, human-readable `user_code` and a `challenge_nonce`.
|
||||
5. The requester displays the `user_code` and begins polling `/api/challenge/poll`.
|
||||
|
||||
### Phase 3: Administrator Approval (`b trust`)
|
||||
1. The operator reads the `user_code` from the requester's terminal and enters it on the administrator device: `b trust <user_code>`.
|
||||
2. The administrator client fetches the pending public key from `/api/pending/<user_code>`.
|
||||
3. The operator validates the public key fingerprint.
|
||||
4. The administrator client signs the requester's public key using `ssh-keygen -Y sign` and submits the signature along with its fingerprint to `/api/approve`.
|
||||
5. The server validates the signature. If valid, the client state transitions to `approved`.
|
||||
|
||||
### Phase 4: Secrets Retrieval & Decryption
|
||||
1. During its next poll, the requester client signs the `challenge_nonce` and submits it to `/api/challenge/poll`.
|
||||
2. The server verifies the signature. Since the client is now approved, it returns the secrets payload (which was encrypted with the requester's public key using `age`).
|
||||
3. The requester client base64-decodes the payload, decrypts it using its local SSH private key, and saves it to `~/.config/bootstrap-client/secrets.decrypted`.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Register Device
|
||||
* **Endpoint**: `POST /api/register`
|
||||
* **Content-Type**: `application/json`
|
||||
* **Request**:
|
||||
```json
|
||||
{
|
||||
"hostname": "my-client-hostname",
|
||||
"os": "Linux",
|
||||
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..."
|
||||
}
|
||||
```
|
||||
* **Response (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"user_code": "G4J2N3",
|
||||
"challenge_nonce": "a6f8b9...",
|
||||
"expires_in": 300
|
||||
}
|
||||
```
|
||||
|
||||
### Get Pending Details
|
||||
* **Endpoint**: `GET /api/pending/<user_code>`
|
||||
* **Response (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..."
|
||||
}
|
||||
```
|
||||
- **Response Body (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"device_id": "uuid-string",
|
||||
"user_code": "A3F9K2"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Get Pending Device Details
|
||||
- **Endpoint**: `GET /api/pending/<user_code>`
|
||||
- **Response Body (200 OK)**:
|
||||
### Approve Device
|
||||
* **Endpoint**: `POST /api/approve`
|
||||
* **Content-Type**: `application/json`
|
||||
* **Request**:
|
||||
```json
|
||||
{
|
||||
"device_id": "uuid-string",
|
||||
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..."
|
||||
"user_code": "G4J2N3",
|
||||
"approver_public_key_fingerprint": "SHA256:...",
|
||||
"signature": "base64-encoded-sshsig-blob"
|
||||
}
|
||||
```
|
||||
> [!NOTE]
|
||||
> The `signature` is the base64-encoded binary content of the SSHSIG file (i.e. the raw base64 string inside the armor, excluding the `-----BEGIN/END SSH SIGNATURE-----` lines).
|
||||
|
||||
### 3. Approve Device
|
||||
- **Endpoint**: `POST /api/approve`
|
||||
- **Request Body**:
|
||||
### Poll Challenge
|
||||
* **Endpoint**: `POST /api/challenge/poll`
|
||||
* **Content-Type**: `application/json`
|
||||
* **Request**:
|
||||
```json
|
||||
{
|
||||
"device_id": "uuid-string",
|
||||
"admin_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...",
|
||||
"signature": "hex-encoded-signature-bytes"
|
||||
"user_code": "G4J2N3",
|
||||
"signature": "base64-encoded-sshsig-blob-of-challenge-nonce"
|
||||
}
|
||||
```
|
||||
- **Response Body (200 OK)**: Empty or success confirmation.
|
||||
|
||||
### 4. Poll Challenge
|
||||
- **Endpoint**: `GET /api/challenge/poll?device_id=<device_id>`
|
||||
- **Response Body (200 OK - Pending)**:
|
||||
* **Response (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"status": "pending"
|
||||
}
|
||||
```
|
||||
- **Response Body (200 OK - Approved & Encrypted)**:
|
||||
```json
|
||||
{
|
||||
"status": "approved",
|
||||
"payload": "-----BEGIN AGE ENCRYPTED FILE-----\n..."
|
||||
"encrypted_secrets": "base64-encoded-age-encrypted-payload"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bash Client Commands and Usage
|
||||
## Client Usage Guide
|
||||
|
||||
### Requirements
|
||||
Ensure `ssh-keygen`, `curl`, `jq`, and `age` are installed. (The Bootstrap `auth` plugin resolves these automatically on start).
|
||||
|
||||
### Global Dependencies
|
||||
- `curl` or `wget` for making HTTP requests.
|
||||
- `jq` for parsing JSON payloads.
|
||||
- `ssh-keygen` for Ed25519 key generation and signing.
|
||||
- `age` and `age-keygen` for decrypting the final payload.
|
||||
|
||||
### Command Structure
|
||||
|
||||
#### 1. Device Registration Request
|
||||
Generates local keys, registers with the server, and polls for the encrypted secret payload.
|
||||
### 1. Requesting Access (`b me`)
|
||||
Run the following on the client machine to initiate registration:
|
||||
```bash
|
||||
b me \
|
||||
--server <server_url> \
|
||||
[--key-dir <directory_to_save_keys>] \
|
||||
[--poll-interval <seconds>]
|
||||
b me [--server <server_url>] [--key-dir <dir>] [--poll-interval <seconds>]
|
||||
```
|
||||
- `--server`: Base URL of the bootstrap authentication server.
|
||||
- `--key-dir`: Directory where the new Ed25519 and age keys will be saved (defaults to `~/.config/bootstrap-client/`).
|
||||
- `--poll-interval`: Frequency of polling in seconds (defaults to `5`).
|
||||
* Default `--server`: `https://b.adityagupta.dev/auth`
|
||||
* Default `--key-dir`: `~/.config/bootstrap-client`
|
||||
* Default `--poll-interval`: `5`
|
||||
|
||||
#### 2. Request Approval
|
||||
Retrieves a pending request by its user code, requests user confirmation, signs the approval payload, and sends it to the server.
|
||||
### 2. Approving Access (`b trust`)
|
||||
Run the following on the administrator machine to approve a pending request:
|
||||
```bash
|
||||
b trust <user_code> \
|
||||
--server <server_url> \
|
||||
--admin-key <path_to_admin_private_key>
|
||||
b trust <user_code> [--server <server_url>] [--admin-key <path_to_admin_private_key>]
|
||||
```
|
||||
- `--code`: The short human readable code displayed on the requesting device.
|
||||
- `--server`: Base URL of the bootstrap authentication server.
|
||||
- `--admin-key`: Path to the administrator's private key used to sign the approval.
|
||||
* Default `--admin-key`: `~/.ssh/id_ed25519`
|
||||
|
||||
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"
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# Plugin Development Guide
|
||||
|
||||
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 sandboxed subshell**.
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike tools (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 sandboxed subshell**.
|
||||
|
||||
This means downloading and invoking a plugin makes no system modifications other than caching the `.sh` file itself. They are fetched only the very first time you invoke them.
|
||||
|
||||
|
||||
@@ -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,22 +35,24 @@ 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
|
||||
|
||||
### A. Context Initialization
|
||||
Before executing an installer script, the `b` CLI initializes the command list:
|
||||
Before executing a tool script, the `b` CLI initializes the command list:
|
||||
```bash
|
||||
export BOOTSTRAP_UNINSTALLER_CMDS="$HOME/.local/state/bootstrap/uninstallers/nvim.cmds"
|
||||
mkdir -p "$(dirname "$BOOTSTRAP_UNINSTALLER_CMDS")"
|
||||
@@ -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.
|
||||
@@ -97,21 +97,23 @@ log_success "Rollback complete."
|
||||
```
|
||||
|
||||
## 6. Resilience Against User Modifications
|
||||
Because `b ware <tool>` allows users to modify installation scripts:
|
||||
Because `b ware <tool>` allows users to modify tool scripts:
|
||||
1. **Dynamic Adaptation:** The manifest is built *during* execution, adapting to whatever packages the user manually added.
|
||||
2. **Fault Isolation:** The `eval` loop ensures that a syntax error in one custom rollback step doesn't crash the removal of other tracked packages.
|
||||
|
||||
## 7. Handling Shared Dependencies
|
||||
The `pkg_remove` helper utilizes reference counting via simple text files (e.g., `~/.local/state/bootstrap/packages/curl`).
|
||||
- **On `pkg_install`**: Append tool name.
|
||||
- **On `pkg_remove`**: Remove tool name. If empty, proceed with system uninstallation.
|
||||
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
|
||||
|
||||
To handle failures during installation (e.g., network drops, script errors, or user cancellation via `Ctrl+C`), the CLI incorporates a transactional approach that balances **automatic rollback** and **resumability**:
|
||||
|
||||
### A. The Interruption Trap & Prompt
|
||||
When running an installer, the central router (`lib/routes.sh`) traps `SIGINT` and `SIGTERM` signals. If the installation fails or is interrupted:
|
||||
When running a tool, the central router (`lib/routes.sh`) traps `SIGINT` and `SIGTERM` signals. If the installation fails or is interrupted:
|
||||
1. The trap catches the event and stops execution.
|
||||
2. The user is prompted interactively:
|
||||
- **Rollback (r)**: Invokes `execute_rollback <tool>` immediately to clean up all partial modifications.
|
||||
@@ -128,6 +130,6 @@ If the user chooses to **keep** the partial state and runs `b <tool>` again:
|
||||
To make rerunning an interrupted script fast and efficient, installers use `download_file <url> <dest>` instead of raw `curl`:
|
||||
1. It downloads the payload to a central cache directory: `~/.local/state/bootstrap/cache/`.
|
||||
2. It uses `curl -C -` to continue the download from the byte offset where it was interrupted.
|
||||
3. Once completed, it copies the cached file to the installer's temp directory.
|
||||
3. Once completed, it copies the cached file to the tool's temp directory.
|
||||
4. Distro package manager commands (`pkg_install`) and shell snippets (`write_env_snippet`) are naturally idempotent, allowing the script to breeze through already completed steps in milliseconds and resume exactly where the heavy work failed.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ The Bootstrap CLI uses a semantic, git-tag-driven versioning workflow.
|
||||
|
||||
- **Do not manually edit the `VERSION` file.**
|
||||
- Develop normally using [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat: Add new tool`, `fix: Typo in script`).
|
||||
- **Note on Installers:** Adding or modifying installers in the `installers/` directory **does not** require a version bump. They are fetched dynamically from the registry when a user runs `b ware <tool>`.
|
||||
- **Note on Tools:** Adding or modifying tools in the `tools/` directory **does not** require a version bump. They are fetched dynamically from the registry when a user runs `b ware <tool>`.
|
||||
|
||||
## 2. When to Cut a Release
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
#
|
||||
# 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)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
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"
|
||||
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
|
||||
}
|
||||
|
||||
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'"
|
||||
}
|
||||
|
||||
main() {
|
||||
install_bat
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Bat installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: starship
|
||||
# DisplayName: Starship
|
||||
# Description: Install Starship shell prompt
|
||||
#
|
||||
# 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)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_starship() {
|
||||
if has_command starship || [ -f "$HOME/.local/bin/starship" ]; then
|
||||
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)
|
||||
local arch=""
|
||||
case "$raw_arch" in
|
||||
x86_64) arch="x86_64" ;;
|
||||
arm64) arch="aarch64" ;;
|
||||
*) log_error "Unsupported Linux architecture: $raw_arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
local target="${arch}-unknown-linux-musl"
|
||||
|
||||
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="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}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
download_file "$download_url" "$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"
|
||||
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"
|
||||
}
|
||||
|
||||
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)"'
|
||||
}
|
||||
|
||||
main() {
|
||||
install_starship
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Starship installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: yazi
|
||||
# DisplayName: Yazi
|
||||
# Description: Install Yazi terminal file manager and dependencies
|
||||
#
|
||||
# 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)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
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'
|
||||
# Shell wrapper for yazi to change directory on exit
|
||||
y() {
|
||||
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")"
|
||||
yazi "$@" --cwd-file="$tmp"
|
||||
if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
|
||||
builtin cd -- "$cwd"
|
||||
fi
|
||||
rm -f -- "$tmp"
|
||||
}
|
||||
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."
|
||||
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
|
||||
}
|
||||
|
||||
main() {
|
||||
install_yazi
|
||||
add_y_wrapper
|
||||
echo
|
||||
log_success "Yazi installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -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 tools
|
||||
|
||||
# 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=(
|
||||
@@ -36,4 +38,22 @@ declare -A INSTALLER_DISPLAYS=(
|
||||
[zoxide]="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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Central routing script for bootstrap installers.
|
||||
# Central routing script for bootstrap tools.
|
||||
# This file is updated automatically by the 'b' command.
|
||||
|
||||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
|
||||
@@ -69,8 +69,8 @@ run_ware() {
|
||||
display_name="$(echo "${tool:0:1}" | tr '[:lower:]' '[:upper:]')${tool:1}"
|
||||
fi
|
||||
|
||||
# Check for local installer first
|
||||
local local_installer="$BOOTSTRAP_DIR/installers/install_${tool}.sh"
|
||||
# Check for local tool definition first
|
||||
local local_installer="$BOOTSTRAP_DIR/tools/${tool}/tool.sh"
|
||||
|
||||
if [ "$bypass_edit" = "true" ] && [ -f "$local_installer" ]; then
|
||||
log_info "Running ${display_name} installer..."
|
||||
@@ -86,7 +86,7 @@ run_ware() {
|
||||
else
|
||||
BOOTSTRAP_BASE_URL="${BOOTSTRAP_BASE_URL:-https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master}"
|
||||
BOOTSTRAP_FALLBACK_URL="${BOOTSTRAP_FALLBACK_URL:-https://raw.githubusercontent.com/sortedcord/bootstrap/refs/heads/master}"
|
||||
local installer_path="installers/install_${tool}.sh"
|
||||
local installer_path="tools/${tool}/tool.sh"
|
||||
local download_status=0
|
||||
|
||||
log_info "Downloading ${display_name} installer..."
|
||||
@@ -270,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
|
||||
@@ -279,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
|
||||
|
||||
@@ -198,6 +199,7 @@ create_fd_symlink() {
|
||||
source_bashrc() {
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
log_info "Re-sourcing ~/.bashrc..."
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.bashrc"
|
||||
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)
|
||||
|
||||
78
readme.md
78
readme.md
@@ -32,7 +32,7 @@ More scripts will be added over time.
|
||||
To bootstrap a new machine and set up the `b` command tool, run the following:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://adityagupta.dev/b | bash
|
||||
curl -fsSL https://b.adityagupta.dev | bash
|
||||
```
|
||||
|
||||
Once bootstrapped, you list all commands available -
|
||||
@@ -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,31 +119,42 @@ b up
|
||||
b up --force
|
||||
```
|
||||
|
||||
## Client Onboarding & Secrets Provisioning
|
||||
|
||||
Bootstrap CLI features a secure, cryptographic client onboarding and secrets provisioning flow. Driven by the lazy-loaded `auth` plugin, it allows you to register new requester devices and authorize them via a trusted administrator machine using SSH Ed25519 key signing and `age` encryption.
|
||||
|
||||
The backend authentication and verification service is implemented in the [bootstrap-auth-server](https://github.com/sortedcord/bootstrap-auth-server) repository. For detailed client specifications, protocols, REST API endpoint specs, and cryptographic steps, refer to the [Client Onboarding & Secrets Provisioning Wiki](docs/client_spec_auth.md).
|
||||
|
||||
### 1. Device Registration (`b me`)
|
||||
|
||||
To register a new, unprovisioned machine:
|
||||
```bash
|
||||
b me [--server <server_url>] [--key-dir <dir>] [--poll-interval <seconds>]
|
||||
```
|
||||
This generates a local SSH Ed25519 key pair, registers the device in a pending state, displays a short `user_code`, and polls the server. Once approved, it retrieves the secrets payload, decrypts it locally using `age`, and saves it to `<key-dir>/secrets.decrypted`.
|
||||
|
||||
### 2. Request Approval (`b trust`)
|
||||
|
||||
To authorize a pending client from an administrator device:
|
||||
```bash
|
||||
b trust <user_code> [--server <server_url>] [--admin-key <path_to_admin_private_key>]
|
||||
```
|
||||
This retrieves the client's public key, prompts the administrator for confirmation, signs the key using `ssh-keygen -Y sign` (under the `bootstrap` namespace), and submits the approval signature back to the 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**.
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike tools (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**.
|
||||
|
||||
Downloading and invoking a plugin makes no system modifications other than caching the `.sh` file itself. They are fetched only the very first time you invoke them.
|
||||
|
||||
### Official Plugins
|
||||
|
||||
Bootstrap comes pre-configured with a set of official plugins ready to use out-of-the-box:
|
||||
* **`auth`**: Handles client onboarding and secrets provisioning, exposing the `b me` and `b trust` CLI commands. See the [Client Onboarding & Secrets Provisioning](#client-onboarding--secrets-provisioning) section for detailed usage.
|
||||
|
||||
* **`weather`**: Fetches and displays a neat weather forecast using `wttr.in`.
|
||||
```bash
|
||||
b weather
|
||||
b weather New York
|
||||
```
|
||||
* **`sysinfo`**: Displays a system resource dashboard showing CPU, memory, disk usage, and OS/kernel details.
|
||||
```bash
|
||||
b sysinfo
|
||||
```
|
||||
* **`todo`**: A command-line todo list manager. Tasks are persisted to a lightweight text file in your user directory.
|
||||
```bash
|
||||
b todo add "Write a new plugin"
|
||||
b todo list
|
||||
b todo done 1
|
||||
```
|
||||
|
||||
* **`weather`**: Fetches weather forecasts via `wttr.in` (Usage: `b weather [location]`).
|
||||
* **`sysinfo`**: Displays a system resource dashboard (Usage: `b sysinfo`).
|
||||
* **`todo`**: A command-line todo manager (Usage: `b todo <add "task"|list|done <id>>`).
|
||||
|
||||
### Adding Third-Party Plugins
|
||||
|
||||
@@ -177,6 +198,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 +236,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
|
||||
@@ -216,8 +250,8 @@ Running scripts directly from the internet is convenient but should always be ap
|
||||
### Why Bootstrap is More Secure
|
||||
Instead of blindly piping large, third-party installation scripts (often hundreds or thousands of lines of unscrutinized code) directly from the internet into your shell, Bootstrap uses a **scrutinized and simplified** approach:
|
||||
|
||||
1. **Audited & Minimised**: Every script in `installers/` has been scrutinized, refactored, and stripped of redundant compatibility layers (like Windows/macOS support or complex environment checks), leaving only the essential, readable logic required for your setup.
|
||||
2. **Controlled Execution**: Since installer scripts are hosted locally in your cloned repository, you can review every line of code before executing it. You are never subject to silent, upstream changes to installers.
|
||||
1. **Audited & Minimised**: Every script in `tools/` has been scrutinized, refactored, and stripped of redundant compatibility layers (like Windows/macOS support or complex environment checks), leaving only the essential, readable logic required for your setup.
|
||||
2. **Controlled Execution**: Since tool scripts are hosted locally in your cloned repository, you can review every line of code before executing it. You are never subject to silent, upstream changes to tools.
|
||||
3. **No Raw Pipes**: We download official binary releases directly to verified temporary locations rather than running arbitrary third-party script pipelines.
|
||||
|
||||
If you want to audit the core Bootstrap CLI itself before running it:
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
INSTALLERS_DIR="$REPO_DIR/installers"
|
||||
TOOLS_DIR="$REPO_DIR/tools"
|
||||
REGISTRY_FILE="$REPO_DIR/lib/registry.sh"
|
||||
|
||||
echo "==> Generating registry.sh..."
|
||||
@@ -13,42 +13,54 @@ 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
|
||||
for f in "$TOOLS_DIR"/*/tool.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
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"
|
||||
|
||||
@@ -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() {
|
||||
79
tools/bat/tool.sh
Normal file
79
tools/bat/tool.sh
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
# Strategy: binary
|
||||
#
|
||||
# Bat Installer Script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_bat() {
|
||||
if has_command bat; then
|
||||
if ! confirm "Bat is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Bat installation."
|
||||
return
|
||||
fi
|
||||
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() {
|
||||
|
||||
write_alias_snippet "bat" "alias cat='bat --paging=never -p'"
|
||||
}
|
||||
|
||||
main() {
|
||||
install_bat
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Bat installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
@@ -2,15 +2,8 @@
|
||||
# Tool: hyperfine
|
||||
# DisplayName: Hyperfine
|
||||
# Description: Command-line benchmarking tool
|
||||
# Strategy: binary
|
||||
#
|
||||
# Hyperfine 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
|
||||
|
||||
@@ -21,7 +14,7 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
install_hyperfine() {
|
||||
if has_command hyperfine || [ -f "$HOME/.local/bin/hyperfine" ]; then
|
||||
if has_command hyperfine; then
|
||||
if ! confirm "Hyperfine is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Hyperfine installation."
|
||||
return
|
||||
@@ -33,12 +26,6 @@ install_hyperfine() {
|
||||
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)
|
||||
@@ -51,21 +38,17 @@ install_hyperfine() {
|
||||
|
||||
log_info "Fetching latest Hyperfine version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/sharkdp/hyperfine/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
latest_tag=$(github_get_latest_release "sharkdp/hyperfine")
|
||||
|
||||
local download_url
|
||||
if [ -n "$latest_tag" ]; then
|
||||
log_info "Latest Hyperfine version found: $latest_tag"
|
||||
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
|
||||
else
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v1.20.0"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
|
||||
else
|
||||
log_info "Latest Hyperfine version found: $latest_tag"
|
||||
fi
|
||||
|
||||
log_info "Downloading Hyperfine from ${download_url}..."
|
||||
local archive="$TMP_DIR/hyperfine.tar.gz"
|
||||
download_file "$download_url" "$archive"
|
||||
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..."
|
||||
@@ -73,7 +56,7 @@ install_hyperfine() {
|
||||
|
||||
local extract_dir="$TMP_DIR/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu"
|
||||
if [ ! -d "$extract_dir" ]; then
|
||||
# Handle case where directory name might differ (e.g. without leading v in directory name or tag)
|
||||
# Handle case where directory name might differ
|
||||
extract_dir=$(find "$TMP_DIR" -maxdepth 1 -type d -name "hyperfine-*" | head -n1)
|
||||
fi
|
||||
|
||||
@@ -82,8 +65,8 @@ install_hyperfine() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install binary to ~/.local/bin
|
||||
local target_dir="$HOME/.local/bin"
|
||||
# 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"
|
||||
@@ -106,27 +89,15 @@ install_hyperfine() {
|
||||
comp_content=$(cat "$extract_dir/autocomplete/hyperfine.bash")
|
||||
write_completion_snippet "hyperfine" "$comp_content"
|
||||
fi
|
||||
}
|
||||
|
||||
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"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
register_tool "hyperfine" "binary" "$latest_tag" "github:sharkdp/hyperfine"
|
||||
}
|
||||
|
||||
main() {
|
||||
install_hyperfine
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Hyperfine installation and configuration complete."
|
||||
log_success "Hyperfine installation complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -2,16 +2,11 @@
|
||||
# Tool: lazygit
|
||||
# DisplayName: lazygit
|
||||
# Description: Simple terminal UI for git commands
|
||||
# Strategy: binary
|
||||
#
|
||||
# lazygit 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 ──────────────────────────────────────────────
|
||||
@@ -30,11 +25,7 @@ install_lazygit() {
|
||||
fi
|
||||
|
||||
local latest_tag=""
|
||||
if has_command curl; then
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/jesseduffield/lazygit/releases/latest \
|
||||
| grep '"tag_name":' | head -n1 \
|
||||
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
latest_tag=$(github_get_latest_release "jesseduffield/lazygit")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.62.2" # fallback
|
||||
@@ -43,14 +34,13 @@ install_lazygit() {
|
||||
|
||||
local version="${latest_tag#v}"
|
||||
|
||||
local arch=$(detect_arch)
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
local arch_str="x86_64"
|
||||
if [ "$arch" = "arm64" ]; then
|
||||
arch_str="arm64"
|
||||
fi
|
||||
|
||||
local url="https://github.com/jesseduffield/lazygit/releases/download/${latest_tag}/lazygit_${version}_linux_${arch_str}.tar.gz"
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
@@ -58,15 +48,16 @@ install_lazygit() {
|
||||
local dest="$TMP_DIR/lazygit.tar.gz"
|
||||
|
||||
log_info "Downloading lazygit ${latest_tag}..."
|
||||
download_file "$url" "$dest"
|
||||
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 "$HOME/.local/bin"
|
||||
mkdir -p "$BOOTSTRAP_BIN"
|
||||
cp "$TMP_DIR/lazygit" "$HOME/.local/bin/lazygit"
|
||||
chmod +x "$HOME/.local/bin/lazygit"
|
||||
track_file "$HOME/.local/bin/lazygit"
|
||||
register_tool "lazygit" "binary" "$latest_tag" "github:jesseduffield/lazygit"
|
||||
}
|
||||
|
||||
# ─── 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() {
|
||||
76
tools/starship/tool.sh
Normal file
76
tools/starship/tool.sh
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# Tool: starship
|
||||
# DisplayName: Starship
|
||||
# Description: Install Starship shell prompt
|
||||
# Strategy: binary
|
||||
#
|
||||
# Starship Installer Script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_starship() {
|
||||
if has_command starship || [ -f "$HOME/.local/bin/starship" ]; then
|
||||
log_info "Starship is already installed."
|
||||
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
|
||||
|
||||
local target="${arch}-unknown-linux-musl"
|
||||
|
||||
log_info "Fetching latest Starship version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "starship/starship")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="latest"
|
||||
fi
|
||||
|
||||
log_info "Downloading Starship ${latest_tag}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
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="$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() {
|
||||
|
||||
|
||||
write_env_snippet "starship" 'eval "$(starship init bash)"'
|
||||
}
|
||||
|
||||
main() {
|
||||
install_starship
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Starship installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
105
tools/yazi/tool.sh
Executable file
105
tools/yazi/tool.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: yazi
|
||||
# DisplayName: Yazi
|
||||
# Description: Install Yazi terminal file manager and dependencies
|
||||
# Strategy: binary
|
||||
#
|
||||
# Yazi Installer Script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
add_y_wrapper() {
|
||||
|
||||
local wrapper_content
|
||||
wrapper_content=$(cat << 'EOF'
|
||||
# Shell wrapper for yazi to change directory on exit
|
||||
y() {
|
||||
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")"
|
||||
yazi "$@" --cwd-file="$tmp"
|
||||
if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
|
||||
builtin cd -- "$cwd"
|
||||
fi
|
||||
rm -f -- "$tmp"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
write_alias_snippet "yazi" "$wrapper_content"
|
||||
}
|
||||
|
||||
install_yazi() {
|
||||
if has_command yazi; then
|
||||
if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Yazi installation."
|
||||
return
|
||||
fi
|
||||
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() {
|
||||
install_yazi
|
||||
add_y_wrapper
|
||||
echo
|
||||
log_success "Yazi installation and configuration complete."
|
||||
}
|
||||
|
||||
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)"'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user