16 Commits

Author SHA1 Message Date
2fa8f6695f Merge pull request #26 from sortedcord/refactor/installer-project-scaffolding
refactor: Scaffolding Redesign - Move Installers to Tools Layout
2026-06-28 15:41:28 +05:30
f2c9a4b104 docs: Route bootstrap script through cloudflare worker 2026-06-28 14:22:48 +05:30
6afaf372f2 docs: Moved authentication to plugins and improve auth documentation 2026-06-28 12:24:27 +05:30
506083b85a refactor: Moved hyperfine to follow tools scaffolding 2026-06-28 11:29:56 +05:30
6e63e54f1e Shifted installers to to tools 2026-06-28 11:21:25 +05:30
5ba08f3d20 Merge branch 'master' of git.adityagupta.dev:sortedcord/bootstrap 2026-06-28 09:05:00 +05:30
eefd792b6c docs: Update readme for authentication plugin 2026-06-28 09:04:48 +05:30
4a03e42b9d docs: Update documentation to incorporate for Installation Strategies Redesign 2026-06-28 09:02:18 +05:30
f6df53ae31 Merge pull request 'feat: Add Client Authentication and Provisioning Plugin' (#22) from feat/auth-plugin into master
Reviewed-on: #22
2026-06-28 08:53:40 +05:30
7d4e23e309 Merge branch 'fix/reference-counting' 2026-06-28 08:53:02 +05:30
190f337f12 merge 2026-06-28 08:51:25 +05:30
db6ec1c1c8 refactor(plugin:auth): Removed dependency loop in favour of pkg_install 2026-06-27 09:16:11 +05:30
ed56ef95a9 feat: Implemented client spec for bootstrap-auth-server with b me and b trust 2026-06-27 09:04:10 +05:30
671cf7f818 feat(Installer): Add installer for hyperfine 2026-06-27 08:17:31 +05:30
f5227569b1 feat(skill): Update add_installer skill 2026-06-27 08:09:46 +05:30
15d3a1a59d feat: Support for drop-in completions
`shell_config.sh` has support for tool completions using the
`write_completion_snippet` and `write_alias_snippet`
2026-06-27 07:58:10 +05:30
31 changed files with 908 additions and 216 deletions

View File

@@ -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,12 +33,12 @@ 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 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.
@@ -51,13 +51,15 @@ If the user provides an official install or curl script in the prompt, or a link
**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 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>
@@ -65,7 +67,7 @@ At the top of your new installer script, right below `#!/usr/bin/env bash`, add
# 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)
@@ -78,16 +80,16 @@ To ensure the user can seamlessly use `b rb <name>` to uninstall or rollback a t
### 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 structure. Copy this and fill in the tool-specific logic (notice there are no standalone execution guards or curl checks):
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
@@ -152,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`
@@ -248,7 +250,7 @@ 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 `$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`.

View File

@@ -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:

10
b.sh
View File

@@ -89,12 +89,12 @@ _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=""
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')
@@ -135,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')

View File

@@ -120,10 +120,16 @@ EOF
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
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..."

View File

@@ -1,13 +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"

View File

@@ -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`

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

@@ -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.

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,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.

View File

@@ -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

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# GitHub API helper functions for Bootstrap installers
# GitHub API helper functions for Bootstrap tools
# Usage: github_get_latest_release <owner/repo>
# Prints the tag_name of the latest release.

View File

@@ -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..."
@@ -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

@@ -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:

View File

@@ -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..."
@@ -16,7 +16,7 @@ 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')