mirror of
https://github.com/sortedcord/bootstrap.git
synced 2026-07-23 04:32:49 +05:30
Compare commits
13 Commits
fix/refere
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
| 506083b85a | |||
| 6e63e54f1e | |||
| 5ba08f3d20 | |||
| eefd792b6c | |||
| 4a03e42b9d | |||
| f6df53ae31 | |||
| 7d4e23e309 | |||
| 190f337f12 | |||
| db6ec1c1c8 | |||
| ed56ef95a9 | |||
| 671cf7f818 | |||
| f5227569b1 | |||
| 15d3a1a59d |
@@ -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`.
|
||||
@@ -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
10
b.sh
@@ -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')
|
||||
|
||||
14
bootstrap.sh
14
bootstrap.sh
@@ -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..."
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
389
docs/installation_strategies.md
Normal file
389
docs/installation_strategies.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Installation Strategies & Installer Lifecycle
|
||||
|
||||
This document explains how Bootstrap categorizes tool installations, the anatomy of an installer script, the full execution lifecycle from invocation to registry recording, and the helper infrastructure available to installers.
|
||||
|
||||
## 1. Installation Strategies
|
||||
|
||||
Every installer declares an explicit **strategy** that describes how it provisions software onto the system. The strategy is recorded in two places:
|
||||
|
||||
1. **At authoring time** — as a `# Strategy:` metadata comment in the installer script header.
|
||||
2. **At runtime** — inside `$BOOTSTRAP_STATE_DIR/registry.json` via `register_tool`.
|
||||
|
||||
### Strategy Types
|
||||
|
||||
| Strategy | Description | Validation (`registry_check`) |
|
||||
| :--- | :--- | :--- |
|
||||
| `binary` | Tool is downloaded as a prebuilt binary (typically from a GitHub release) and placed into `$BOOTSTRAP_BIN`. | Confirms the registered binary path exists and is executable. |
|
||||
| `managed` | Tool is installed through its own dedicated version manager or installer (e.g. NVM for Node.js, `rustup` for Rust). Bootstrap orchestrates the manager but doesn't own the binary directly. | Confirms the tool is available via `command -v`. |
|
||||
| `system` | Tool is installed entirely through the system package manager (`pacman`, `apt`, `dnf`). | Validates recorded system packages using `pkg_check`, falling back to `command -v` when no dependencies are recorded. |
|
||||
|
||||
### Which Strategy to Use
|
||||
|
||||
- **Prefer `binary`** when the upstream project provides standalone Linux binaries on GitHub (e.g. `bat`, `lazygit`, `hyperfine`, `yazi`, `nvim`).
|
||||
- **Use `managed`** when the tool has its own version/runtime manager that must be preserved for updates (e.g. `nvm` for Node.js, `rustup` for Rust, `zoxide` which uses its own install script).
|
||||
- **Use `system`** when the tool is only available through the distro's package manager and there is no upstream binary distribution (e.g. `docker`, `yay`).
|
||||
|
||||
## 2. Installer Script Anatomy
|
||||
|
||||
Every installer lives at `installers/install_<tool>.sh` and follows a strict structure.
|
||||
|
||||
### 2.1 Metadata Header
|
||||
|
||||
The first lines of the file are structured comments parsed by `scripts/generate_registry.sh` to auto-generate `lib/registry.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
# Strategy: binary
|
||||
#
|
||||
```
|
||||
|
||||
| Field | Purpose |
|
||||
| :--- | :--- |
|
||||
| `Tool` | The CLI key used to invoke the installer (`b bat`). Must be lowercase, no spaces. |
|
||||
| `DisplayName` | Human-readable name shown in `b ware` listings and log messages. |
|
||||
| `Description` | One-line description. The word "Install" at the start is stripped automatically by the registry generator. |
|
||||
| `Strategy` | One of `binary`, `managed`, or `system`. |
|
||||
|
||||
### 2.2 Script Body Structure
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
|
||||
# Temporary directory (auto-cleaned)
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# Core installation function
|
||||
install_<tool>() {
|
||||
# 1. Check if already installed, offer reinstall/upgrade
|
||||
# 2. Detect architecture, fetch latest version
|
||||
# 3. Download, extract, install
|
||||
# 4. Track files for rollback
|
||||
# 5. Register in the JSON registry
|
||||
}
|
||||
|
||||
# Optional: shell environment configuration
|
||||
configure_shell() {
|
||||
# Write env/alias/completion snippets
|
||||
}
|
||||
|
||||
main() {
|
||||
install_<tool>
|
||||
configure_shell # if needed
|
||||
|
||||
echo
|
||||
log_success "<Tool> installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
### 2.3 Key Conventions
|
||||
|
||||
- **No standalone execution guards**: Installers are always executed by the Bootstrap runtime, which sources all libraries before running the script. They must never check for or source libraries themselves.
|
||||
- **No `curl` availability checks**: The runtime guarantees `curl` is available.
|
||||
- **No direct `.bashrc` manipulation**: Use `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` to write shell configuration fragments into the snippet directories (`env.d/`, `aliases.d/`, `completions.d/`). The loader sources these automatically.
|
||||
- **Temp directories**: Use `make_temp_dir` (from `lib/common.sh`) for temporary work and clean up via a `trap cleanup EXIT`.
|
||||
|
||||
## 3. Available Helper Functions
|
||||
|
||||
Installers run inside a prepared environment where all Bootstrap libraries have been sourced. The following helper functions are available:
|
||||
|
||||
### 3.1 Common Utilities (`lib/common.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `log_info`, `log_success`, `log_warn`, `log_error` | Coloured logging to stdout/stderr. |
|
||||
| `confirm "<prompt>"` | Interactive yes/no prompt. Reads from `/dev/tty` to support piped execution. |
|
||||
| `has_command <cmd>` | Returns `0` if the command exists on `$PATH`. |
|
||||
| `make_temp_dir` | Creates a `mktemp -d` temporary directory and prints its path. |
|
||||
| `download_file <url> <dest>` | Cached, resumable download. Files are first stored in `$BOOTSTRAP_CACHE_DIR/downloads/` and then copied to `<dest>`. |
|
||||
| `version_lt <v1> <v2>` | Semver comparison. Returns `0` if `v1 < v2`. |
|
||||
|
||||
### 3.2 Platform & Package Management (`lib/platform.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `detect_distro` | Returns `arch`, `debian`, `fedora`, or `unknown`. |
|
||||
| `detect_arch` | Returns `x86_64` or `arm64`. |
|
||||
| `pkg_install <spec>...` | Installs system packages. Supports distro-specific mappings: `"arch:docker\|debian:docker.io\|fedora:docker"`. |
|
||||
| `pkg_check <spec>...` | Returns `0` if all specified packages are installed. |
|
||||
| `pkg_remove <spec>...` | Removes system packages. |
|
||||
|
||||
### 3.3 GitHub Release Helpers (`lib/github.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `github_get_latest_release <owner/repo>` | Queries the GitHub API and prints the `tag_name` of the latest release. |
|
||||
| `github_get_download_url <owner/repo> <tag> <regex>` | Finds the asset matching the regex pattern in the given release and prints its download URL. |
|
||||
| `github_download_asset <owner/repo> <tag> <regex> <dest>` | Resolves and downloads the matching asset to `<dest>`. |
|
||||
|
||||
**Why `github_get_latest_release` is separate from `github_download_asset`**: Asset filenames often embed the version string (e.g. `bat-v0.26.1-x86_64-unknown-linux-gnu.tar.gz`). The installer needs the concrete version *before* constructing the asset pattern and also passes it to `register_tool`.
|
||||
|
||||
### 3.4 Shell Configuration (`lib/shell_config.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `write_env_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/env.d/<name>.sh`. Auto-registers a rollback command to delete the snippet. |
|
||||
| `write_alias_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/aliases.d/<name>.sh`. Auto-registers rollback. |
|
||||
| `write_completion_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/completions.d/<name>.sh`. Auto-registers rollback. |
|
||||
|
||||
### 3.5 Rollback & File Tracking (`lib/rollback.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `track_file <path>` | Records `sudo rm -f '<path>'` in the uninstaller manifest for the current tool. |
|
||||
| `track_dir <path>` | Records `sudo rm -rf '<path>'` in the uninstaller manifest. |
|
||||
| `add_rollback_cmd <cmd>` | Prepends an arbitrary shell command to the manifest (LIFO order). |
|
||||
|
||||
### 3.6 Registry Helpers (`lib/registry_helpers.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `register_tool <tool> <strategy> [version] [source]` | Records the tool in `registry.json` with its strategy, version, source, timestamp, and (for `binary` strategy) its binary path. |
|
||||
| `registry_add_sys_deps <tool> <dep1> <dep2>...` | Appends system package dependency names to the tool's `system_dependencies` array in the registry (used for reference counting during uninstallation). |
|
||||
| `registry_remove_tool <tool>` | Deletes the tool's entire entry from `registry.json`. |
|
||||
| `registry_check <tool>` | Validates that the tool is correctly installed according to its declared strategy. |
|
||||
|
||||
## 4. Execution Lifecycle
|
||||
|
||||
This is the complete flow from when a user types `b <tool>` to when the installation is recorded.
|
||||
|
||||
### 4.1 Invocation
|
||||
|
||||
```
|
||||
User types: b bat
|
||||
│
|
||||
▼
|
||||
b.sh (shell function)
|
||||
│
|
||||
├── Auto-update check (once per 24h)
|
||||
│
|
||||
▼
|
||||
bash routes.sh bat
|
||||
```
|
||||
|
||||
`b.sh` is a shell function sourced from `~/.bashrc`. It delegates to `routes.sh` in a subshell.
|
||||
|
||||
### 4.2 Library Loading
|
||||
|
||||
`routes.sh` sources the core libraries in order:
|
||||
|
||||
1. `lib/common.sh` — Logging, `download_file`, `make_temp_dir`, XDG path variables
|
||||
2. `lib/rollback.sh` — Manifest system, `track_file`, `track_dir`, `init_rollback_system`
|
||||
3. `lib/platform.sh` — Distro/arch detection, `pkg_install`/`pkg_remove`
|
||||
4. `lib/shell_config.sh` — `write_env_snippet`, `write_alias_snippet`, etc.
|
||||
5. `lib/registry.sh` — Auto-generated lookup tables (`INSTALLERS`, `INSTALLER_DISPLAYS`, `INSTALLER_STRATEGIES`, `INSTALLER_KEYS`)
|
||||
6. `lib/registry_helpers.sh` — `register_tool`, `registry_add_sys_deps`, etc.
|
||||
7. `lib/github.sh` — GitHub release API helpers
|
||||
8. `lib/plugins.sh` — Plugin system
|
||||
|
||||
All functions are exported to subshells so the installer script inherits them.
|
||||
|
||||
### 4.3 Routing
|
||||
|
||||
`routes.sh` resolves the command. If the argument matches a key in the `INSTALLERS` registry, it is treated as a tool name and routed to `run_ware`:
|
||||
|
||||
```
|
||||
routes.sh receives "bat"
|
||||
│
|
||||
├── Is it a built-in command (help, rb, fall, con, up, gone, etc.)? → No
|
||||
├── Is it a registered tool key in INSTALLERS? → Yes
|
||||
│
|
||||
▼
|
||||
run_ware "bat" "-y"
|
||||
```
|
||||
|
||||
### 4.4 Installer Resolution
|
||||
|
||||
`run_ware` locates the installer script:
|
||||
|
||||
1. **Local check**: Does `$BOOTSTRAP_DIR/installers/install_bat.sh` exist? If yes, use it directly.
|
||||
2. **Remote download**: If not local, download from the primary Git server (`git.adityagupta.dev`). On failure, fall back to the GitHub mirror.
|
||||
|
||||
### 4.5 Pre-Execution Setup
|
||||
|
||||
Before running the installer, `run_ware` calls `setup_uninstaller_context "bat"`, which:
|
||||
|
||||
- Sets `BOOTSTRAP_CURRENT_TOOL=bat`
|
||||
- Sets `BOOTSTRAP_UNINSTALLER_CMDS=$BOOTSTRAP_STATE_DIR/uninstallers/bat.cmds`
|
||||
- If a manifest already exists from a previous interrupted run, it preserves it (resume support).
|
||||
- Otherwise, creates a fresh empty manifest file.
|
||||
|
||||
Signal traps (`INT`, `TERM`) are installed to catch interruptions.
|
||||
|
||||
### 4.6 Installer Execution
|
||||
|
||||
The installer script runs in a `bash` subshell. During execution:
|
||||
|
||||
- **`track_file`/`track_dir`** calls prepend removal commands to `bat.cmds` (LIFO order).
|
||||
- **`write_env_snippet`/`write_alias_snippet`** automatically record their own rollback commands.
|
||||
- **`pkg_install`** installs system packages. If the tool uses `system` strategy, the installer calls `registry_add_sys_deps` to register these packages for reference counting.
|
||||
- **`register_tool "bat" "binary" "$version" "github:sharkdp/bat"`** writes the tool's metadata to `$BOOTSTRAP_STATE_DIR/registry.json` using thread-safe `flock`.
|
||||
|
||||
### 4.7 Post-Execution
|
||||
|
||||
If the installer exits successfully (`exit 0`), `run_ware`:
|
||||
|
||||
1. Calls `mark_install_success "bat"` — appends `INSTALL: bat` to `$BOOTSTRAP_STATE_DIR/history.log`.
|
||||
2. Re-sources `~/.bashrc` so new env/alias snippets take effect immediately.
|
||||
|
||||
If the installer fails or is interrupted, the user is offered a choice:
|
||||
|
||||
- **Rollback (`r`)**: Executes the manifest line-by-line to undo all recorded changes.
|
||||
- **Keep (`k`)**: Preserves partial changes so the user can resume or debug.
|
||||
|
||||
## 5. The Registry (`registry.json`)
|
||||
|
||||
The JSON registry is the central source of truth for installed tool state. It lives at `$BOOTSTRAP_STATE_DIR/registry.json`.
|
||||
|
||||
### 5.1 Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"bat": {
|
||||
"strategy": "binary",
|
||||
"version": "v0.26.1",
|
||||
"source": "github:sharkdp/bat",
|
||||
"installed_at": "2025-06-28T03:15:00Z",
|
||||
"bin": "/home/user/.local/share/bootstrap/bin/bat"
|
||||
},
|
||||
"docker": {
|
||||
"strategy": "system",
|
||||
"installed_at": "2025-06-28T03:20:00Z",
|
||||
"source": "os-package-manager",
|
||||
"system_dependencies": [
|
||||
"arch:docker|debian:docker.io|fedora:docker"
|
||||
]
|
||||
},
|
||||
"node": {
|
||||
"strategy": "managed",
|
||||
"version": "v0.40.5",
|
||||
"source": "github:nvm-sh/nvm",
|
||||
"installed_at": "2025-06-28T03:25:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Thread Safety
|
||||
|
||||
All mutations go through `registry_set`, which acquires an exclusive file lock (`flock -x`) on `registry.json.lock` before applying a `jq` filter. This prevents race conditions if multiple installers run simultaneously.
|
||||
|
||||
## 6. The Auto-Generated Registry (`lib/registry.sh`)
|
||||
|
||||
`lib/registry.sh` is a **build-time artifact**, not a runtime state file. It is auto-generated by `scripts/generate_registry.sh`, which parses the metadata headers from every `installers/install_*.sh` file and produces four Bash data structures:
|
||||
|
||||
| Variable | Type | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| `INSTALLERS` | `declare -A` | Maps tool key → description |
|
||||
| `INSTALLER_DISPLAYS` | `declare -A` | Maps tool key → human-readable display name |
|
||||
| `INSTALLER_STRATEGIES` | `declare -A` | Maps tool key → strategy (`binary`/`managed`/`system`) |
|
||||
| `INSTALLER_KEYS` | Array | Sorted list of all tool keys |
|
||||
|
||||
These are used by `routes.sh` for command routing, by `b ware` (with no arguments) for listing available tools, and by `b help` for the help output.
|
||||
|
||||
**Important**: `lib/registry.sh` must be regenerated whenever an installer is added, removed, or has its metadata header changed. Run:
|
||||
|
||||
```bash
|
||||
./scripts/generate_registry.sh
|
||||
```
|
||||
|
||||
## 7. Uninstallation Flow
|
||||
|
||||
### 7.1 Named Uninstall (`b rb <tool>`)
|
||||
|
||||
```
|
||||
b rb bat
|
||||
│
|
||||
▼
|
||||
uninstall_tool "bat"
|
||||
│
|
||||
├── 1. execute_rollback "bat"
|
||||
│ → Reads bat.cmds line-by-line (LIFO)
|
||||
│ → Executes: rm binary, rm env snippet, rm alias snippet, etc.
|
||||
│ → Deletes bat.cmds
|
||||
│
|
||||
├── 2. Reference-count system dependencies
|
||||
│ → For each dep in registry_get_sys_deps("bat"):
|
||||
│ → Count how many OTHER tools also list this dep
|
||||
│ → If count == 0: pkg_remove the dep
|
||||
│ → If count > 0: keep it
|
||||
│
|
||||
├── 3. registry_remove_tool "bat"
|
||||
│ → Deletes .tools.bat from registry.json
|
||||
│
|
||||
└── 4. Remove "INSTALL: bat" from history.log
|
||||
```
|
||||
|
||||
### 7.2 Multi-Tool Uninstall (`b rb bat,yazi,zoxide`)
|
||||
|
||||
The argument is split on commas and `uninstall_tool` is called for each tool sequentially.
|
||||
|
||||
### 7.3 Bare Rollback (`b rb`)
|
||||
|
||||
Reads the last line of `history.log`. If it is `INSTALL: <tool>`, calls `uninstall_tool` for that tool.
|
||||
|
||||
### 7.4 Savepoint Rollback (`b rb <savepoint>`)
|
||||
|
||||
If the argument doesn't match a registered tool, it is treated as a savepoint name. The system walks `history.log` backwards, uninstalling each tool encountered until it reaches the matching `SAVEPOINT:` entry.
|
||||
|
||||
## 8. Strategy-Specific Patterns
|
||||
|
||||
### 8.1 Binary Strategy Example (Bat)
|
||||
|
||||
```bash
|
||||
# Detect arch, fetch latest tag from GitHub
|
||||
latest_tag=$(github_get_latest_release "sharkdp/bat")
|
||||
|
||||
# Download the matching release asset
|
||||
github_download_asset "sharkdp/bat" "$latest_tag" "bat-${latest_tag}-${target}\.tar\.gz" "$archive"
|
||||
|
||||
# Extract, copy binary to $BOOTSTRAP_BIN, make executable
|
||||
cp "$extract_dir/bat" "$BOOTSTRAP_BIN/bat"
|
||||
chmod +x "$BOOTSTRAP_BIN/bat"
|
||||
track_file "$BOOTSTRAP_BIN/bat"
|
||||
|
||||
# Register
|
||||
register_tool "bat" "binary" "$latest_tag" "github:sharkdp/bat"
|
||||
```
|
||||
|
||||
### 8.2 Managed Strategy Example (Node.js via NVM)
|
||||
|
||||
```bash
|
||||
# Download and extract NVM into $BOOTSTRAP_RUNTIMES/nvm
|
||||
download_file "$nvm_url" "$TMP_DIR/nvm.tar.gz"
|
||||
tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$BOOTSTRAP_RUNTIMES/nvm" --strip-components=1
|
||||
track_dir "$BOOTSTRAP_RUNTIMES/nvm"
|
||||
|
||||
# Write env snippet so NVM loads on shell startup
|
||||
write_env_snippet "node" "$content"
|
||||
|
||||
# Use NVM to install Node LTS
|
||||
nvm install --lts
|
||||
|
||||
# Register (NVM manages the actual node binary)
|
||||
register_tool "node" "managed" "$latest_tag" "github:nvm-sh/nvm"
|
||||
```
|
||||
|
||||
### 8.3 System Strategy Example (Docker)
|
||||
|
||||
```bash
|
||||
# Install via system package manager
|
||||
pkg_install "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Register the dependency for reference counting
|
||||
registry_add_sys_deps "docker" "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Additional system configuration (groups, systemd)
|
||||
sudo usermod -aG docker "$USER"
|
||||
add_rollback_cmd "sudo gpasswd -d $USER docker || true"
|
||||
|
||||
# Register
|
||||
register_tool "docker" "system" "" "os-package-manager"
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# Plugin Development Guide
|
||||
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike installers (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a sandboxed subshell**.
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike tools (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a sandboxed subshell**.
|
||||
|
||||
This means downloading and invoking a plugin makes no system modifications other than caching the `.sh` file itself. They are fetched only the very first time you invoke them.
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
## 1. Objective
|
||||
Provide a robust rollback mechanism by dynamically generating an uninstallation command list during the installation process. This avoids external parsers, keeps dependencies low, and leverages native Bash execution. It also includes a stateful savepoint system to revert complex environments.
|
||||
|
||||
## 2. Core Concept: Dynamic Command Manifests
|
||||
Instead of tracking state in data files (like JSON), the system procedurally builds a **Command Manifest** (`~/.local/state/bootstrap/uninstallers/<tool>.cmds`) as the installation progresses. Every helper action records its inverse command as an independent line in this manifest.
|
||||
## 2. Core Concept: Procedural Manifests & JSON Registry
|
||||
The system uses a hybrid approach:
|
||||
1. **Procedural Command Manifests**: As the installation progresses, a LIFO script manifest (`$BOOTSTRAP_STATE_DIR/uninstallers/<tool>.cmds`) is built by prepending the inverse commands for each helper action (files, directories, env/alias snippets).
|
||||
2. **Centralized JSON Registry**: Metadata, strategy, and system-level dependencies are tracked in a thread-safe `$BOOTSTRAP_STATE_DIR/registry.json` using `jq`. During uninstallation, this registry is used to reference-count and safely remove shared system dependencies.
|
||||
|
||||
## 3. History & Savepoints (`b fall` and `b rb`)
|
||||
To allow rolling back multiple installations or returning to a known good state, the system maintains a chronological **History Log** acting as a stack.
|
||||
@@ -33,22 +35,24 @@ INSTALL: nvim
|
||||
```
|
||||
|
||||
### C. Bare Rollback (`b rb`)
|
||||
When `b rb` is executed without arguments, it rolls back the single most recent change:
|
||||
When `b rb` is executed without arguments, it rolls back the single most recent installation:
|
||||
1. Reads the last line of the history log (e.g., `INSTALL: nvim`).
|
||||
2. Executes the command manifest for `nvim`.
|
||||
3. Deletes the last line from the history log.
|
||||
2. Runs `uninstall_tool nvim` which executes `nvim.cmds` and cleans up registry entries.
|
||||
|
||||
### D. Savepoint Rollback (`b rb <name>`)
|
||||
When `b rb init` is executed, it rolls back all changes made after that savepoint:
|
||||
### D. Named Rollback (`b rb <tool1>,<tool2>...`)
|
||||
Users can uninstall specific tools by name (e.g., `b rb nvim` or `b rb nvim,yazi`). The system runs the corresponding uninstaller manifests, reference-counts and removes any orphaned system dependencies, and cleans up the history log and registry.
|
||||
|
||||
### E. Savepoint Rollback (`b rb <savepoint>`)
|
||||
If the argument does not match an installed tool in the registry, it is treated as a savepoint:
|
||||
1. Parses the history log from bottom to top.
|
||||
2. For each `INSTALL: <tool>` encountered, it executes the rollback manifest for `<tool>`.
|
||||
3. Stops when it reaches `SAVEPOINT: init`.
|
||||
4. Truncates the history log back to the savepoint.
|
||||
2. For each `INSTALL: <tool>` encountered, it runs `uninstall_tool <tool>`.
|
||||
3. Stops when it reaches the specified `SAVEPOINT: <name>`.
|
||||
4. Truncates the history log back to that savepoint.
|
||||
|
||||
## 4. Required Abstractions & Helper Modifications
|
||||
|
||||
### A. Context Initialization
|
||||
Before executing an installer script, the `b` CLI initializes the command list:
|
||||
Before executing a tool script, the `b` CLI initializes the command list:
|
||||
```bash
|
||||
export BOOTSTRAP_UNINSTALLER_CMDS="$HOME/.local/state/bootstrap/uninstallers/nvim.cmds"
|
||||
mkdir -p "$(dirname "$BOOTSTRAP_UNINSTALLER_CMDS")"
|
||||
@@ -64,22 +68,18 @@ add_rollback_cmd() {
|
||||
}
|
||||
```
|
||||
|
||||
### C. Modifying Existing Helpers
|
||||
Existing helpers automatically generate their own inverse commands.
|
||||
- **`pkg_install`:**
|
||||
### C. Helper Operations
|
||||
Helper functions record their inverse actions directly to the manifest:
|
||||
- **`write_env_snippet` / `write_alias_snippet` / `write_completion_snippet`:**
|
||||
```bash
|
||||
add_rollback_cmd "pkg_remove $pkg"
|
||||
add_rollback_cmd "rm -f \"\$BOOTSTRAP_DIR/env.d/\$snippet_name.sh\""
|
||||
```
|
||||
- **`write_env_snippet` / `write_alias_snippet`:**
|
||||
- **`track_file` / `track_dir`**:
|
||||
```bash
|
||||
add_rollback_cmd "rm -f \"$HOME/.config/bootstrap/env.d/$snippet_name.sh\""
|
||||
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
|
||||
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
|
||||
```
|
||||
|
||||
### D. New File Tracking Helpers
|
||||
```bash
|
||||
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
|
||||
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
|
||||
```
|
||||
Note: distro packages are not tracked via `add_rollback_cmd`. Instead, installers declare them as system dependencies (see below).
|
||||
|
||||
## 5. The Rollback Execution (`b rollback <tool>`)
|
||||
Execution is line-by-line and fault-tolerant, allowing safe recovery even if a user injects a malformed command.
|
||||
@@ -97,21 +97,23 @@ log_success "Rollback complete."
|
||||
```
|
||||
|
||||
## 6. Resilience Against User Modifications
|
||||
Because `b ware <tool>` allows users to modify installation scripts:
|
||||
Because `b ware <tool>` allows users to modify tool scripts:
|
||||
1. **Dynamic Adaptation:** The manifest is built *during* execution, adapting to whatever packages the user manually added.
|
||||
2. **Fault Isolation:** The `eval` loop ensures that a syntax error in one custom rollback step doesn't crash the removal of other tracked packages.
|
||||
|
||||
## 7. Handling Shared Dependencies
|
||||
The `pkg_remove` helper utilizes reference counting via simple text files (e.g., `~/.local/state/bootstrap/packages/curl`).
|
||||
- **On `pkg_install`**: Append tool name.
|
||||
- **On `pkg_remove`**: Remove tool name. If empty, proceed with system uninstallation.
|
||||
System dependencies installed via `pkg_install` must be registered via `registry_add_sys_deps <tool> <dep1> <dep2>...`.
|
||||
When a tool is uninstalled:
|
||||
1. The registry is checked to see if any other installed tool still lists those dependencies.
|
||||
2. If the reference count drops to `0`, the dependency is automatically removed using `pkg_remove`.
|
||||
3. If other tools still depend on it, it is kept.
|
||||
|
||||
## 8. Fault Tolerance, Resumability, and Interrupted Installations
|
||||
|
||||
To handle failures during installation (e.g., network drops, script errors, or user cancellation via `Ctrl+C`), the CLI incorporates a transactional approach that balances **automatic rollback** and **resumability**:
|
||||
|
||||
### A. The Interruption Trap & Prompt
|
||||
When running an installer, the central router (`lib/routes.sh`) traps `SIGINT` and `SIGTERM` signals. If the installation fails or is interrupted:
|
||||
When running a tool, the central router (`lib/routes.sh`) traps `SIGINT` and `SIGTERM` signals. If the installation fails or is interrupted:
|
||||
1. The trap catches the event and stops execution.
|
||||
2. The user is prompted interactively:
|
||||
- **Rollback (r)**: Invokes `execute_rollback <tool>` immediately to clean up all partial modifications.
|
||||
@@ -128,6 +130,6 @@ If the user chooses to **keep** the partial state and runs `b <tool>` again:
|
||||
To make rerunning an interrupted script fast and efficient, installers use `download_file <url> <dest>` instead of raw `curl`:
|
||||
1. It downloads the payload to a central cache directory: `~/.local/state/bootstrap/cache/`.
|
||||
2. It uses `curl -C -` to continue the download from the byte offset where it was interrupted.
|
||||
3. Once completed, it copies the cached file to the installer's temp directory.
|
||||
3. Once completed, it copies the cached file to the tool's temp directory.
|
||||
4. Distro package manager commands (`pkg_install`) and shell snippets (`write_env_snippet`) are naturally idempotent, allowing the script to breeze through already completed steps in milliseconds and resume exactly where the heavy work failed.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ The Bootstrap CLI uses a semantic, git-tag-driven versioning workflow.
|
||||
|
||||
- **Do not manually edit the `VERSION` file.**
|
||||
- Develop normally using [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat: Add new tool`, `fix: Typo in script`).
|
||||
- **Note on Installers:** Adding or modifying installers in the `installers/` directory **does not** require a version bump. They are fetched dynamically from the registry when a user runs `b ware <tool>`.
|
||||
- **Note on Tools:** Adding or modifying tools in the `tools/` directory **does not** require a version bump. They are fetched dynamically from the registry when a user runs `b ware <tool>`.
|
||||
|
||||
## 2. When to Cut a Release
|
||||
|
||||
|
||||
@@ -1,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.
|
||||
|
||||
@@ -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" "$@"
|
||||
|
||||
@@ -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
242
plugins/auth.sh
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Authentication & Provisioning Plugin for Bootstrap CLI
|
||||
# Handles requester (b me) and approver (b trust) flows.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure dependencies are met
|
||||
pkg_install "arch:openssh|debian:openssh-client|fedora:openssh-clients" "curl" "jq" "age"
|
||||
|
||||
|
||||
# Ensure public key exists next to private key for ssh-keygen -Y sign
|
||||
ensure_pubkey_exists() {
|
||||
local priv_key="$1"
|
||||
local pub_key="${priv_key}.pub"
|
||||
if [ ! -f "$pub_key" ]; then
|
||||
ssh-keygen -y -f "$priv_key" > "$pub_key"
|
||||
fi
|
||||
}
|
||||
|
||||
COMMAND="${1:-}"
|
||||
if [ -z "$COMMAND" ]; then
|
||||
echo "Usage: b auth <me|trust> [args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
|
||||
# Defaults
|
||||
SERVER_URL="https://b.adityagupta.dev/auth"
|
||||
KEY_DIR="$HOME/.config/bootstrap-client"
|
||||
POLL_INTERVAL=5
|
||||
ADMIN_KEY="$HOME/.ssh/id_ed25519"
|
||||
USER_CODE=""
|
||||
|
||||
if [ "$COMMAND" = "trust" ]; then
|
||||
if [ $# -lt 1 ]; then
|
||||
log_error "user_code is required for trust."
|
||||
echo "Usage: b trust <user_code> [--server <server_url>] [--admin-key <path>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
USER_CODE="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Parse remaining arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server)
|
||||
SERVER_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--key-dir)
|
||||
KEY_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
POLL_INTERVAL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--admin-key)
|
||||
ADMIN_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$COMMAND" = "me" ]; then
|
||||
mkdir -p "$KEY_DIR"
|
||||
local_key="$KEY_DIR/id_ed25519"
|
||||
|
||||
if [ ! -f "$local_key" ]; then
|
||||
log_info "Generating local Ed25519 key pair under $KEY_DIR..."
|
||||
ssh-keygen -t ed25519 -N "" -f "$local_key" >/dev/null
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$local_key"
|
||||
pub_key=$(cat "${local_key}.pub")
|
||||
hostname=$(hostname 2>/dev/null || uname -n)
|
||||
os=$(uname -s 2>/dev/null || echo "linux")
|
||||
|
||||
# Safely construct JSON payload
|
||||
json_payload=$(jq -n \
|
||||
--arg hn "$hostname" \
|
||||
--arg os "$os" \
|
||||
--arg pk "$pub_key" \
|
||||
'{hostname: $hn, os: $os, public_key: $pk}')
|
||||
|
||||
log_info "Registering device with $SERVER_URL..."
|
||||
|
||||
register_response=$(curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$json_payload" \
|
||||
"$SERVER_URL/api/register")
|
||||
|
||||
user_code=$(echo "$register_response" | jq -r '.user_code // empty')
|
||||
challenge_nonce=$(echo "$register_response" | jq -r '.challenge_nonce // empty')
|
||||
|
||||
if [ -z "$user_code" ] || [ -z "$challenge_nonce" ]; then
|
||||
log_error "Failed to retrieve registration codes from server response."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
log_success "Device registration initiated successfully!"
|
||||
echo "Please authorize this device on your administrator machine using:"
|
||||
echo " b trust $user_code --server $SERVER_URL"
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Verification Code: $user_code"
|
||||
echo "--------------------------------------------------------"
|
||||
log_info "Waiting for administrator approval (polling every ${POLL_INTERVAL}s)..."
|
||||
|
||||
# Prepare challenge poll file signing
|
||||
temp_nonce_file=$(mktemp)
|
||||
temp_sig_file="${temp_nonce_file}.sig"
|
||||
echo -n "$challenge_nonce" > "$temp_nonce_file"
|
||||
|
||||
# Ensure cleanup of temp files
|
||||
cleanup() {
|
||||
rm -f "$temp_nonce_file" "$temp_sig_file"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
rm -f "$temp_sig_file"
|
||||
|
||||
# Sign challenge nonce
|
||||
if ! ssh-keygen -Y sign -f "$local_key" -n "bootstrap" "$temp_nonce_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing of challenge nonce failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get raw base64 from armored signature file
|
||||
signature_b64=$(grep -v '^-' "$temp_sig_file" | tr -d '\n')
|
||||
|
||||
poll_payload=$(jq -n \
|
||||
--arg uc "$user_code" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, signature: $sig}')
|
||||
|
||||
poll_out=$(mktemp)
|
||||
http_code=$(curl -s -o "$poll_out" -w "%{http_code}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$poll_payload" \
|
||||
"$SERVER_URL/api/challenge/poll")
|
||||
|
||||
poll_body=$(cat "$poll_out")
|
||||
rm -f "$poll_out"
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
enc_secrets=$(echo "$poll_body" | jq -r '.encrypted_secrets // empty')
|
||||
if [ -n "$enc_secrets" ] && [ "$enc_secrets" != "null" ]; then
|
||||
log_success "Device approved by administrator! Decrypting secrets payload..."
|
||||
|
||||
decrypted_file="$KEY_DIR/secrets.decrypted"
|
||||
if echo "$enc_secrets" | base64 -d | age --decrypt -i "$local_key" > "$decrypted_file" 2>/dev/null; then
|
||||
log_success "Secrets successfully provisioned and written to: $decrypted_file"
|
||||
cat "$decrypted_file"
|
||||
break
|
||||
else
|
||||
log_error "Decryption using age failed. Please ensure the private key has not been altered."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
|
||||
elif [ "$COMMAND" = "trust" ]; then
|
||||
if [ ! -f "$ADMIN_KEY" ]; then
|
||||
log_error "Admin private key not found at: $ADMIN_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$ADMIN_KEY"
|
||||
|
||||
log_info "Fetching pending device details for user code: $USER_CODE"
|
||||
pending_response=$(curl -fsSL "$SERVER_URL/api/pending/$USER_CODE")
|
||||
|
||||
requester_pub_key=$(echo "$pending_response" | jq -r '.public_key // empty')
|
||||
if [ -z "$requester_pub_key" ]; then
|
||||
log_error "No pending registration found for code '$USER_CODE'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Pending Device Public Key:"
|
||||
echo "$requester_pub_key"
|
||||
echo "--------------------------------------------------------"
|
||||
|
||||
# Prompt for confirmation (read from tty to support pipeline scenarios)
|
||||
read -r -p "Do you trust and approve this device? [y/N]: " confirm_choice </dev/tty || confirm_choice="N"
|
||||
if [[ ! "$confirm_choice" =~ ^[Yy]$ ]]; then
|
||||
log_warn "Approval aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate signature of the requester's public key
|
||||
temp_pubkey_file=$(mktemp)
|
||||
temp_pubkey_sig_file="${temp_pubkey_file}.sig"
|
||||
echo -n "$requester_pub_key" > "$temp_pubkey_file"
|
||||
|
||||
# Cleanup trap
|
||||
cleanup_trust() {
|
||||
rm -f "$temp_pubkey_file" "$temp_pubkey_sig_file"
|
||||
}
|
||||
trap cleanup_trust EXIT INT TERM
|
||||
|
||||
if ! ssh-keygen -Y sign -f "$ADMIN_KEY" -n "bootstrap" "$temp_pubkey_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing using administrator key failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
signature_b64=$(grep -v '^-' "$temp_pubkey_sig_file" | tr -d '\n')
|
||||
|
||||
# Get fingerprint
|
||||
admin_pubkey_str=$(ssh-keygen -y -f "$ADMIN_KEY")
|
||||
temp_admin_pub=$(mktemp)
|
||||
echo "$admin_pubkey_str" > "$temp_admin_pub"
|
||||
approver_fingerprint=$(ssh-keygen -lf "$temp_admin_pub" | awk '{print $2}')
|
||||
rm -f "$temp_admin_pub"
|
||||
|
||||
# Prepare payload
|
||||
approve_payload=$(jq -n \
|
||||
--arg uc "$USER_CODE" \
|
||||
--arg fp "$approver_fingerprint" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, approver_public_key_fingerprint: $fp, signature: $sig}')
|
||||
|
||||
log_info "Submitting cryptographic approval to server..."
|
||||
curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$approve_payload" \
|
||||
"$SERVER_URL/api/approve"
|
||||
|
||||
log_success "Device with code $USER_CODE has been approved."
|
||||
fi
|
||||
57
readme.md
57
readme.md
@@ -79,7 +79,7 @@ It automatically fuzzy-finds the folder in case there is no exact match. Also, i
|
||||
|
||||
### Rollbacks and Savepoints (`b rb` and `b fall`)
|
||||
|
||||
Bootstrap CLI features a powerful, procedural rollback system. It strictly tracks every extracted binary, configuration snippet, and package manager transaction to ensure your environment stays clean.
|
||||
Bootstrap CLI features a robust rollback and uninstallation system driven by procedural manifests and a centralized JSON registry.
|
||||
|
||||
To safely uninstall the very last tool you installed (including wiping its shell paths and aliases):
|
||||
|
||||
@@ -87,11 +87,21 @@ To safely uninstall the very last tool you installed (including wiping its shell
|
||||
b rb
|
||||
```
|
||||
|
||||
To uninstall specific tools by name (supporting single or comma-separated lists of tools):
|
||||
|
||||
```bash
|
||||
b rb nvim
|
||||
b rb bat,yazi,zoxide
|
||||
```
|
||||
|
||||
Shared system package dependencies are reference-counted in the registry and will be automatically cleaned up only when the last tool depending on them is removed.
|
||||
|
||||
To create a named savepoint before experimenting with your setup:
|
||||
|
||||
```bash
|
||||
b fall pre_dev_setup
|
||||
```
|
||||
*(Note: Savepoint names cannot conflict with the names of any available tools).*
|
||||
|
||||
To completely roll back all installations made after that savepoint, restoring your system back to that exact state:
|
||||
|
||||
@@ -109,9 +119,33 @@ 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**.
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
@@ -216,8 +263,8 @@ Running scripts directly from the internet is convenient but should always be ap
|
||||
### Why Bootstrap is More Secure
|
||||
Instead of blindly piping large, third-party installation scripts (often hundreds or thousands of lines of unscrutinized code) directly from the internet into your shell, Bootstrap uses a **scrutinized and simplified** approach:
|
||||
|
||||
1. **Audited & Minimised**: Every script in `installers/` has been scrutinized, refactored, and stripped of redundant compatibility layers (like Windows/macOS support or complex environment checks), leaving only the essential, readable logic required for your setup.
|
||||
2. **Controlled Execution**: Since installer scripts are hosted locally in your cloned repository, you can review every line of code before executing it. You are never subject to silent, upstream changes to installers.
|
||||
1. **Audited & Minimised**: Every script in `tools/` has been scrutinized, refactored, and stripped of redundant compatibility layers (like Windows/macOS support or complex environment checks), leaving only the essential, readable logic required for your setup.
|
||||
2. **Controlled Execution**: Since tool scripts are hosted locally in your cloned repository, you can review every line of code before executing it. You are never subject to silent, upstream changes to tools.
|
||||
3. **No Raw Pipes**: We download official binary releases directly to verified temporary locations rather than running arbitrary third-party script pipelines.
|
||||
|
||||
If you want to audit the core Bootstrap CLI itself before running it:
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
INSTALLERS_DIR="$REPO_DIR/installers"
|
||||
TOOLS_DIR="$REPO_DIR/tools"
|
||||
REGISTRY_FILE="$REPO_DIR/lib/registry.sh"
|
||||
|
||||
echo "==> Generating registry.sh..."
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user