11 Commits

8 changed files with 728 additions and 28 deletions

2
b.sh
View File

@@ -89,7 +89,7 @@ _b_completion() {
# If completing the first argument after 'b'
if [ "$COMP_CWORD" -eq 1 ]; then
opts="all con gone up ware bware"
opts="all con gone up ware bware me trust"
local routes_dir="$HOME/.config/bootstrap"
local installer_keys=""

View File

@@ -125,6 +125,12 @@ EOF
mkdir -p "$routes_dir/installers"
cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/"
fi
# Also copy plugins if they exist locally
if [ -d "$_SCRIPT_DIR/plugins" ]; then
mkdir -p "$routes_dir/plugins"
cp -r "$_SCRIPT_DIR/plugins/"* "$routes_dir/plugins/"
fi
else
log_info "Downloading bootstrap scripts..."
local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"

View 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"
```

View File

@@ -3,8 +3,10 @@
## 1. Objective
Provide a robust rollback mechanism by dynamically generating an uninstallation command list during the installation process. This avoids external parsers, keeps dependencies low, and leverages native Bash execution. It also includes a stateful savepoint system to revert complex environments.
## 2. Core Concept: Dynamic Command Manifests
Instead of tracking state in data files (like JSON), the system procedurally builds a **Command Manifest** (`~/.local/state/bootstrap/uninstallers/<tool>.cmds`) as the installation progresses. Every helper action records its inverse command as an independent line in this manifest.
## 2. Core Concept: Procedural Manifests & JSON Registry
The system uses a hybrid approach:
1. **Procedural Command Manifests**: As the installation progresses, a LIFO script manifest (`$BOOTSTRAP_STATE_DIR/uninstallers/<tool>.cmds`) is built by prepending the inverse commands for each helper action (files, directories, env/alias snippets).
2. **Centralized JSON Registry**: Metadata, strategy, and system-level dependencies are tracked in a thread-safe `$BOOTSTRAP_STATE_DIR/registry.json` using `jq`. During uninstallation, this registry is used to reference-count and safely remove shared system dependencies.
## 3. History & Savepoints (`b fall` and `b rb`)
To allow rolling back multiple installations or returning to a known good state, the system maintains a chronological **History Log** acting as a stack.
@@ -33,17 +35,19 @@ INSTALL: nvim
```
### C. Bare Rollback (`b rb`)
When `b rb` is executed without arguments, it rolls back the single most recent change:
When `b rb` is executed without arguments, it rolls back the single most recent installation:
1. Reads the last line of the history log (e.g., `INSTALL: nvim`).
2. Executes the command manifest for `nvim`.
3. Deletes the last line from the history log.
2. Runs `uninstall_tool nvim` which executes `nvim.cmds` and cleans up registry entries.
### D. Savepoint Rollback (`b rb <name>`)
When `b rb init` is executed, it rolls back all changes made after that savepoint:
### D. Named Rollback (`b rb <tool1>,<tool2>...`)
Users can uninstall specific tools by name (e.g., `b rb nvim` or `b rb nvim,yazi`). The system runs the corresponding uninstaller manifests, reference-counts and removes any orphaned system dependencies, and cleans up the history log and registry.
### E. Savepoint Rollback (`b rb <savepoint>`)
If the argument does not match an installed tool in the registry, it is treated as a savepoint:
1. Parses the history log from bottom to top.
2. For each `INSTALL: <tool>` encountered, it executes the rollback manifest for `<tool>`.
3. Stops when it reaches `SAVEPOINT: init`.
4. Truncates the history log back to the savepoint.
2. For each `INSTALL: <tool>` encountered, it runs `uninstall_tool <tool>`.
3. Stops when it reaches the specified `SAVEPOINT: <name>`.
4. Truncates the history log back to that savepoint.
## 4. Required Abstractions & Helper Modifications
@@ -64,22 +68,18 @@ add_rollback_cmd() {
}
```
### C. Modifying Existing Helpers
Existing helpers automatically generate their own inverse commands.
- **`pkg_install`:**
### C. Helper Operations
Helper functions record their inverse actions directly to the manifest:
- **`write_env_snippet` / `write_alias_snippet` / `write_completion_snippet`:**
```bash
add_rollback_cmd "pkg_remove $pkg"
add_rollback_cmd "rm -f \"\$BOOTSTRAP_DIR/env.d/\$snippet_name.sh\""
```
- **`write_env_snippet` / `write_alias_snippet`:**
- **`track_file` / `track_dir`**:
```bash
add_rollback_cmd "rm -f \"$HOME/.config/bootstrap/env.d/$snippet_name.sh\""
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
```
### D. New File Tracking Helpers
```bash
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
```
Note: distro packages are not tracked via `add_rollback_cmd`. Instead, installers declare them as system dependencies (see below).
## 5. The Rollback Execution (`b rollback <tool>`)
Execution is line-by-line and fault-tolerant, allowing safe recovery even if a user injects a malformed command.
@@ -102,9 +102,11 @@ Because `b ware <tool>` allows users to modify installation scripts:
2. **Fault Isolation:** The `eval` loop ensures that a syntax error in one custom rollback step doesn't crash the removal of other tracked packages.
## 7. Handling Shared Dependencies
The `pkg_remove` helper utilizes reference counting via simple text files (e.g., `~/.local/state/bootstrap/packages/curl`).
- **On `pkg_install`**: Append tool name.
- **On `pkg_remove`**: Remove tool name. If empty, proceed with system uninstallation.
System dependencies installed via `pkg_install` must be registered via `registry_add_sys_deps <tool> <dep1> <dep2>...`.
When a tool is uninstalled:
1. The registry is checked to see if any other installed tool still lists those dependencies.
2. If the reference count drops to `0`, the dependency is automatically removed using `pkg_remove`.
3. If other tools still depend on it, it is kept.
## 8. Fault Tolerance, Resumability, and Interrupted Installations

View File

@@ -216,6 +216,14 @@ for script in "${SCRIPTS[@]}"; do
exit 1
fi
;;
me)
run_plugin "auth" "me" "$@"
exit $?
;;
trust)
run_plugin "auth" "trust" "$@"
exit $?
;;
con)
if [ -f "$BOOTSTRAP_DIR/commands/con.sh" ]; then
. "$BOOTSTRAP_DIR/commands/con.sh" "$@"

View File

@@ -1,5 +1,11 @@
{
"plugins": {
"auth": {
"version": "1.0.0",
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/auth.sh",
"bootstrap": "2.2.0",
"description": "Client Authentication and Provisioning Plugin"
},
"weather": {
"version": "1.0.0",
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/weather.sh",

242
plugins/auth.sh Normal file
View File

@@ -0,0 +1,242 @@
#!/usr/bin/env bash
#
# Authentication & Provisioning Plugin for Bootstrap CLI
# Handles requester (b me) and approver (b trust) flows.
#
set -euo pipefail
# Ensure dependencies are met
pkg_install "arch:openssh|debian:openssh-client|fedora:openssh-clients" "curl" "jq" "age"
# Ensure public key exists next to private key for ssh-keygen -Y sign
ensure_pubkey_exists() {
local priv_key="$1"
local pub_key="${priv_key}.pub"
if [ ! -f "$pub_key" ]; then
ssh-keygen -y -f "$priv_key" > "$pub_key"
fi
}
COMMAND="${1:-}"
if [ -z "$COMMAND" ]; then
echo "Usage: b auth <me|trust> [args...]" >&2
exit 1
fi
shift
# Defaults
SERVER_URL="https://b.adityagupta.dev/auth"
KEY_DIR="$HOME/.config/bootstrap-client"
POLL_INTERVAL=5
ADMIN_KEY="$HOME/.ssh/id_ed25519"
USER_CODE=""
if [ "$COMMAND" = "trust" ]; then
if [ $# -lt 1 ]; then
log_error "user_code is required for trust."
echo "Usage: b trust <user_code> [--server <server_url>] [--admin-key <path>]" >&2
exit 1
fi
USER_CODE="$1"
shift
fi
# Parse remaining arguments
while [ $# -gt 0 ]; do
case "$1" in
--server)
SERVER_URL="$2"
shift 2
;;
--key-dir)
KEY_DIR="$2"
shift 2
;;
--poll-interval)
POLL_INTERVAL="$2"
shift 2
;;
--admin-key)
ADMIN_KEY="$2"
shift 2
;;
*)
log_error "Unknown argument: $1"
exit 1
;;
esac
done
if [ "$COMMAND" = "me" ]; then
mkdir -p "$KEY_DIR"
local_key="$KEY_DIR/id_ed25519"
if [ ! -f "$local_key" ]; then
log_info "Generating local Ed25519 key pair under $KEY_DIR..."
ssh-keygen -t ed25519 -N "" -f "$local_key" >/dev/null
fi
ensure_pubkey_exists "$local_key"
pub_key=$(cat "${local_key}.pub")
hostname=$(hostname 2>/dev/null || uname -n)
os=$(uname -s 2>/dev/null || echo "linux")
# Safely construct JSON payload
json_payload=$(jq -n \
--arg hn "$hostname" \
--arg os "$os" \
--arg pk "$pub_key" \
'{hostname: $hn, os: $os, public_key: $pk}')
log_info "Registering device with $SERVER_URL..."
register_response=$(curl -fsSL -X POST \
-H "Content-Type: application/json" \
-d "$json_payload" \
"$SERVER_URL/api/register")
user_code=$(echo "$register_response" | jq -r '.user_code // empty')
challenge_nonce=$(echo "$register_response" | jq -r '.challenge_nonce // empty')
if [ -z "$user_code" ] || [ -z "$challenge_nonce" ]; then
log_error "Failed to retrieve registration codes from server response."
exit 1
fi
echo "--------------------------------------------------------"
log_success "Device registration initiated successfully!"
echo "Please authorize this device on your administrator machine using:"
echo " b trust $user_code --server $SERVER_URL"
echo "--------------------------------------------------------"
echo "Verification Code: $user_code"
echo "--------------------------------------------------------"
log_info "Waiting for administrator approval (polling every ${POLL_INTERVAL}s)..."
# Prepare challenge poll file signing
temp_nonce_file=$(mktemp)
temp_sig_file="${temp_nonce_file}.sig"
echo -n "$challenge_nonce" > "$temp_nonce_file"
# Ensure cleanup of temp files
cleanup() {
rm -f "$temp_nonce_file" "$temp_sig_file"
}
trap cleanup EXIT INT TERM
while true; do
rm -f "$temp_sig_file"
# Sign challenge nonce
if ! ssh-keygen -Y sign -f "$local_key" -n "bootstrap" "$temp_nonce_file" >/dev/null 2>&1; then
log_error "Cryptographic signing of challenge nonce failed."
exit 1
fi
# Get raw base64 from armored signature file
signature_b64=$(grep -v '^-' "$temp_sig_file" | tr -d '\n')
poll_payload=$(jq -n \
--arg uc "$user_code" \
--arg sig "$signature_b64" \
'{user_code: $uc, signature: $sig}')
poll_out=$(mktemp)
http_code=$(curl -s -o "$poll_out" -w "%{http_code}" -X POST \
-H "Content-Type: application/json" \
-d "$poll_payload" \
"$SERVER_URL/api/challenge/poll")
poll_body=$(cat "$poll_out")
rm -f "$poll_out"
if [ "$http_code" = "200" ]; then
enc_secrets=$(echo "$poll_body" | jq -r '.encrypted_secrets // empty')
if [ -n "$enc_secrets" ] && [ "$enc_secrets" != "null" ]; then
log_success "Device approved by administrator! Decrypting secrets payload..."
decrypted_file="$KEY_DIR/secrets.decrypted"
if echo "$enc_secrets" | base64 -d | age --decrypt -i "$local_key" > "$decrypted_file" 2>/dev/null; then
log_success "Secrets successfully provisioned and written to: $decrypted_file"
cat "$decrypted_file"
break
else
log_error "Decryption using age failed. Please ensure the private key has not been altered."
exit 1
fi
fi
fi
sleep "$POLL_INTERVAL"
done
elif [ "$COMMAND" = "trust" ]; then
if [ ! -f "$ADMIN_KEY" ]; then
log_error "Admin private key not found at: $ADMIN_KEY"
exit 1
fi
ensure_pubkey_exists "$ADMIN_KEY"
log_info "Fetching pending device details for user code: $USER_CODE"
pending_response=$(curl -fsSL "$SERVER_URL/api/pending/$USER_CODE")
requester_pub_key=$(echo "$pending_response" | jq -r '.public_key // empty')
if [ -z "$requester_pub_key" ]; then
log_error "No pending registration found for code '$USER_CODE'."
exit 1
fi
echo "--------------------------------------------------------"
echo "Pending Device Public Key:"
echo "$requester_pub_key"
echo "--------------------------------------------------------"
# Prompt for confirmation (read from tty to support pipeline scenarios)
read -r -p "Do you trust and approve this device? [y/N]: " confirm_choice </dev/tty || confirm_choice="N"
if [[ ! "$confirm_choice" =~ ^[Yy]$ ]]; then
log_warn "Approval aborted."
exit 0
fi
# Generate signature of the requester's public key
temp_pubkey_file=$(mktemp)
temp_pubkey_sig_file="${temp_pubkey_file}.sig"
echo -n "$requester_pub_key" > "$temp_pubkey_file"
# Cleanup trap
cleanup_trust() {
rm -f "$temp_pubkey_file" "$temp_pubkey_sig_file"
}
trap cleanup_trust EXIT INT TERM
if ! ssh-keygen -Y sign -f "$ADMIN_KEY" -n "bootstrap" "$temp_pubkey_file" >/dev/null 2>&1; then
log_error "Cryptographic signing using administrator key failed."
exit 1
fi
signature_b64=$(grep -v '^-' "$temp_pubkey_sig_file" | tr -d '\n')
# Get fingerprint
admin_pubkey_str=$(ssh-keygen -y -f "$ADMIN_KEY")
temp_admin_pub=$(mktemp)
echo "$admin_pubkey_str" > "$temp_admin_pub"
approver_fingerprint=$(ssh-keygen -lf "$temp_admin_pub" | awk '{print $2}')
rm -f "$temp_admin_pub"
# Prepare payload
approve_payload=$(jq -n \
--arg uc "$USER_CODE" \
--arg fp "$approver_fingerprint" \
--arg sig "$signature_b64" \
'{user_code: $uc, approver_public_key_fingerprint: $fp, signature: $sig}')
log_info "Submitting cryptographic approval to server..."
curl -fsSL -X POST \
-H "Content-Type: application/json" \
-d "$approve_payload" \
"$SERVER_URL/api/approve"
log_success "Device with code $USER_CODE has been approved."
fi

View File

@@ -79,7 +79,7 @@ It automatically fuzzy-finds the folder in case there is no exact match. Also, i
### Rollbacks and Savepoints (`b rb` and `b fall`)
Bootstrap CLI features a powerful, procedural rollback system. It strictly tracks every extracted binary, configuration snippet, and package manager transaction to ensure your environment stays clean.
Bootstrap CLI features a robust rollback and uninstallation system driven by procedural manifests and a centralized JSON registry.
To safely uninstall the very last tool you installed (including wiping its shell paths and aliases):
@@ -87,11 +87,21 @@ To safely uninstall the very last tool you installed (including wiping its shell
b rb
```
To uninstall specific tools by name (supporting single or comma-separated lists of tools):
```bash
b rb nvim
b rb bat,yazi,zoxide
```
Shared system package dependencies are reference-counted in the registry and will be automatically cleaned up only when the last tool depending on them is removed.
To create a named savepoint before experimenting with your setup:
```bash
b fall pre_dev_setup
```
*(Note: Savepoint names cannot conflict with the names of any available tools).*
To completely roll back all installations made after that savepoint, restoring your system back to that exact state:
@@ -109,6 +119,30 @@ b up
b up --force
```
### Client Authentication and Provisioning (`b me` and `b trust`)
Bootstrap CLI supports a secure, cryptographic client onboarding and secrets provisioning flow. This allows you to securely register new requester devices and authorize them via an administrator device.
For the complete protocol specification, including the underlying REST API and cryptographic details, see the [Client Authentication and Provisioning Specification](docs/client_spec_auth.md).
#### 1. Device Registration (`b me`)
To initiate registration on a new, unprovisioned machine, run:
```bash
b me [--server <server_url>] [--key-dir <dir>] [--poll-interval <seconds>]
```
This generates a local SSH Ed25519 key pair, registers your device in a pending state with the authentication server, displays a short `user_code`, and starts polling for administrator approval. Once approved, the encrypted payload containing secrets is retrieved, decrypted locally via `age`, and saved to `<key-dir>/secrets.decrypted`.
#### 2. Request Approval (`b trust`)
To authorize a pending device, run this command on an already provisioned administrator machine:
```bash
b trust <user_code> [--server <server_url>] [--admin-key <path_to_admin_private_key>]
```
This retrieves the pending device's public key, prompts the administrator for confirmation, cryptographically signs the public key using the admin key, and submits the approval signature back to the authentication server.
## Plugins (`b <plugin_name>`)
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike installers (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a subshell**.
@@ -177,6 +211,20 @@ b gone -f
Then reload your shell configuration or run `unset -f b` to clear the function definition from your current terminal session.
## Directory Layout
Bootstrap isolates all of its components and installed software inside XDG-compliant directories. You can override these variables in your shell environment, but they default to the following:
| Variable | Default | Purpose |
| :--- | :--- | :--- |
| `BOOTSTRAP_DIR` | `~/.config/bootstrap` | Core configuration, libraries, shell snippets (`env.d`, `aliases.d`, `completions.d`), and local installers |
| `BOOTSTRAP_DATA_DIR` | `~/.local/share/bootstrap` | Active installation bin files, optional components, and runtime installations |
| `BOOTSTRAP_STATE_DIR` | `~/.local/state/bootstrap` | Stateful registry, logs, and rollback/uninstall manifests |
| `BOOTSTRAP_CACHE_DIR` | `~/.cache/bootstrap` | Downloaded files cache and temporary workspace files |
| `BOOTSTRAP_BIN` | `$BOOTSTRAP_DATA_DIR/bin` | Target directory for all installed tools' binaries |
| `BOOTSTRAP_OPT` | `$BOOTSTRAP_DATA_DIR/opt` | Destination for complex, multi-file software distributions |
| `BOOTSTRAP_RUNTIMES` | `$BOOTSTRAP_DATA_DIR/runtimes` | Workspace for programming language runtime managers (e.g. NVM) |
## Development
If you are developing this tool locally:
@@ -201,7 +249,6 @@ The scripts are intentionally straightforward Bash scripts that can be inspected
Potential additions include:
* RSA based authentication
* Development environment bootstrap
* Workstation setup
* Server provisioning