5 Commits

Author SHA1 Message Date
db6ec1c1c8 refactor(plugin:auth): Removed dependency loop in favour of pkg_install
Some checks failed
Lint / lint (push) Failing after 14s
Lint / lint (pull_request) Failing after 12s
2026-06-27 09:16:11 +05:30
ed56ef95a9 feat: Implemented client spec for bootstrap-auth-server with b me and b trust
Some checks failed
Lint / lint (push) Failing after 17s
Lint / lint (pull_request) Failing after 12s
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
9 changed files with 453 additions and 20 deletions

View File

@@ -20,7 +20,7 @@ bootstrap/
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install(), pkg_check(), pkg_remove()
│ ├── rollback.sh # Rollback tracking (track_file, track_dir, add_rollback_cmd)
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet, write_completion_snippet
│ ├── registry.sh # Dynamically generated installer registry
│ └── routes.sh # Central router script
├── commands/ # Non-installer commands (help, con, uninstall)
@@ -35,17 +35,26 @@ bootstrap/
When adding a new installer named `<name>`:
### Step 1: Create the installer script
### Step 1: Analyze user request & gather details
Create `installers/install_<name>.sh` using the template below.
When the user asks you to create an installer, they often provide either an official `curl` install script or a link to a `.tar.gz` release.
You MUST do the following before writing the script:
If the user provides an official install or curl script in the prompt:
- Read and analyze the script.
- Remove redundant parts like macOS and Windows compatibility.
- Strip unnecessary shell boilerplate, self-update logic, and other bloat.
- Implement only the essential Linux installation logic inside the `install_<name>` function.
**If the user provides a `curl` command or link to an install script:**
- Execute the `curl` command (or use `read_url_content`) to fetch the script and analyze what it actually does under the hood.
- Do NOT simply execute the official script blindly in your installer.
- Re-write its functionality according to the conventions of the bootstrap installer.
- Strip away redundant code, OS checks for macOS/Windows (we only target Linux), and unnecessary shell configuration logic.
- Implement only the core, essential Linux installation logic inside the `install_<name>` function.
### Step 2: Add metadata comments to the top of your installer script
**If the user provides a link to a `.tar.gz` (or `.zip`):**
- First, download the archive to a temporary directory and extract it to inspect its contents.
- Analyze the extracted folder structure to decide what needs to be installed (e.g., binaries, man pages, completions) and what should be ignored/deleted.
- Write the `install_<name>` function to download, extract, and copy only those essential files. (Use `download_file` and temporary directories, see "Resumable Download and Extraction" below).
### Step 2: Create the installer script
### Step 3: Add metadata comments to the top of your installer script
At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following three metadata headers:
```bash
@@ -56,15 +65,15 @@ At the top of your new installer script, right below `#!/usr/bin/env bash`, add
The central router `lib/routes.sh` and autocomplete function in `b.sh` will dynamically parse this metadata from all `install_*.sh` scripts to register the installer and keys automatically! No manual edits to `lib/routes.sh` or `b.sh` are required.
### Step 3: Implement Rollback Tracking (Crucial)
### Step 4: Implement Rollback Tracking (Crucial)
To ensure the user can seamlessly use `b rb <name>`, all manual modifications must be tracked:
- When extracting binaries to `~/.local/bin/`, use `track_file "$HOME/.local/bin/binary"`.
- When creating directories like `~/.config/tool/`, use `track_dir "$HOME/.config/tool"`.
- When running manual apt/dnf/npm commands, log their inverses: `add_rollback_cmd "sudo npm uninstall -g package"`.
Note: `pkg_install`, `write_env_snippet`, and `write_alias_snippet` will automatically track themselves.
Note: `pkg_install`, `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` will automatically track themselves.
### Step 4: Verify (optional)
### Step 5: Verify (optional)
Verify that the installer works and appears in the help output:
- Run `b all` to confirm it appears in the help list.
@@ -126,6 +135,7 @@ configure_shell() {
# Use drop-in snippets for shell configuration (they auto-rollback)
# write_env_snippet "<name>" "export VAR_NAME=value\neval \"\$(<name> init bash)\""
# write_alias_snippet "<name>" "alias <name>='<command>'"
# write_completion_snippet "<name>" "source <(<command> completion bash)"
:
}
@@ -184,6 +194,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|---|---|
| `write_env_snippet <name> <content>` | Creates an isolated `env.d/` shell drop-in snippet and registers it for rollback. |
| `write_alias_snippet <name> <content>` | Creates an isolated `aliases.d/` shell drop-in snippet and registers it for rollback. |
| `write_completion_snippet <name> <content>` | Creates an isolated `completions.d/` bash completion snippet and registers it for rollback. |
---
@@ -241,7 +252,7 @@ track_file "/usr/local/bin/binary"
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
2. **Confirmation prompts**: Always ask before installing. Check if already installed first.
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `~/.local/bin/`, you MUST call `track_file`. If you run `makepkg`, you MUST call `add_rollback_cmd` for `pacman -R`.
4. **Shell Drop-ins**: Always use `write_env_snippet` or `write_alias_snippet` instead of manually injecting code directly into `~/.bashrc`.
4. **Shell Drop-ins**: Always use `write_env_snippet`, `write_alias_snippet`, or `write_completion_snippet` instead of manually injecting code directly into `~/.bashrc`.
5. **No hardcoded paths**: Use `$HOME`, library functions, and `detect_*` helpers.
6. **Error handling**: Use `set -euo pipefail` after the guard block.
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.

2
b.sh
View File

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

View File

@@ -71,6 +71,7 @@ install_bootstrap() {
mkdir -p "$routes_dir"
mkdir -p "$routes_dir/env.d"
mkdir -p "$routes_dir/aliases.d"
mkdir -p "$routes_dir/completions.d"
# List of all files to download/copy
local files=(
@@ -104,6 +105,12 @@ install_bootstrap() {
mkdir -p "$routes_dir/installers"
cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/"
fi
# Also copy plugins if they exist locally
if [ -d "$_SCRIPT_DIR/plugins" ]; then
mkdir -p "$routes_dir/plugins"
cp -r "$_SCRIPT_DIR/plugins/"* "$routes_dir/plugins/"
fi
else
log_info "Downloading bootstrap scripts..."
local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
@@ -142,6 +149,7 @@ export BOOTSTRAP_DIR="$HOME/.config/bootstrap"
[ -f "$BOOTSTRAP_DIR/b.sh" ] && . "$BOOTSTRAP_DIR/b.sh"
for f in "$BOOTSTRAP_DIR/env.d/"*.sh; do [ -r "$f" ] && . "$f"; done
for f in "$BOOTSTRAP_DIR/aliases.d/"*.sh; do [ -r "$f" ] && . "$f"; done
for f in "$BOOTSTRAP_DIR/completions.d/"*.sh; do [ -r "$f" ] && . "$f"; done
# <<< bootstrap-cli setup <<<
EOF

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# Tool: hyperfine
# DisplayName: Hyperfine
# Description: Command-line benchmarking tool
#
# Hyperfine Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
install_hyperfine() {
if has_command hyperfine || [ -f "$HOME/.local/bin/hyperfine" ]; then
if ! confirm "Hyperfine is already installed. Reinstall/Upgrade?"; then
log_info "Skipping Hyperfine installation."
return
fi
else
if ! confirm "Install Hyperfine?"; then
log_info "Skipping Hyperfine installation."
return
fi
fi
# Ensure curl is installed
if ! has_command curl; then
log_info "curl not found. Installing curl..."
pkg_install curl
fi
# Detect architecture
local raw_arch
raw_arch=$(detect_arch)
local arch=""
case "$raw_arch" in
x86_64) arch="x86_64" ;;
arm64) arch="aarch64" ;;
*) log_error "Unsupported Linux architecture: $raw_arch"; exit 1 ;;
esac
log_info "Fetching latest Hyperfine version from GitHub..."
local latest_tag=""
latest_tag=$(curl -sL https://api.github.com/repos/sharkdp/hyperfine/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
local download_url
if [ -n "$latest_tag" ]; then
log_info "Latest Hyperfine version found: $latest_tag"
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
else
latest_tag="v1.20.0"
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
fi
log_info "Downloading Hyperfine from ${download_url}..."
local archive="$TMP_DIR/hyperfine.tar.gz"
download_file "$download_url" "$archive"
# Extract the archive
log_info "Extracting Hyperfine archive..."
tar -xzf "$archive" -C "$TMP_DIR"
local extract_dir="$TMP_DIR/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu"
if [ ! -d "$extract_dir" ]; then
# Handle case where directory name might differ (e.g. without leading v in directory name or tag)
extract_dir=$(find "$TMP_DIR" -maxdepth 1 -type d -name "hyperfine-*" | head -n1)
fi
if [ -z "$extract_dir" ] || [ ! -d "$extract_dir" ]; then
log_error "Failed to locate extracted Hyperfine directory."
exit 1
fi
# Install binary to ~/.local/bin
local target_dir="$HOME/.local/bin"
mkdir -p "$target_dir"
log_info "Installing Hyperfine to $target_dir/hyperfine..."
cp "$extract_dir/hyperfine" "$target_dir/hyperfine"
chmod +x "$target_dir/hyperfine"
track_file "$target_dir/hyperfine"
# Install man page if present
if [ -f "$extract_dir/hyperfine.1" ]; then
local man_dir="$HOME/.local/share/man/man1"
mkdir -p "$man_dir"
log_info "Installing man page to $man_dir/hyperfine.1..."
cp "$extract_dir/hyperfine.1" "$man_dir/hyperfine.1"
track_file "$man_dir/hyperfine.1"
fi
# Install autocomplete if present
if [ -f "$extract_dir/autocomplete/hyperfine.bash" ]; then
log_info "Installing bash completions..."
local comp_content
comp_content=$(cat "$extract_dir/autocomplete/hyperfine.bash")
write_completion_snippet "hyperfine" "$comp_content"
fi
}
configure_shell() {
# Add ~/.local/bin to PATH for the current process
export PATH="$HOME/.local/bin:$PATH"
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "local-bin path"
done
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
}
main() {
install_hyperfine
configure_shell
echo
log_success "Hyperfine installation and configuration complete."
}
main "$@"

View File

@@ -5,6 +5,7 @@ declare -A INSTALLERS=(
[asciicinema]="asciinema terminal recorder"
[bat]="Bat (alternative to cat) and configure alias"
[docker]="Container runtime and orchestration platform"
[hyperfine]="Command-line benchmarking tool"
[lazygit]="Simple terminal UI for git commands"
[node]="Node.js (LTS) and NVM"
[nvim]="Neovim 0.12.0 and configuration"
@@ -22,6 +23,7 @@ declare -A INSTALLER_DISPLAYS=(
[asciicinema]="asciicinema"
[bat]="Bat"
[docker]="Docker"
[hyperfine]="Hyperfine"
[lazygit]="lazygit"
[node]="Node"
[nvim]="Neovim"
@@ -34,4 +36,4 @@ declare -A INSTALLER_DISPLAYS=(
[zoxide]="Zoxide"
)
INSTALLER_KEYS=(agy asciicinema bat docker lazygit node nvim pnpm rust starship uv yay yazi zoxide)
INSTALLER_KEYS=(agy asciicinema bat docker hyperfine lazygit node nvim pnpm rust starship uv yay yazi zoxide)

View File

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

View File

@@ -158,6 +158,34 @@ remove_alias_snippet() {
fi
}
# Write completion snippet to completions.d/
# Usage: write_completion_snippet <name> <content>
write_completion_snippet() {
local name="$1"
local content="$2"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
mkdir -p "$dir"
log_info "Writing completion snippet '$name' to $dir/${name}.sh"
echo "$content" > "$dir/${name}.sh"
if type add_rollback_cmd >/dev/null 2>&1; then
add_rollback_cmd "rm -f \"$dir/${name}.sh\""
fi
}
# Remove completion snippet from completions.d/
# Usage: remove_completion_snippet <name>
remove_completion_snippet() {
local name="$1"
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
if [ -f "$dir/${name}.sh" ]; then
log_info "Removing completion snippet '$name'"
rm -f "$dir/${name}.sh"
fi
}
# Setup fd symlink for Debian/Ubuntu (fdfind -> fd)
create_fd_symlink() {
if ! has_command fd && has_command fdfind; then
@@ -176,8 +204,4 @@ source_bashrc() {
# Export functions and variables for subshells
export _LIB_SHELL_CONFIG_SOURCED=1
export -f get_shell_configs remove_block inject_block add_alias_if_missing add_env_if_missing create_fd_symlink write_env_snippet write_alias_snippet remove_env_snippet remove_alias_snippet source_bashrc
export -f get_shell_configs remove_block inject_block add_alias_if_missing add_env_if_missing create_fd_symlink write_env_snippet write_alias_snippet remove_env_snippet remove_alias_snippet write_completion_snippet remove_completion_snippet source_bashrc

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