29 Commits

Author SHA1 Message Date
83c524441c fix #19: Prevent Checkpoint from having same bug as
Some checks failed
Lint / lint (push) Failing after 14s
Lint / lint (pull_request) Failing after 13s
2026-06-27 00:39:33 +05:30
cee345e3f0 feat: Unified Uninstallation and Rollback
- Removed `BOOTSTRAP_PACKAGES_DIR`
b rb <tool> can now uninstall that particular tool
2026-06-27 00:20:56 +05:30
0c16640593 refactor: Simplified pkg_install and pkg_remove
`pkg_install` and `pkg_remove` are just wrappers that map aliases and
directly invooke the system package manager.

- They do not manage state or inject rollback commands
2026-06-27 00:19:14 +05:30
d5c90d6e85 refactor: Use XDG compliant isolated directory structure
Some checks failed
Lint / lint (push) Failing after 21s
Lint / lint (pull_request) Failing after 16s
- Using $BOOTSTRAP_BIN, $BOOTSTRAP_OPT, etc
- Add defensive fallback for undefined vars in common.sh
2026-06-26 23:52:03 +05:30
4c1c7de0b7 refactor: Remove redundant curl availability checks
Some checks failed
Lint / lint (push) Failing after 56s
Lint / lint (pull_request) Failing after 1m5s
2026-06-26 21:49:24 +05:30
29de051b7d refactor: Remove standalone exec prevention code blocks from installers
Some checks failed
Lint / lint (push) Failing after 16s
Lint / lint (pull_request) Failing after 35s
2026-06-26 21:40:48 +05:30
a4e5bc1175 refactor: Remove legacy backwards compat code
Some checks failed
Lint / lint (push) Failing after 37s
Lint / lint (pull_request) Failing after 47s
Remove the configure shell code blocks
2026-06-26 21:37:11 +05:30
36c7be07b3 refactor: Migrate all installers to use installation strategies 2026-06-26 21:35:34 +05:30
0eaea2c997 refactor: Unify fragmented install strategies within installers
Some checks failed
Lint / lint (push) Failing after 33s
Lint / lint (pull_request) Failing after 17s
bat and yazi installers use latest binary releases from github over
package manager for arch and fedora
2026-06-26 20:19:19 +05:30
4eec27570e refactor: Installers use github_get_latest_release helpers
Some checks failed
Lint / lint (push) Failing after 14s
Lint / lint (pull_request) Failing after 13s
2026-06-26 20:11:54 +05:30
f5a266ff70 feat: Implement the github release helper with github.sh 2026-06-26 19:57:47 +05:30
c42687a710 feat: Added registry helpers for installers
Some checks failed
Lint / lint (push) Failing after 13s
Lint / lint (pull_request) Failing after 13s
2026-06-26 18:36:39 +05:30
7f3ff45f05 refactor: Use jq instead of custom posix complient json.sh
Some checks failed
Lint / lint (push) Failing after 14s
Lint / lint (pull_request) Failing after 15s
While json.sh worked decently for reading json files, I didn't want to
implement writing to json files as well and make it completely
unreadable due to the added complexity.

So, I think its better to just use jq and keep things relatively simple
with the tradeoff of a lightweight dependency
2026-06-26 18:19:23 +05:30
780e79364f fix #9: Add validation check for pkg_remove
Some checks failed
Lint / lint (push) Failing after 3m14s
Lint / lint (pull_request) Failing after 17s
2026-06-25 22:40:13 +05:30
f158c4e913 feat(installers): Added lazygit installer 2026-06-25 22:21:26 +05:30
fc4303bc99 chore: Bumpt plugin supported version 2026-06-25 22:08:37 +05:30
6b0d07d70a ci: Added linting workflow using shellcheck 2026-06-25 22:03:50 +05:30
f8f41e4295 release: v2.2.0 2026-06-25 21:48:46 +05:30
a254001da8 feat(plugins): decouple runtime cache and add dynamic auto-generation 2026-06-25 21:48:28 +05:30
9a7404a65f feat(plugins): Added the official bootstrap plugin repository 2026-06-25 21:46:30 +05:30
b697fc5bba fix: Prevent unbounded parallel loop in multi file downloader 2026-06-25 21:26:34 +05:30
355588c7f9 refactor: Added new download_multiple_files_parallel helper function
- plugin.sh uses this to download manifests concurrently
2026-06-25 21:16:28 +05:30
d108f14ce5 refactor: Run plugin directly in memory in ephemeral mode 2026-06-25 19:41:50 +05:30
62a4759724 feat: Added ephemeral support for plugins 2026-06-25 19:38:01 +05:30
fdb2e108ee docs: Added plugins and development guide 2026-06-25 19:26:51 +05:30
9c86486ee6 feat: Added support for lazy loading plugins 2026-06-25 19:20:09 +05:30
33b98477bf feat: Added a custom json extractor 2026-06-25 19:18:13 +05:30
b813061e9a feat(installers): Added docker installer 2026-06-25 10:32:06 +05:30
7fe9ac913b refactor: bashrc is always sourced after tool install automatically
- Added source_bashrc funciton in shell_confi.sh
- Removed bashrc source commands from installer scripts
- Updated skill instructions
2026-06-25 09:53:03 +05:30
42 changed files with 1567 additions and 436 deletions

5
.agents/AGENTS.md Normal file
View File

@@ -0,0 +1,5 @@
# Bootstrap Project Rules
## Repository Cleanliness & Runtime Separation
- **No Repository Clutter**: Do not commit, track, or create runtime configuration, cache, or temporary files in the repository root.
- **Dynamic Initialization**: All runtime-generated files (such as `plugin_sources.txt`, `lib/plugin_cache.sh`, or local plugin downloads) must reside strictly under the user's active `$BOOTSTRAP_DIR` (e.g., `~/.config/bootstrap`). The CLI must auto-generate or initialize these files dynamically at runtime if they are missing, ensuring a zero-configuration out-of-the-box experience.

View File

@@ -59,7 +59,7 @@ The central router `lib/routes.sh` and autocomplete function in `b.sh` will dyna
### Step 3: 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 extracting binaries to `~/.local/bin/`, use `track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/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.
@@ -116,8 +116,8 @@ install_<name>() {
# Or manual downloads (always use download_file for resumability!):
# local url="https://..."
# download_file "$url" "$TMP_DIR/binary"
# cp "$TMP_DIR/binary" "$HOME/.local/bin/binary"
# track_file "$HOME/.local/bin/binary" # Important for rollback!
# cp "$TMP_DIR/binary" "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary"
# track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary" # Important for rollback!
}
# ─── Shell Configuration (if needed) ─────────────────────────────────
@@ -200,14 +200,13 @@ trap cleanup EXIT
### Distro-specific mapping
```bash
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "curl" "git"
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "git"
```
### Fetching latest GitHub release tag
```bash
local latest_tag=""
if has_command curl; then
latest_tag=$(curl -sL https://api.github.com/repos/<owner>/<repo>/releases/latest \
| grep '"tag_name":' | head -n1 \
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
@@ -246,3 +245,4 @@ track_file "/usr/local/bin/binary"
6. **Error handling**: Use `set -euo pipefail` after the guard block.
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.
8. **Clean Official Scripts**: When implementing official curl/install scripts provided in the prompt, strip them of bloat, macOS/Windows support, and redundant shell setups before writing the script.
9. **No manual shell re-sourcing**: Do NOT manually run `source ~/.bashrc` or print instructions asking the user to run it. Sourcing of the shell configuration is handled automatically by the central router and CLI at the end of the installation.

View File

@@ -25,12 +25,22 @@ When the user asks to "cut a release", "bump the version", or "tag a new version
- 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.
3. **Run the release script** non-interactively (if an actual bump is needed):
```bash
./scripts/release.sh --<level> -y
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:
feat: Resumable Download Helper and Manifest Preservation
Other Updates:
docs: Update readme
feat(skills): Add Installer to use rollback and savepoint hooks
```
(e.g., `./scripts/release.sh --minor -y`)
4. **Push** the tag and commit (ask for user confirmation before pushing):
4. **Run the release script** non-interactively, passing the compiled description:
```bash
./scripts/release.sh --<level> -y -m "<verbose description>"
```
5. **Push** the tag and commit (ask for user confirmation before pushing):
```bash
git push origin master <tag>
```

37
.gitea/workflows/lint.yml Normal file
View File

@@ -0,0 +1,37 @@
name: Lint
on:
push:
branches-ignore:
- main
- master
paths-ignore:
- '**.md'
- '.gitignore'
- 'docs/**'
pull_request:
branches:
- master
paths-ignore:
- '**.md'
- '.gitignore'
- 'docs/**'
jobs:
lint:
runs-on: production
steps:
- name: Install Dependencies
run: |
apt-get update
apt-get install -y git shellcheck
- name: Checkout Code
run: |
git config --global --add safe.directory '*'
rm -rf * .git || true
git clone --depth 1 $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git .
- name: Run ShellCheck
run: |
find . -type f -name '*.sh' | xargs shellcheck

31
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Lint
on:
push:
branches:
- main
- master
paths-ignore:
- '**.md'
- '.gitignore'
- 'docs/**'
pull_request:
branches:
- main
- master
paths-ignore:
- '**.md'
- '.gitignore'
- 'docs/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Run ShellCheck
run: |
sudo apt-get update && sudo apt-get install -y shellcheck
find . -type f -name '*.sh' | xargs shellcheck

View File

@@ -1 +1 @@
2.1.0
2.2.0

9
b.sh
View File

@@ -66,6 +66,15 @@ b() {
# Execute the routes file
bash "$routes_file" "$@"
local ret=$?
# Sourced again in the parent shell after successfully running the command
if [ $ret -eq 0 ]; then
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
return $ret
}
# Autocompletion for the b command in Bash

View File

@@ -8,11 +8,6 @@ if [ -z "${BASH_VERSION:-}" ]; then
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
echo "Error: curl is required to run this script." >&2
exit 1
fi
# Detect if the script is sourced
is_sourced=false
if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then
@@ -41,7 +36,7 @@ else
BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR"
_BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
_LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh")
_LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/plugins.sh" "lib/registry_helpers.sh" "lib/github.sh")
_curl_args=()
for _lib in "${_LIBS[@]}"; do
@@ -56,6 +51,8 @@ if [ -f "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" ]; then
. "$BOOTSTRAP_SOURCE_DIR/lib/rollback.sh"
. "$BOOTSTRAP_SOURCE_DIR/lib/platform.sh"
. "$BOOTSTRAP_SOURCE_DIR/lib/shell_config.sh"
. "$BOOTSTRAP_SOURCE_DIR/lib/registry_helpers.sh"
. "$BOOTSTRAP_SOURCE_DIR/lib/github.sh"
init_rollback_system
else
echo "Error: Failed to locate or download bootstrap libraries." >&2
@@ -68,10 +65,27 @@ install_bootstrap() {
[ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc")
local routes_dir="$HOME/.config/bootstrap"
mkdir -p "$routes_dir"
mkdir -p "$routes_dir/env.d"
mkdir -p "$routes_dir/aliases.d"
# Initialize XDG directories
mkdir -p "$HOME/.local/share/bootstrap/bin"
mkdir -p "$HOME/.local/share/bootstrap/opt"
mkdir -p "$HOME/.local/share/bootstrap/runtimes"
mkdir -p "$HOME/.local/state/bootstrap/logs"
mkdir -p "$HOME/.local/state/bootstrap/rollback"
mkdir -p "$HOME/.cache/bootstrap/downloads"
mkdir -p "$HOME/.cache/bootstrap/tmp"
# Create the universal binary PATH snippet
cat << 'EOF' > "$routes_dir/env.d/bootstrap-bin.sh"
export BOOTSTRAP_BIN="$BOOTSTRAP_BIN"
case ":$PATH:" in
*":$BOOTSTRAP_BIN:"*) ;;
*) export PATH="$BOOTSTRAP_BIN:$PATH" ;;
esac
EOF
# List of all files to download/copy
local files=(
"VERSION"
@@ -82,12 +96,20 @@ install_bootstrap() {
"lib/rollback.sh"
"lib/platform.sh"
"lib/shell_config.sh"
"lib/registry_helpers.sh"
"lib/github.sh"
"lib/plugins.sh"
"commands/help.sh"
"commands/con.sh"
"commands/uninstall.sh"
"commands/up.sh"
)
if ! pkg_check jq >/dev/null 2>&1; then
log_info "jq is missing. Installing jq..."
pkg_install jq
fi
if [ -f "$_SCRIPT_DIR/b.sh" ] && [ -f "$_SCRIPT_DIR/lib/routes.sh" ]; then
log_info "Using local files from repository..."
for file in "${files[@]}"; do
@@ -137,6 +159,13 @@ install_bootstrap() {
# >>> bootstrap-cli setup >>>
export BOOTSTRAP_DIR="$HOME/.config/bootstrap"
export BOOTSTRAP_DATA_DIR="$HOME/.local/share/bootstrap"
export BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap"
export BOOTSTRAP_CACHE_DIR="$HOME/.cache/bootstrap"
export BOOTSTRAP_BIN="$BOOTSTRAP_DATA_DIR/bin"
export BOOTSTRAP_OPT="$BOOTSTRAP_DATA_DIR/opt"
export BOOTSTRAP_RUNTIMES="$BOOTSTRAP_DATA_DIR/runtimes"
[ -f "$BOOTSTRAP_DIR/b.sh" ] && . "$BOOTSTRAP_DIR/b.sh"
for f in "$BOOTSTRAP_DIR/env.d/"*.sh; do [ -r "$f" ] && . "$f"; done
for f in "$BOOTSTRAP_DIR/aliases.d/"*.sh; do [ -r "$f" ] && . "$f"; done

View File

@@ -78,6 +78,9 @@ EOF
fi
# Remove the installation directory
rm -rf "$BOOTSTRAP_DATA_DIR"
rm -rf "$BOOTSTRAP_STATE_DIR"
rm -rf "$BOOTSTRAP_CACHE_DIR"
rm -rf "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
if [ "$FORCE" = "true" ]; then

View File

@@ -48,6 +48,13 @@ if version_lt "$local_ver" "$remote_ver" || [ "$force_update" = true ]; then
if bash "$tmp_bootstrap"; then
# Update the last update timestamp
date +%s > "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/.last_b_update" 2>/dev/null || true
# Update plugin cache
if [ -f "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib/plugins.sh" ]; then
. "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib/plugins.sh"
update_plugin_cache
fi
log_success "Bootstrap CLI successfully updated to version $remote_ver!"
else
log_error "Failed to execute bootstrap installer."

162
docs/client_spec_auth.md Normal file
View File

@@ -0,0 +1,162 @@
# Client Authentication and Provisioning Specification
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.
---
## The Authentication and Provisioning Flow
The complete flow consists of five sequential phases.
### 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.
### 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>
```
### 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.
---
## API Endpoints Reference
### 1. Register Device
- **Endpoint**: `POST /api/register`
- **Request Body**:
```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)**:
```json
{
"device_id": "uuid-string",
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..."
}
```
### 3. Approve Device
- **Endpoint**: `POST /api/approve`
- **Request Body**:
```json
{
"device_id": "uuid-string",
"admin_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...",
"signature": "hex-encoded-signature-bytes"
}
```
- **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)**:
```json
{
"status": "pending"
}
```
- **Response Body (200 OK - Approved & Encrypted)**:
```json
{
"status": "approved",
"payload": "-----BEGIN AGE ENCRYPTED FILE-----\n..."
}
```
---
## Bash Client Commands and Usage
### 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.
```bash
b me \
--server <server_url> \
[--key-dir <directory_to_save_keys>] \
[--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`).
#### 2. Request Approval
Retrieves a pending request by its user code, requests user confirmation, signs the approval payload, and sends it to the server.
```bash
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.

View File

@@ -0,0 +1,69 @@
# 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**.
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.
## 1. Writing a Plugin Script
A plugin is fundamentally a single Bash script. When executed by a user via `b <plugin_name>`, `bootstrap` runs the script in a subshell. This guarantees that any variables or state changes your plugin makes to the shell environment will not leak into the parent shell, preserving the integrity of the user's terminal.
Because plugins execute within the `bootstrap` context, you automatically have access to all internal library functions (e.g., `lib/common.sh`, `lib/platform.sh`). For example, you can safely use logging functions like `log_info`, `log_success`, and `log_error`.
Example `my_plugin.sh`:
```bash
#!/usr/bin/env bash
# My Awesome Plugin
# You can use bootstrap's built-in functions natively:
log_info "Initializing awesome plugin..."
if [ "${1:-}" == "--help" ]; then
echo "Usage: b my_plugin [args]"
exit 0
fi
log_success "Task completed successfully!"
```
## 2. Creating a Manifest
For your plugin to be discoverable and dynamically updatable by `bootstrap`, you must provide a JSON manifest. `bootstrap` uses a robust, native Bash-based JSON parser to read this manifest.
Create a JSON file (e.g., `plugins.json`) and host it publicly (e.g., as a GitHub raw URL).
Example `plugins.json`:
```json
{
"plugins": {
"my_plugin": {
"version": "1.0.0",
"url": "https://raw.githubusercontent.com/yourusername/repo/main/my_plugin.sh",
"bootstrap": "2.1.0",
"description": "An awesome plugin that prints logs"
}
}
}
```
* **`version`**: The current semantic version of your plugin. When `bootstrap` detects a version change during `b up`, it automatically clears the cached `.sh` file, forcing a lazy re-download on the next invocation.
* **`url`**: The raw, direct URL to your `.sh` plugin script.
* **`bootstrap`**: The latest version of `bootstrap` that this plugin has been tested against and is compatible with. If the user's `bootstrap` version is newer than this value, a warning is displayed notifying them of potential incompatibility.
## 3. Distribution
To let users install your plugin, simply provide them the raw URL to your JSON manifest.
Users will add it by running:
```bash
b plugin sources
```
They simply append your URL as a new line in the sources file. Once saved, `bootstrap` will automatically fetch your manifest and build a fast-lookup cache. The user can then immediately invoke your plugin:
```bash
b my_plugin
```

View File

@@ -2,21 +2,16 @@
# Tool: agy
# DisplayName: Antigravity
# Description: Install Antigravity CLI
# Strategy: binary
#
# Antigravity CLI Installer Script (Linux Only)
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
# Constants
DOWNLOAD_BASE_URL="https://antigravity-cli-auto-updater-974169037036.us-central1.run.app"
TARGET_DIR="$HOME/.local/bin"
TARGET_DIR="$BOOTSTRAP_BIN"
BINARY_PATH="$TARGET_DIR/agy"
install_agy() {
@@ -55,19 +50,12 @@ install_agy() {
exit 1
fi
# POSIX-compliant JSON parser (no jq dependencies)
parse_json_key() {
local payload="$1"
local key="$2"
echo "$payload" | sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
}
local version
local url
local sha512
version=$(parse_json_key "$manifest_json" "version")
url=$(parse_json_key "$manifest_json" "url")
sha512=$(parse_json_key "$manifest_json" "sha512")
version=$(echo "$manifest_json" | jq -r '.version // empty')
url=$(echo "$manifest_json" | jq -r '.url // empty')
sha512=$(echo "$manifest_json" | jq -r '.sha512 // empty')
if [ -z "$url" ] || [ -z "$sha512" ]; then
log_error "Failed to parse release manifest."
@@ -134,16 +122,11 @@ install_agy() {
track_file "$BINARY_PATH"
log_success "Antigravity CLI successfully installed to $BINARY_PATH."
register_tool "agy" "binary" "" "github:sortedcord/agy"
}
configure_shell() {
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "local-bin path"
done
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
}
run_handoff() {

View File

@@ -2,16 +2,11 @@
# Tool: asciicinema
# DisplayName: asciicinema
# Description: Install asciinema terminal recorder
# Strategy: binary
#
# asciinema Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -21,12 +16,9 @@ cleanup() {
trap cleanup EXIT
install_asciicinema() {
local latest_tag=""
if has_command curl; then
log_info "Fetching latest asciinema version from GitHub..."
latest_tag=$(curl -sL https://api.github.com/repos/asciinema/asciinema/releases/latest \
| grep '"tag_name":' | head -n1 \
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
latest_tag=$(github_get_latest_release "asciinema/asciinema")
fi
if [ -z "$latest_tag" ]; then
@@ -73,22 +65,21 @@ install_asciicinema() {
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
esac
local download_url="https://github.com/asciinema/asciinema/releases/download/${latest_tag}/asciinema-${asciinema_arch}"
log_info "Downloading asciinema ${latest_tag} for ${arch}..."
download_file "$download_url" "$TMP_DIR/asciinema"
github_download_asset "asciinema/asciinema" "$latest_tag" "asciinema-${asciinema_arch}" "$TMP_DIR/asciinema"
log_info "Installing asciinema to /usr/local/bin..."
sudo cp "$TMP_DIR/asciinema" /usr/local/bin/asciinema
sudo chmod +x /usr/local/bin/asciinema
track_file "/usr/local/bin/asciinema"
log_info "Installing asciinema to $BOOTSTRAP_BIN..."
cp "$TMP_DIR/asciinema" "$BOOTSTRAP_BIN/asciinema"
chmod +x "$BOOTSTRAP_BIN/asciinema"
track_file "$BOOTSTRAP_BIN/asciinema"
# Create compatibility symlink matching the installer name spelling
log_info "Creating compatibility symlink for asciicinema..."
sudo ln -sf /usr/local/bin/asciinema /usr/local/bin/asciicinema
ln -sf "$BOOTSTRAP_BIN/asciinema" /usr/local/bin/asciicinema
track_file "/usr/local/bin/asciicinema"
log_success "asciinema ${latest_tag} installed."
register_tool "asciicinema" "binary" "$latest_tag" "github:asciinema/asciinema"
}
main() {

View File

@@ -2,16 +2,11 @@
# Tool: bat
# DisplayName: Bat
# Description: Install Bat (alternative to cat) and configure alias
# Strategy: binary
#
# Bat Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -21,69 +16,54 @@ cleanup() {
trap cleanup EXIT
install_bat() {
local distro
distro=$(detect_distro)
if [ "$distro" = "arch" ]; then
log_info "Arch Linux detected"
log_info "Installing Bat..."
pkg_install bat
elif [ "$distro" = "fedora" ]; then
log_info "Fedora detected"
log_info "Installing Bat..."
pkg_install bat
elif [ "$distro" = "debian" ]; then
log_info "Debian/Ubuntu detected"
pkg_install curl
log_info "Fetching latest Bat version from GitHub..."
local latest_tag=""
latest_tag=$(curl -sL https://api.github.com/repos/sharkdp/bat/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
if [ -z "$latest_tag" ]; then
latest_tag="v0.26.1"
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
else
log_info "Latest Bat version found: $latest_tag"
if has_command bat; then
if ! confirm "Bat is already installed. Reinstall/Upgrade?"; then
log_info "Skipping Bat installation."
return
fi
# Remove leading 'v' for file name version
local version="${latest_tag#v}"
# Detect architecture mapping
local arch
arch=$(detect_arch)
local deb_arch="amd64"
if [ "$arch" = "arm64" ]; then
deb_arch="arm64"
fi
local deb_url="https://github.com/sharkdp/bat/releases/download/${latest_tag}/bat_${version}_${deb_arch}.deb"
log_info "Downloading Bat from ${deb_url}..."
download_file "$deb_url" "$TMP_DIR/bat.deb"
log_info "Installing Bat package..."
sudo apt install -y "$TMP_DIR/bat.deb"
add_rollback_cmd "sudo apt remove -y bat"
else
log_error "Unsupported distribution."
exit 1
fi
local arch
arch=$(detect_arch)
local target=""
case "$arch" in
x86_64) target="x86_64-unknown-linux-gnu" ;;
arm64) target="aarch64-unknown-linux-gnu" ;;
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
esac
log_info "Fetching latest Bat version from GitHub..."
local latest_tag=""
latest_tag=$(github_get_latest_release "sharkdp/bat")
if [ -z "$latest_tag" ]; then
latest_tag="v0.26.1"
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
else
log_info "Latest Bat version found: $latest_tag"
fi
log_info "Downloading Bat ${latest_tag}..."
local archive="$TMP_DIR/bat.tar.gz"
github_download_asset "sharkdp/bat" "$latest_tag" "bat-${latest_tag}-${target}\.tar\.gz" "$archive"
log_info "Extracting Bat binary..."
tar -xzf "$archive" -C "$TMP_DIR"
local extract_dir="$TMP_DIR/bat-${latest_tag}-${target}"
local target_dir="$BOOTSTRAP_BIN"
mkdir -p "$target_dir"
log_info "Installing Bat to $target_dir/bat..."
cp "$extract_dir/bat" "$target_dir/bat"
chmod +x "$target_dir/bat"
track_file "$target_dir/bat"
register_tool "bat" "binary" "$latest_tag" "github:sharkdp/bat"
}
configure_shell() {
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "bat alias"
done
if [ -f "$HOME/.bash_aliases" ]; then
remove_block "$HOME/.bash_aliases" "bat alias"
fi
write_alias_snippet "bat" "alias cat='bat --paging=never -p'"
}
@@ -94,7 +74,6 @@ main() {
echo
log_success "Bat installation and configuration complete."
log_info "Please close and reopen your terminal or run: source ~/.bashrc to apply changes."
}
main "$@"

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Tool: docker
# DisplayName: Docker
# Description: Container runtime and orchestration platform
# Strategy: system
#
# Docker Installer Script
#
set -euo pipefail
# ─── Installation Logic ──────────────────────────────────────────────
install_docker() {
if has_command docker; then
if ! confirm "Docker is already installed. Reinstall/Upgrade?"; then
log_info "Skipping Docker installation."
return
fi
else
if ! confirm "Install Docker?"; then
log_info "Skipping Docker installation."
return
fi
fi
# Use pkg_install for distro packages (it automatically handles rollback hooks for the packages!)
pkg_install "arch:docker|debian:docker.io|fedora:docker"
# Ensure docker group exists (some distros might not create it immediately)
if ! getent group docker >/dev/null 2>&1; then
log_info "Creating docker group..."
add_rollback_cmd "sudo groupdel docker || true"
sudo groupadd docker
fi
# Configure user group
if ! groups "$USER" | grep -q "\bdocker\b"; then
log_info "Adding $USER to the docker group..."
add_rollback_cmd "sudo gpasswd -d $USER docker || true"
sudo usermod -aG docker "$USER"
log_warn "You will need to log out and log back in, or run 'newgrp docker' for the group changes to take effect."
fi
# Enable and start systemd services
if has_command systemctl; then
log_info "Enabling and starting Docker services..."
# Add rollback cmds for systemd
add_rollback_cmd "sudo systemctl disable --now docker.service || true"
add_rollback_cmd "sudo systemctl disable --now containerd.service || true"
sudo systemctl enable --now docker.service || true
sudo systemctl enable --now containerd.service || true
fi
register_tool "docker" "system" "" "os-package-manager"
}
# ─── Main ─────────────────────────────────────────────────────────────
main() {
install_docker
echo
log_success "Docker installation and configuration complete."
}
main "$@"

71
installers/install_lazygit.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Tool: lazygit
# DisplayName: lazygit
# Description: Simple terminal UI for git commands
# Strategy: binary
#
# lazygit Installer Script
#
set -euo pipefail
# ─── Installation Logic ──────────────────────────────────────────────
install_lazygit() {
if has_command lazygit; then
if ! confirm "lazygit is already installed. Reinstall/Upgrade?"; then
log_info "Skipping lazygit installation."
return
fi
else
if ! confirm "Install lazygit?"; then
log_info "Skipping lazygit installation."
return
fi
fi
local latest_tag=""
latest_tag=$(github_get_latest_release "jesseduffield/lazygit")
if [ -z "$latest_tag" ]; then
latest_tag="v0.62.2" # fallback
log_warn "Failed to fetch latest version. Falling back to: $latest_tag"
fi
local version="${latest_tag#v}"
local arch=$(detect_arch)
local arch_str="x86_64"
if [ "$arch" = "arm64" ]; then
arch_str="arm64"
fi
TMP_DIR="$(make_temp_dir)"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT
local dest="$TMP_DIR/lazygit.tar.gz"
log_info "Downloading lazygit ${latest_tag}..."
github_download_asset "jesseduffield/lazygit" "$latest_tag" "lazygit_${version}_linux_${arch_str}\.tar\.gz" "$dest"
log_info "Extracting..."
tar -xzf "$dest" -C "$TMP_DIR"
mkdir -p "$BOOTSTRAP_BIN"
cp "$TMP_DIR/lazygit" "$HOME/.local/bin/lazygit"
chmod +x "$HOME/.local/bin/lazygit"
track_file "$HOME/.local/bin/lazygit"
register_tool "lazygit" "binary" "$latest_tag" "github:jesseduffield/lazygit"
}
# ─── Main ─────────────────────────────────────────────────────────────
main() {
install_lazygit
echo
log_success "lazygit installation complete."
}
main "$@"

View File

@@ -2,16 +2,11 @@
# Tool: node
# DisplayName: Node
# Description: Install Node.js (LTS) and NVM
# Strategy: managed
#
# Node.js and NVM Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -21,7 +16,7 @@ cleanup() {
trap cleanup EXIT
install_nvm() {
if has_command nvm || [ -s "$HOME/.nvm/nvm.sh" ]; then
if has_command nvm || [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then
log_info "NVM is already installed."
fi
@@ -34,7 +29,7 @@ install_nvm() {
# Try to fetch the latest version of NVM from GitHub API
log_info "Fetching the latest NVM version..."
local latest_tag=""
latest_tag=$(curl -sL https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
latest_tag=$(github_get_latest_release "nvm-sh/nvm")
if [ -z "$latest_tag" ]; then
latest_tag="v0.40.5" # Fallback version if API request fails
@@ -47,25 +42,20 @@ install_nvm() {
log_info "Downloading NVM from $nvm_url..."
download_file "$nvm_url" "$TMP_DIR/nvm.tar.gz"
log_info "Extracting NVM archive directly to $HOME/.nvm (stripping versioned subfolder to keep config generic)..."
mkdir -p "$HOME/.nvm"
tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
log_info "Extracting NVM archive directly to $BOOTSTRAP_RUNTIMES/nvm (stripping versioned subfolder to keep config generic)..."
mkdir -p "$BOOTSTRAP_RUNTIMES/nvm"
tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$BOOTSTRAP_RUNTIMES/nvm" --strip-components=1
track_dir "$HOME/.nvm"
track_dir "$BOOTSTRAP_RUNTIMES/nvm"
log_success "NVM source files successfully extracted to $HOME/.nvm."
log_success "NVM source files successfully extracted to $BOOTSTRAP_RUNTIMES/nvm."
}
configure_shell() {
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "nvm setup"
done
local content
content=$(cat << 'EOF'
export NVM_DIR="$HOME/.nvm"
export NVM_DIR="$BOOTSTRAP_RUNTIMES/nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load NVM
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # Load NVM bash completion
EOF
@@ -76,10 +66,10 @@ EOF
install_node() {
# Ensure NVM is loaded in this script context
if [ -s "$HOME/.nvm/nvm.sh" ]; then
if [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then
# Temporarily disable nounset as nvm.sh does not support set -u
set +u
. "$HOME/.nvm/nvm.sh"
. "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh"
else
log_error "Could not load NVM to install Node.js."
return 1
@@ -95,6 +85,7 @@ install_node() {
nvm alias default 'lts/*'
log_success "Node.js installed successfully!"
set -u
register_tool "node" "managed" "$latest_tag" "github:nvm-sh/nvm"
}
main() {
@@ -106,10 +97,9 @@ main() {
if has_command node; then
log_success "Node.js (via NVM) installation and configuration complete."
log_info "Installed Node version: $(node --version)"
log_info "Installed NVM version: $(nvm --version 2>/dev/null || cat "$HOME/.nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")"
log_info "Installed NVM version: $(nvm --version 2>/dev/null || cat "$BOOTSTRAP_RUNTIMES/nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")"
else
log_success "Installation complete."
log_info "Please close and reopen your terminal or run: source ~/.bashrc to verify."
fi
}

View File

@@ -2,20 +2,16 @@
# Tool: nvim
# DisplayName: Neovim
# Description: Install Neovim 0.12.0 and configuration
# Strategy: binary
#
# Neovim Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
NVIM_VERSION="0.12.0"
NVIM_INSTALL_DIR="/opt/nvim"
NVIM_INSTALL_DIR="$BOOTSTRAP_OPT/nvim"
NVIM_BIN_DIR="$BOOTSTRAP_BIN"
NVIM_CONFIG_REPO="https://git.adityagupta.dev/sortedcord/editor.git"
NVIM_CONFIG_DIR="$HOME/.config/nvim"
@@ -33,7 +29,7 @@ check_config_dir() {
install_packages() {
log_info "Detecting distribution and installing dependencies..."
pkg_install \
git tar curl unzip ripgrep fzf nodejs npm xclip wl-clipboard \
git tar unzip ripgrep fzf nodejs npm xclip wl-clipboard \
"arch:fd|debian:fd-find|fedora:fd-find" \
"arch:cmake|debian:cmake|fedora:cmake" \
"arch:make|debian:build-essential|fedora:make" \
@@ -76,23 +72,23 @@ install_nvim() {
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
esac
local nvim_url="https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-${nvim_arch}.tar.gz"
log_info "Downloading Neovim v${NVIM_VERSION} for ${arch}..."
download_file "$nvim_url" "$TMP_DIR/nvim.tar.gz"
github_download_asset "neovim/neovim" "v${NVIM_VERSION}" "nvim-${nvim_arch}\.tar\.gz" "$TMP_DIR/nvim.tar.gz"
tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR"
sudo rm -rf "$NVIM_INSTALL_DIR"
sudo mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
rm -rf "$NVIM_INSTALL_DIR"
mkdir -p "$(dirname "$NVIM_INSTALL_DIR")"
mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
sudo ln -sf "$NVIM_INSTALL_DIR/bin/nvim" /usr/local/bin/nvim
ln -sf "$NVIM_INSTALL_DIR/bin/nvim" "$NVIM_BIN_DIR/nvim"
track_dir "$NVIM_INSTALL_DIR"
track_file "/usr/local/bin/nvim"
track_file "$NVIM_BIN_DIR/nvim"
log_success "Installed:"
nvim --version | head -n1
register_tool "nvim" "binary" "$NVIM_VERSION" "github:neovim/neovim"
}
install_config() {
@@ -111,24 +107,6 @@ install_config() {
}
configure_shell() {
# Clean up legacy inline edits from bashrc and bash_aliases
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
if [ -f "$config_file" ]; then
local tmp_file
tmp_file=$(mktemp)
sed '/^export EDITOR="nvim"/d' "$config_file" > "$tmp_file"
cat "$tmp_file" > "$config_file"
rm -f "$tmp_file"
fi
done
if [ -f "$HOME/.bash_aliases" ]; then
local tmp_file
tmp_file=$(mktemp)
sed '/^alias vim="nvim"/d' "$HOME/.bash_aliases" > "$tmp_file"
cat "$tmp_file" > "$HOME/.bash_aliases"
rm -f "$tmp_file"
fi
write_alias_snippet "nvim" 'alias vim="nvim"'
write_env_snippet "nvim" 'export EDITOR="nvim"'

View File

@@ -2,6 +2,7 @@
# Tool: pnpm
# DisplayName: Pnpm
# Description: Install pnpm package manager
# Strategy: binary
#
# pnpm Installer Script
#
@@ -17,12 +18,6 @@
# curl -fsSL https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -127,14 +122,17 @@ install_pnpm() {
}
libc_suffix="$(detect_libc_suffix)"
# Fetch the latest version from the npm registry, or use PNPM_VERSION if set
# Fetch the latest version from GitHub, or use PNPM_VERSION if set
if [ -z "${PNPM_VERSION:-}" ]; then
log_info "Fetching latest pnpm version from npm registry..."
version_json="$(download "https://registry.npmjs.org/@pnpm/exe")" || {
log_error "Failed to fetch pnpm version info from npm registry."
log_info "Fetching latest pnpm version from GitHub..."
local tag
tag=$(github_get_latest_release "pnpm/pnpm")
if [ -n "$tag" ]; then
version="${tag#v}"
else
log_error "Failed to fetch pnpm version info from GitHub."
return 1
}
version="$(echo "$version_json" | grep -o '"latest":[[:space:]]*"[0-9.]*"' | grep -o '[0-9.]*')"
fi
else
version="${PNPM_VERSION}"
fi
@@ -151,7 +149,7 @@ install_pnpm() {
if [ "$major_version" -ge 11 ]; then
# v11+: distributed as tarballs containing the binary and dist/ directory
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}.tar.gz" "$TMP_DIR/pnpm.tar.gz" || {
github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}\.tar\.gz" "$TMP_DIR/pnpm.tar.gz" || {
log_error "Failed to download pnpm tarball."
return 1
}
@@ -166,7 +164,7 @@ install_pnpm() {
}
else
# Older versions: distributed as a single executable binary
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}" "$TMP_DIR/pnpm" || {
github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}" "$TMP_DIR/pnpm" || {
log_error "Failed to download pnpm binary."
return 1
}
@@ -179,23 +177,19 @@ install_pnpm() {
track_dir "$HOME/.local/share/pnpm"
log_success "pnpm v${version} installed successfully!"
register_tool "pnpm" "binary" "$version" "github:pnpm/pnpm"
}
# ─── Shell Configuration ─────────────────────────────────────────────
configure_shell() {
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "pnpm setup"
done
# pnpm's `setup --force` configures PNPM_HOME and PATH automatically,
# but we also add an env block to ensure PNPM_HOME is set consistently.
local content
content=$(cat << 'EOF'
# pnpm
export PNPM_HOME="$HOME/.local/share/pnpm"
export PNPM_HOME="$BOOTSTRAP_RUNTIMES/pnpm"
case ":$PATH:" in
*":$PNPM_HOME:"*) ;;
*) export PATH="$PNPM_HOME:$PATH" ;;
@@ -218,7 +212,6 @@ main() {
log_info "Installed pnpm version: $(pnpm --version 2>/dev/null || echo 'unknown')"
else
log_success "Installation complete."
log_info "Please close and reopen your terminal or run: source ~/.bashrc to verify."
fi
}

View File

@@ -2,16 +2,11 @@
# Tool: rust
# DisplayName: Rust
# Description: Install Rustup and Rust compiler/toolchain
# Strategy: managed
#
# Rust Installer Script (Simplified Local Rustup Init)
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -20,14 +15,6 @@ cleanup() {
}
trap cleanup EXIT
# Ensure we have curl
install_downloader() {
if ! has_command curl; then
log_info "curl not found. Installing curl..."
pkg_install curl
fi
}
detect_target_triple() {
local ostype
ostype="$(uname -s)"
@@ -61,11 +48,14 @@ detect_target_triple() {
}
install_rust() {
if has_command rustup || [ -f "$HOME/.cargo/bin/rustup" ]; then
export CARGO_HOME="$BOOTSTRAP_RUNTIMES/cargo"
export RUSTUP_HOME="$BOOTSTRAP_RUNTIMES/rustup"
if has_command rustup || [ -f "$BOOTSTRAP_RUNTIMES/cargo/bin/rustup" ]; then
log_info "Rust (rustup) is already installed."
fi
install_downloader
local target
target=$(detect_target_triple)
@@ -87,19 +77,19 @@ install_rust() {
"$dest" -y --no-modify-path
add_rollback_cmd "rustup self uninstall -y"
register_tool "rust" "managed" "" "rustup"
}
configure_shell() {
# Add ~/.cargo/bin to PATH for the current process
export PATH="$HOME/.cargo/bin:$PATH"
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "rust init"
done
write_env_snippet "rust" '. "$HOME/.cargo/env"'
local snippet_content=$(cat << 'EOF'
export CARGO_HOME="$BOOTSTRAP_RUNTIMES/cargo"
export RUSTUP_HOME="$BOOTSTRAP_RUNTIMES/rustup"
. "$CARGO_HOME/env"
EOF
)
write_env_snippet "rust" "$snippet_content"
}
main() {

View File

@@ -2,16 +2,11 @@
# Tool: starship
# DisplayName: Starship
# Description: Install Starship shell prompt
# Strategy: binary
#
# Starship Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -25,12 +20,6 @@ install_starship() {
log_info "Starship is already installed."
fi
# Ensure curl is installed
if ! has_command curl; then
log_info "curl not found. Installing curl..."
pkg_install curl
fi
# Detect architecture
local raw_arch
raw_arch=$(detect_arch)
@@ -45,47 +34,33 @@ install_starship() {
log_info "Fetching latest Starship version from GitHub..."
local latest_tag=""
latest_tag=$(curl -sL https://api.github.com/repos/starship/starship/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
local download_url
if [ -n "$latest_tag" ]; then
log_info "Latest Starship version found: $latest_tag"
download_url="https://github.com/starship/starship/releases/download/${latest_tag}/starship-${target}.tar.gz"
else
latest_tag=$(github_get_latest_release "starship/starship")
if [ -z "$latest_tag" ]; then
latest_tag="latest"
log_warn "Failed to fetch latest version from GitHub. Falling back to downloading latest release directly."
download_url="https://github.com/starship/starship/releases/latest/download/starship-${target}.tar.gz"
fi
log_info "Downloading Starship from ${download_url}..."
log_info "Downloading Starship ${latest_tag}..."
local archive="$TMP_DIR/starship.tar.gz"
download_file "$download_url" "$archive"
github_download_asset "starship/starship" "$latest_tag" "starship-${target}\.tar\.gz" "$archive"
# Extract the binary
log_info "Extracting Starship binary..."
tar -xzf "$archive" -C "$TMP_DIR"
# Install to ~/.local/bin
local target_dir="$HOME/.local/bin"
local target_dir="$BOOTSTRAP_BIN"
mkdir -p "$target_dir"
log_info "Installing Starship to $target_dir/starship..."
cp "$TMP_DIR/starship" "$target_dir/starship"
chmod +x "$target_dir/starship"
track_file "$target_dir/starship"
register_tool "starship" "binary" "$latest_tag" "github:starship/starship"
}
configure_shell() {
# Add ~/.local/bin to PATH for the current process
export PATH="$HOME/.local/bin:$PATH"
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "local-bin path"
remove_block "$config_file" "starship init"
done
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
write_env_snippet "starship" 'eval "$(starship init bash)"'
}

View File

@@ -2,16 +2,11 @@
# Tool: uv
# DisplayName: uv
# Description: Fast Python package installer and resolver
# Strategy: binary
#
# uv Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -28,12 +23,6 @@ install_uv() {
fi
fi
# Ensure curl is installed
if ! has_command curl; then
log_info "curl not found. Installing curl..."
pkg_install curl
fi
# Detect architecture
local raw_arch
raw_arch=$(detect_arch)
@@ -54,28 +43,22 @@ install_uv() {
log_info "Fetching latest uv version from GitHub..."
local latest_tag=""
latest_tag=$(curl -sL https://api.github.com/repos/astral-sh/uv/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
latest_tag=$(github_get_latest_release "astral-sh/uv")
local download_url
if [ -n "$latest_tag" ]; then
log_info "Latest uv version found: $latest_tag"
download_url="https://github.com/astral-sh/uv/releases/download/${latest_tag}/uv-${target}.tar.gz"
else
if [ -z "$latest_tag" ]; then
latest_tag="latest"
log_warn "Failed to fetch latest version from GitHub. Falling back to downloading latest release directly."
download_url="https://github.com/astral-sh/uv/releases/latest/download/uv-${target}.tar.gz"
fi
log_info "Downloading uv from ${download_url}..."
log_info "Downloading uv ${latest_tag}..."
local archive="$TMP_DIR/uv.tar.gz"
download_file "$download_url" "$archive"
github_download_asset "astral-sh/uv" "$latest_tag" "uv-${target}\.tar\.gz" "$archive"
# Extract the binaries
log_info "Extracting uv binaries..."
tar -xzf "$archive" --strip-components 1 -C "$TMP_DIR"
# Install to ~/.local/bin
local target_dir="$HOME/.local/bin"
local target_dir="$BOOTSTRAP_BIN"
mkdir -p "$target_dir"
log_info "Installing uv and uvx to $target_dir..."
cp "$TMP_DIR/uv" "$target_dir/uv"
@@ -83,20 +66,12 @@ install_uv() {
chmod +x "$target_dir/uv" "$target_dir/uvx"
track_file "$target_dir/uv"
track_file "$target_dir/uvx"
register_tool "uv" "binary" "$latest_tag" "github:astral-sh/uv"
}
configure_shell() {
# Add ~/.local/bin to PATH for the current process
export PATH="$HOME/.local/bin:$PATH"
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "local-bin path"
remove_block "$config_file" "uv completion"
done
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
write_env_snippet "uv" 'eval "$(uv generate-shell-completion bash)"'
}

View File

@@ -2,16 +2,11 @@
# Tool: yay
# DisplayName: Yay
# Description: Install Yay AUR helper
# Strategy: system
#
# Yay Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
# ─── Installation Logic ──────────────────────────────────────────────
@@ -66,6 +61,7 @@ install_yay() {
cd "$orig_dir"
log_info "Cleaning up installer directory..."
rm -rf "$clone_dir"
register_tool "yay" "system" "" "aur:yay-bin"
}
# ─── Main ─────────────────────────────────────────────────────────────

View File

@@ -2,16 +2,11 @@
# Tool: yazi
# DisplayName: Yazi
# Description: Install Yazi terminal file manager and dependencies
# Strategy: binary
#
# Yazi Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
TMP_DIR="$(make_temp_dir)"
@@ -21,11 +16,6 @@ cleanup() {
trap cleanup EXIT
add_y_wrapper() {
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "yazi wrapper"
done
local wrapper_content
wrapper_content=$(cat << 'EOF'
@@ -40,77 +30,68 @@ y() {
}
EOF
)
write_alias_snippet "yazi" "$wrapper_content"
}
install_yazi() {
local distro
distro=$(detect_distro)
if [ "$distro" = "arch" ]; then
log_info "Arch Linux detected"
if has_command yazi; then
log_info "Yazi is already installed."
if has_command yazi; then
if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then
log_info "Skipping Yazi installation."
return
fi
log_info "Installing Yazi..."
pkg_install yazi
log_info "Installing dependencies subsequently..."
pkg_install ffmpeg 7zip jq poppler fd ripgrep fzf zoxide resvg imagemagick
elif [ "$distro" = "debian" ]; then
log_info "Debian/Ubuntu detected"
if has_command yazi; then
log_info "Yazi is already installed."
fi
pkg_install curl git
log_info "Fetching latest Yazi version from GitHub..."
local latest_tag
latest_tag=$(curl -sL https://api.github.com/repos/sxyazi/yazi/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
if [ -z "$latest_tag" ]; then
latest_tag="v26.5.6"
fi
local deb_url="https://github.com/sxyazi/yazi/releases/download/${latest_tag}/yazi-x86_64-unknown-linux-gnu.deb"
log_info "Downloading Yazi ${latest_tag} from ${deb_url}..."
download_file "$deb_url" "$TMP_DIR/yazi.deb"
log_info "Installing Yazi package..."
sudo apt install -y "$TMP_DIR/yazi.deb"
add_rollback_cmd "sudo apt remove -y yazi"
log_info "Installing dependencies subsequently..."
pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick 7zip || \
pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick p7zip-full
create_fd_symlink
elif [ "$distro" = "fedora" ]; then
log_info "Fedora detected"
if has_command yazi; then
log_info "Yazi is already installed."
fi
log_info "Installing dnf-plugins-core..."
pkg_install dnf-plugins-core
log_info "Enabling lihaohong/yazi copr repo..."
sudo dnf copr enable -y lihaohong/yazi
log_info "Installing Yazi (without weak dependencies first)..."
sudo dnf install -y yazi --setopt=install_weak_deps=False
add_rollback_cmd "sudo dnf remove -y yazi"
log_info "Installing weak dependencies subsequently..."
pkg_install yazi
else
log_error "Unsupported distribution."
exit 1
fi
# Ensure required extraction tools are installed
if ! has_command unzip; then
log_info "unzip not found. Installing unzip..."
pkg_install unzip
fi
local arch
arch=$(detect_arch)
local target=""
case "$arch" in
x86_64) target="x86_64-unknown-linux-gnu" ;;
arm64) target="aarch64-unknown-linux-gnu" ;;
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
esac
log_info "Fetching latest Yazi version from GitHub..."
local latest_tag=""
latest_tag=$(github_get_latest_release "sxyazi/yazi")
if [ -z "$latest_tag" ]; then
latest_tag="v0.3.3"
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
fi
log_info "Downloading Yazi ${latest_tag}..."
local archive="$TMP_DIR/yazi.zip"
github_download_asset "sxyazi/yazi" "$latest_tag" "yazi-${target}\.zip" "$archive"
log_info "Extracting Yazi binaries..."
unzip -q "$archive" -d "$TMP_DIR"
local extract_dir="$TMP_DIR/yazi-${target}"
local target_dir="$BOOTSTRAP_BIN"
mkdir -p "$target_dir"
log_info "Installing Yazi to $target_dir..."
cp "$extract_dir/yazi" "$target_dir/yazi"
cp "$extract_dir/ya" "$target_dir/ya"
chmod +x "$target_dir/yazi" "$target_dir/ya"
track_file "$target_dir/yazi"
track_file "$target_dir/ya"
log_info "Installing system dependencies for Yazi..."
pkg_install ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find"
create_fd_symlink
register_tool "yazi" "binary" "$latest_tag" "github:sxyazi/yazi"
# Add the system dependencies to the registry for uninstallation tracking
registry_add_sys_deps "yazi" ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find"
}
main() {

View File

@@ -2,24 +2,13 @@
# Tool: zoxide
# DisplayName: Zoxide
# Description: Install Zoxide directory jumper
# Strategy: managed
#
# Zoxide Installer Script
#
# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi
set -euo pipefail
install_curl() {
if ! has_command curl; then
log_info "curl not found. Installing curl..."
pkg_install curl
fi
}
install_fzf() {
if has_command fzf; then
@@ -36,24 +25,17 @@ install_zoxide() {
log_info "Zoxide is already installed."
fi
install_curl
log_info "Downloading and running the official zoxide installer..."
curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh
track_file "$HOME/.local/bin/zoxide"
register_tool "zoxide" "managed" "" "github:ajeetdsouza/zoxide"
}
configure_shell() {
# Add ~/.local/bin to PATH for the current process
export PATH="$HOME/.local/bin:$PATH"
# Clean up legacy in-place configuration blocks
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
for config_file in "${target_files[@]}"; do
remove_block "$config_file" "zoxide init"
done
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
write_env_snippet "zoxide" 'eval "$(zoxide init --cmd cd bash)"'
}

View File

@@ -7,6 +7,15 @@ if [ -n "${_LIB_COMMON_SOURCED:-}" ]; then
fi
_LIB_COMMON_SOURCED=1
# Export global environment paths with default fallbacks
export BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
export BOOTSTRAP_DATA_DIR="${BOOTSTRAP_DATA_DIR:-$HOME/.local/share/bootstrap}"
export BOOTSTRAP_STATE_DIR="${BOOTSTRAP_STATE_DIR:-$HOME/.local/state/bootstrap}"
export BOOTSTRAP_CACHE_DIR="${BOOTSTRAP_CACHE_DIR:-$HOME/.cache/bootstrap}"
export BOOTSTRAP_BIN="${BOOTSTRAP_BIN:-$BOOTSTRAP_DATA_DIR/bin}"
export BOOTSTRAP_OPT="${BOOTSTRAP_OPT:-$BOOTSTRAP_DATA_DIR/opt}"
export BOOTSTRAP_RUNTIMES="${BOOTSTRAP_RUNTIMES:-$BOOTSTRAP_DATA_DIR/runtimes}"
# Ensure running in Bash
require_bash() {
if [ -z "${BASH_VERSION:-}" ]; then
@@ -88,7 +97,7 @@ version_lt() {
download_file() {
local url="$1"
local dest="$2"
local cache_dir="$HOME/.local/state/bootstrap/cache"
local cache_dir="$BOOTSTRAP_CACHE_DIR/downloads"
mkdir -p "$cache_dir"
@@ -123,7 +132,50 @@ download_file() {
cp "$cache_file" "$dest"
}
# Helper to download multiple files in parallel using background jobs (batched to 10 at a time)
download_multiple_files_parallel() {
# Usage: download_multiple_files_parallel url1 dest1 [url2 dest2 ...]
local urls=()
local dests=()
local exit_code=0
while [ $# -ge 2 ]; do
urls+=("$1")
dests+=("$2")
shift 2
done
local total=${#urls[@]}
local batch_size=10
for ((i=0; i<total; i+=batch_size)); do
local pids=()
local batch_urls=()
# Start up to batch_size background jobs
for ((j=i; j<i+batch_size && j<total; j++)); do
local url="${urls[$j]}"
local dest="${dests[$j]}"
mkdir -p "$(dirname "$dest")" 2>/dev/null || true
curl -fsSL "$url" -o "$dest" &
pids+=($!)
batch_urls+=("$url")
done
# Wait for all background jobs in the current batch to finish
for ((j=0; j<${#pids[@]}; j++)); do
if ! wait "${pids[$j]}"; then
log_warn "Failed to download from ${batch_urls[$j]}"
exit_code=1
fi
done
done
return $exit_code
}
# Export functions and variables for subshells
export _LIB_COMMON_SOURCED=1
export RED GREEN YELLOW BLUE NC
export -f require_bash log_info log_success log_warn log_error confirm has_command make_temp_dir version_lt download_file
export -f require_bash log_info log_success log_warn log_error confirm has_command make_temp_dir version_lt download_file download_multiple_files_parallel

58
lib/github.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# GitHub API helper functions for Bootstrap installers
# Usage: github_get_latest_release <owner/repo>
# Prints the tag_name of the latest release.
# Installers still use this function instead of just directly invoking download_asset function:
# - Asset names often contain the version
# - Installers may compare the latest tag from github against the locally installed version before doing any work.
# - We need concrete version string so we can pass it to the reigster_tool function.
github_get_latest_release() {
local repo="$1"
local tag
tag=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" | jq -r '.tag_name // empty')
echo "$tag"
}
# Usage: github_get_download_url <owner/repo> <tag> <regex_pattern>
# Finds the asset matching the regex pattern in the specified release tag and prints its download URL.
github_get_download_url() {
local repo="$1"
local tag="$2"
local pattern="$3"
# If the tag is exactly 'latest', fetch the latest release asset list
local endpoint
if [ "$tag" = "latest" ]; then
endpoint="https://api.github.com/repos/$repo/releases/latest"
else
endpoint="https://api.github.com/repos/$repo/releases/tags/$tag"
fi
local url
url=$(curl -fsSL "$endpoint" | jq -r --arg regex "$pattern" '.assets[] | select(.name | test($regex; "i")) | .browser_download_url' | head -n1)
echo "$url"
}
# Usage: github_download_asset <owner/repo> <tag> <regex_pattern> <dest_file>
# Resolves the URL for the matching asset and downloads it to dest_file.
github_download_asset() {
local repo="$1"
local tag="$2"
local pattern="$3"
local dest="$4"
local url
url=$(github_get_download_url "$repo" "$tag" "$pattern")
if [ -z "$url" ]; then
log_error "Could not find asset matching regex '$pattern' for $repo@$tag"
return 1
fi
log_info "Downloading $url ..."
download_file "$url" "$dest"
}
export -f github_get_latest_release github_get_download_url github_download_asset

View File

@@ -86,18 +86,7 @@ pkg_install() {
if ! pkg_check "$pkg"; then
to_install+=("$pkg")
fi
# Reference counting logic
if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then
local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg"
if ! grep -q "^${BOOTSTRAP_CURRENT_TOOL}$" "$ref_file" 2>/dev/null; then
echo "$BOOTSTRAP_CURRENT_TOOL" >> "$ref_file"
# Register rollback command
if type add_rollback_cmd >/dev/null 2>&1; then
add_rollback_cmd "pkg_remove $pkg"
fi
fi
fi
done
if [ ${#to_install[@]} -eq 0 ]; then
@@ -169,21 +158,15 @@ pkg_remove() {
local to_remove=()
for pkg in "${pkgs[@]}"; do
if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then
local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg"
if [ -f "$ref_file" ]; then
# Remove this tool from the reference file
sed -i "/^${BOOTSTRAP_CURRENT_TOOL}$/d" "$ref_file"
if [ -s "$ref_file" ]; then
log_info "Skipping removal of '$pkg'; it is required by other tools."
continue
else
rm -f "$ref_file"
fi
fi
local is_installed=0
if pkg_check "$pkg"; then
is_installed=1
fi
to_remove+=("$pkg")
if [ "$is_installed" -eq 1 ]; then
to_remove+=("$pkg")
fi
done
if [ ${#to_remove[@]} -eq 0 ]; then

180
lib/plugins.sh Normal file
View File

@@ -0,0 +1,180 @@
#!/usr/bin/env bash
# Parses a plugin manifest using jq and outputs bash array assignments
parse_plugin_manifest() {
jq -r '
.plugins | to_entries[] |
(if .value.version then "PLUGIN_VERSIONS[\"" + .key + "\"]=\"" + .value.version + "\"" else empty end),
(if .value.url then "PLUGIN_URLS[\"" + .key + "\"]=\"" + .value.url + "\"" else empty end),
(if .value.bootstrap then "PLUGIN_BOOTSTRAP_VERSIONS[\"" + .key + "\"]=\"" + .value.bootstrap + "\"" else empty end)
'
}
# Ensures that the plugin sources file exists, initializing it with the official repository by default
ensure_sources_file() {
local sources_file="$BOOTSTRAP_DIR/plugin_sources.txt"
if [ ! -f "$sources_file" ]; then
mkdir -p "$BOOTSTRAP_DIR"
echo "# Add raw URLs to JSON plugin manifests here, one per line." > "$sources_file"
echo "# Official Bootstrap plugin repository" >> "$sources_file"
echo "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins.json" >> "$sources_file"
fi
}
# Fetches manifests from sources and generates the cache
update_plugin_cache() {
ensure_sources_file
local cache_file="$BOOTSTRAP_DIR/lib/plugin_cache.sh"
local sources_file="$BOOTSTRAP_DIR/plugin_sources.txt"
mkdir -p "$BOOTSTRAP_DIR/lib"
# Initialize cache file
cat << 'EOF' > "$cache_file"
# Auto-generated plugin cache. Do not edit manually.
declare -g -A PLUGIN_URLS
declare -g -A PLUGIN_VERSIONS
declare -g -A PLUGIN_BOOTSTRAP_VERSIONS
EOF
if [ -f "$sources_file" ]; then
local dl_args=()
local temp_manifests=()
while IFS= read -r url || [ -n "$url" ]; do
# Skip empty lines and comments
[[ -z "$url" || "$url" == \#* ]] && continue
local temp_file
temp_file=$(mktemp --suffix=".json" 2>/dev/null || mktemp)
dl_args+=("$url" "$temp_file")
temp_manifests+=("$temp_file")
done < "$sources_file"
if [ ${#dl_args[@]} -gt 0 ]; then
log_info "Fetching ${#temp_manifests[@]} plugin manifests in parallel..."
download_multiple_files_parallel "${dl_args[@]}"
for temp_file in "${temp_manifests[@]}"; do
if [ -s "$temp_file" ]; then
cat "$temp_file" | parse_plugin_manifest >> "$cache_file"
fi
rm -f "$temp_file"
done
fi
fi
# Clear downloaded scripts to force lazy re-download of the updated versions
rm -rf "$BOOTSTRAP_DIR/plugins" 2>/dev/null || true
log_success "Plugin cache updated successfully."
}
manage_plugin_sources() {
ensure_sources_file
local sources_file="$BOOTSTRAP_DIR/plugin_sources.txt"
local editor="${EDITOR:-}"
if [ -z "$editor" ]; then
if has_command nvim; then editor="nvim"
elif has_command vim; then editor="vim"
elif has_command nano; then editor="nano"
else editor="vi"
fi
fi
$editor "$sources_file"
# Update cache after editing
update_plugin_cache
}
handle_plugin() {
local subcmd="${1:-}"
case "$subcmd" in
sources)
manage_plugin_sources
;;
update)
update_plugin_cache
;;
*)
log_error "Unknown plugin command: $subcmd"
log_info "Available commands: b plugin sources, b plugin update"
exit 1
;;
esac
}
run_plugin() {
local plugin_name="$1"
shift
local is_ephemeral=false
local cmd_args=()
for arg in "$@"; do
if [ "$arg" = "-e" ] || [ "$arg" = "--ephemeral" ]; then
is_ephemeral=true
else
cmd_args+=("$arg")
fi
done
local url="${PLUGIN_URLS[$plugin_name]:-}"
if [ -z "$url" ]; then
log_error "Plugin '$plugin_name' not found in cache."
return 1
fi
# Check compatibility version
local compat_ver="${PLUGIN_BOOTSTRAP_VERSIONS[$plugin_name]:-}"
if [ -n "$compat_ver" ]; then
local current_ver="0.0.0"
if [ -f "$BOOTSTRAP_DIR/VERSION" ]; then
current_ver=$(cat "$BOOTSTRAP_DIR/VERSION" | tr -d '[:space:]')
fi
if version_lt "$compat_ver" "$current_ver"; then
log_warn "Plugin '$plugin_name' is only tested up to bootstrap version $compat_ver (current: $current_ver). It may be incompatible."
fi
fi
if [ "$is_ephemeral" = "true" ]; then
log_info "Downloading and running plugin '$plugin_name' (ephemeral)..."
local script_content
if ! script_content=$(curl -fsSL "$url"); then
log_error "Failed to download plugin '$plugin_name' from $url"
return 1
fi
# Execute the plugin directly in memory in a subshell
(
export BOOTSTRAP_DIR
# We use bash -c and pass the script content to keep stdin free for interactive plugins
# The "$0" arg for bash -c is set to the plugin name
bash -c "$script_content" "$plugin_name" "${cmd_args[@]}"
)
return $?
else
local plugin_dir="$BOOTSTRAP_DIR/plugins"
local local_plugin="$plugin_dir/${plugin_name}.sh"
if [ ! -f "$local_plugin" ]; then
log_info "Downloading plugin '$plugin_name'..."
mkdir -p "$plugin_dir"
if ! curl -fsSL "$url" -o "$local_plugin"; then
log_error "Failed to download plugin '$plugin_name' from $url"
rm -f "$local_plugin"
return 1
fi
chmod +x "$local_plugin"
fi
log_info "Running plugin '$plugin_name'..."
# Execute the plugin in a subshell, passing any additional arguments
(
export BOOTSTRAP_DIR
bash "$local_plugin" "${cmd_args[@]}"
)
return $?
fi
}

View File

@@ -4,6 +4,8 @@ declare -A INSTALLERS=(
[agy]="Antigravity CLI"
[asciicinema]="asciinema terminal recorder"
[bat]="Bat (alternative to cat) and configure alias"
[docker]="Container runtime and orchestration platform"
[lazygit]="Simple terminal UI for git commands"
[node]="Node.js (LTS) and NVM"
[nvim]="Neovim 0.12.0 and configuration"
[pnpm]="pnpm package manager"
@@ -19,6 +21,8 @@ declare -A INSTALLER_DISPLAYS=(
[agy]="Antigravity"
[asciicinema]="asciicinema"
[bat]="Bat"
[docker]="Docker"
[lazygit]="lazygit"
[node]="Node"
[nvim]="Neovim"
[pnpm]="Pnpm"
@@ -30,4 +34,21 @@ declare -A INSTALLER_DISPLAYS=(
[zoxide]="Zoxide"
)
INSTALLER_KEYS=(agy asciicinema bat node nvim pnpm rust starship uv yay yazi zoxide)
declare -A INSTALLER_STRATEGIES=(
[agy]="binary"
[asciicinema]="binary"
[bat]="binary"
[docker]="system"
[lazygit]="binary"
[node]="managed"
[nvim]="binary"
[pnpm]="binary"
[rust]="managed"
[starship]="binary"
[uv]="binary"
[yay]="system"
[yazi]="binary"
[zoxide]="managed"
)
INSTALLER_KEYS=(agy asciicinema bat docker lazygit node nvim pnpm rust starship uv yay yazi zoxide)

133
lib/registry_helpers.sh Normal file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Registry management helpers for Bootstrap
# Ensures the registry file exists
ensure_registry() {
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
if [ ! -f "$registry_file" ]; then
mkdir -p "$(dirname "$registry_file")"
echo '{"tools": {}}' > "$registry_file"
fi
echo "$registry_file"
}
# Safely applies a jq filter to the registry using a file lock
registry_set() {
local jq_filter="$1"
shift
local registry_file
registry_file=$(ensure_registry)
local lock_file="${registry_file}.lock"
(
flock -x 200
local temp_file
temp_file=$(mktemp)
# Apply jq filter with any additional arguments passed in
jq "$@" "$jq_filter" "$registry_file" > "$temp_file" && mv "$temp_file" "$registry_file"
) 200>"$lock_file"
}
# Usage: register_tool <tool_name> <strategy> [version] [source]
register_tool() {
local tool="$1"
local strategy="$2"
local version="${3:-}"
local source="${4:-}"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local bindir="$BOOTSTRAP_BIN"
local filter='if .tools == null then .tools = {} else . end |
.tools[$tool].strategy = $strategy |
.tools[$tool].installed_at = $timestamp |
(if $version != "" then .tools[$tool].version = $version else . end) |
(if $source != "" then .tools[$tool].source = $source else . end) |
(if $strategy == "binary" then .tools[$tool].bin = ($bindir + "/" + $tool) else . end)'
registry_set "$filter" \
--arg tool "$tool" \
--arg strategy "$strategy" \
--arg version "$version" \
--arg source "$source" \
--arg timestamp "$timestamp" \
--arg bindir "$bindir"
}
# Usage: registry_add_sys_deps <tool_name> <dep1> <dep2>...
registry_add_sys_deps() {
local tool="$1"
shift
if [ $# -eq 0 ]; then
return 0
fi
local deps_json
deps_json=$(printf '%s\n' "$@" | jq -R . | jq -s .)
local filter='if .tools == null then .tools = {} else . end |
.tools[$tool].system_dependencies = ((.tools[$tool].system_dependencies // []) + $deps | unique)'
registry_set "$filter" --arg tool "$tool" --argjson deps "$deps_json"
}
# Usage: registry_remove_tool <tool_name>
registry_remove_tool() {
local tool="$1"
registry_set 'del(.tools[$tool])' --arg tool "$tool"
}
# Usage: registry_get_sys_deps <tool_name>
registry_get_sys_deps() {
local tool="$1"
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
if [ -f "$registry_file" ]; then
jq -r --arg tool "$tool" '.tools[$tool].system_dependencies[]? // empty' "$registry_file"
fi
}
# Usage: registry_check <tool_name>
# Validates that a tool is actually installed according to its strategy
registry_check() {
local tool="$1"
local registry_file
registry_file=$(ensure_registry)
local strategy
strategy=$(jq -r --arg tool "$tool" '.tools[$tool].strategy // empty' "$registry_file")
if [ -z "$strategy" ]; then
return 1
fi
if [ "$strategy" = "binary" ]; then
local bin_path
bin_path=$(jq -r --arg tool "$tool" '.tools[$tool].bin // empty' "$registry_file")
if [ -n "$bin_path" ] && [ -x "$bin_path" ]; then
return 0
fi
elif [ "$strategy" = "managed" ]; then
if command -v "$tool" >/dev/null 2>&1; then
return 0
fi
elif [ "$strategy" = "system" ]; then
local deps=()
while IFS= read -r dep; do
[ -n "$dep" ] && deps+=("$dep")
done < <(registry_get_sys_deps "$tool")
if [ ${#deps[@]} -eq 0 ]; then
if command -v "$tool" >/dev/null 2>&1; then
return 0
fi
else
if pkg_check "${deps[@]}"; then
return 0
fi
fi
fi
return 1
}
export -f ensure_registry registry_set register_tool registry_add_sys_deps registry_remove_tool registry_get_sys_deps registry_check

View File

@@ -8,11 +8,11 @@ _LIB_ROLLBACK_SOURCED=1
BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap"
BOOTSTRAP_HISTORY_LOG="$BOOTSTRAP_STATE_DIR/history.log"
BOOTSTRAP_UNINSTALLERS_DIR="$BOOTSTRAP_STATE_DIR/uninstallers"
BOOTSTRAP_PACKAGES_DIR="$BOOTSTRAP_STATE_DIR/packages"
init_rollback_system() {
mkdir -p "$BOOTSTRAP_UNINSTALLERS_DIR"
mkdir -p "$BOOTSTRAP_PACKAGES_DIR"
touch "$BOOTSTRAP_HISTORY_LOG"
}
@@ -51,6 +51,13 @@ track_dir() {
create_savepoint() {
local name="$1"
# Prevent savepoints from having the same name as a tool
if [ -n "${INSTALLERS[$name]:-}" ]; then
log_error "Cannot create savepoint named '$name' because it conflicts with a tool name."
return 1
fi
echo "SAVEPOINT: $name" >> "$BOOTSTRAP_HISTORY_LOG"
log_success "Savepoint '$name' created."
}
@@ -84,6 +91,41 @@ execute_rollback() {
log_success "Rollback of '$tool' complete."
}
uninstall_tool() {
local tool="$1"
# 1. Execute the rollback manifest to remove files/dirs/env/aliases
execute_rollback "$tool"
# 2. Reference counting and cleanup of system dependencies
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
if [ -f "$registry_file" ] && jq -e --arg tool "$tool" '.tools | has($tool)' "$registry_file" >/dev/null; then
while IFS= read -r dep; do
[ -z "$dep" ] && continue
local other_users
other_users=$(jq -r --arg tool "$tool" --arg dep "$dep" '
.tools | to_entries | map(select(.key != $tool and (.value.system_dependencies | type == "array") and (.value.system_dependencies | index($dep)))) | length
' "$registry_file")
if [ "$other_users" -eq 0 ]; then
log_info "System dependency '$dep' is no longer required by any registered tool. Removing..."
pkg_remove "$dep"
else
log_info "Keeping system dependency '$dep' (required by other tools)"
fi
done < <(registry_get_sys_deps "$tool")
# Remove from registry
registry_remove_tool "$tool"
fi
# 3. Remove the tool from history.log
if [ -f "$BOOTSTRAP_HISTORY_LOG" ]; then
sed -i "/^INSTALL: ${tool}$/d" "$BOOTSTRAP_HISTORY_LOG"
fi
}
rollback_bare() {
if [ ! -s "$BOOTSTRAP_HISTORY_LOG" ]; then
log_info "No history available to rollback."
@@ -95,9 +137,7 @@ rollback_bare() {
if [[ "$last_line" == INSTALL:* ]]; then
local tool="${last_line#INSTALL: }"
execute_rollback "$tool"
# Remove the last line efficiently
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
uninstall_tool "$tool"
elif [[ "$last_line" == SAVEPOINT:* ]]; then
local sp="${last_line#SAVEPOINT: }"
log_warn "Last action was savepoint '$sp'. Cannot bare-rollback a savepoint."
@@ -122,8 +162,7 @@ rollback_to_savepoint() {
break
elif [[ "$last_line" == INSTALL:* ]]; then
local tool="${last_line#INSTALL: }"
execute_rollback "$tool"
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
uninstall_tool "$tool"
elif [[ "$last_line" == SAVEPOINT:* ]]; then
local sp="${last_line#SAVEPOINT: }"
log_info "Removing intermediate savepoint '$sp'..."
@@ -135,4 +174,4 @@ rollback_to_savepoint() {
done
}
export -f init_rollback_system setup_uninstaller_context add_rollback_cmd track_file track_dir create_savepoint mark_install_success execute_rollback rollback_bare rollback_to_savepoint
export -f init_rollback_system setup_uninstaller_context add_rollback_cmd track_file track_dir create_savepoint mark_install_success execute_rollback uninstall_tool rollback_bare rollback_to_savepoint

View File

@@ -35,6 +35,18 @@ else
INSTALLER_KEYS=()
fi
# Source plugin system
if [ -f "$BOOTSTRAP_DIR/lib/plugins.sh" ]; then
. "$BOOTSTRAP_DIR/lib/plugins.sh"
if [ ! -f "$BOOTSTRAP_DIR/lib/plugin_cache.sh" ]; then
# Silently auto-generate cache if missing so official plugins are ready instantly
update_plugin_cache >/dev/null 2>&1 || true
fi
if [ -f "$BOOTSTRAP_DIR/lib/plugin_cache.sh" ]; then
. "$BOOTSTRAP_DIR/lib/plugin_cache.sh"
fi
fi
# Helper function to run/edit installer scripts
run_ware() {
local tool="$1"
@@ -127,6 +139,7 @@ run_ware() {
if [ "$run_status" -eq 0 ] && [ "$interrupted" = "false" ]; then
mark_install_success "$tool"
source_bashrc
else
echo
if [ "$interrupted" = "true" ]; then
@@ -190,6 +203,11 @@ for script in "${SCRIPTS[@]}"; do
else
# Handle non-installer commands
case "$script" in
plugin)
handle_plugin "$@"
# Once handle_plugin completes, we should exit so it doesn't process more SCRIPTS
exit $?
;;
all)
if [ -f "$BOOTSTRAP_DIR/commands/help.sh" ]; then
. "$BOOTSTRAP_DIR/commands/help.sh"
@@ -257,14 +275,23 @@ for script in "${SCRIPTS[@]}"; do
if [ -z "$target" ]; then
rollback_bare
else
rollback_to_savepoint "$target"
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
if [ -f "$registry_file" ] && jq -e --arg t "$target" '.tools | has($t)' "$registry_file" >/dev/null; then
uninstall_tool "$target"
else
rollback_to_savepoint "$target"
fi
fi
exit 0
;;
*)
log_error "Unknown command '$script'."
log_info "Run 'b all' to list all available commands."
exit 1
if [[ -n "${PLUGIN_URLS[$script]:-}" ]]; then
run_plugin "$script" "$@"
else
log_error "Unknown command '$script'."
log_info "Run 'b all' to list all available commands."
exit 1
fi
;;
esac
fi

View File

@@ -166,8 +166,18 @@ create_fd_symlink() {
fi
}
# Source the bashrc file to reload configurations
source_bashrc() {
if [ -f "$HOME/.bashrc" ]; then
log_info "Re-sourcing ~/.bashrc..."
. "$HOME/.bashrc"
fi
}
# 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
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

22
plugins.json Normal file
View File

@@ -0,0 +1,22 @@
{
"plugins": {
"weather": {
"version": "1.0.0",
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/weather.sh",
"bootstrap": "2.2.0",
"description": "Show weather forecast for your location"
},
"sysinfo": {
"version": "1.0.0",
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/sysinfo.sh",
"bootstrap": "2.2.0",
"description": "Show system information and hardware statistics"
},
"todo": {
"version": "1.0.0",
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/todo.sh",
"bootstrap": "2.2.0",
"description": "A simple command-line todo list manager"
}
}
}

74
plugins/sysinfo.sh Normal file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# System Information Dashboard Plugin for bootstrap CLI
main() {
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
echo "Usage: b sysinfo"
echo ""
echo "Displays a beautiful system resource and hardware information dashboard."
return 0
fi
echo -e "${BLUE}==================================================${NC}"
echo -e " ${GREEN}SYSTEM INFORMATION DASHBOARD${NC}"
echo -e "${BLUE}==================================================${NC}"
# OS Info
local os_name="Unknown"
if [ -f /etc/os-release ]; then
os_name=$(grep "^PRETTY_NAME=" /etc/os-release | cut -d= -f2 | tr -d '"')
elif [ "$(uname)" = "Darwin" ]; then
os_name="macOS $(sw_vers -productVersion)"
else
os_name=$(uname -s)
fi
echo -e "${BLUE}OS:${NC} $os_name"
echo -e "${BLUE}Kernel:${NC} $(uname -r)"
echo -e "${BLUE}Uptime:${NC} $(uptime | sed 's/^ *//')"
# CPU Info
local cpu_info="Unknown"
if [ -f /proc/cpuinfo ]; then
cpu_info=$(grep -m1 "model name" /proc/cpuinfo | cut -d: -f2 | sed 's/^ *//')
elif [ "$(uname)" = "Darwin" ]; then
cpu_info=$(sysctl -n machdep.cpu.brand_string)
fi
echo -e "${BLUE}CPU:${NC} $cpu_info"
# Load Average
local load_avg
load_avg=$(uptime | awk -F'load average:' '{ print $2 }' | sed 's/^ *//')
echo -e "${BLUE}Load Avg:${NC} $load_avg"
# Memory Usage
echo -e "${BLUE}Memory:${NC}"
if has_command free; then
free -h | awk 'NR==2{printf " Used: %s / Total: %s (%.2f%%)\n", $3, $2, $3/$2*100}'
elif [ -f /proc/meminfo ]; then
local mem_total
mem_total=$(grep "MemTotal" /proc/meminfo | awk '{print $2}')
local mem_free
mem_free=$(grep "MemFree" /proc/meminfo | awk '{print $2}')
local mem_used=$((mem_total - mem_free))
# Convert to MB
local total_mb=$((mem_total / 1024))
local used_mb=$((mem_used / 1024))
local pct=$((used_mb * 100 / total_mb))
echo " Used: ${used_mb}MB / Total: ${total_mb}MB (${pct}%)"
elif [ "$(uname)" = "Darwin" ]; then
local total_mem
total_mem=$(sysctl -n hw.memsize)
local total_gb=$((total_mem / 1024 / 1024 / 1024))
echo " Total: ${total_gb}GB"
else
echo " Unavailable"
fi
# Disk Usage
echo -e "${BLUE}Disk Space (Root):${NC}"
df -h / | awk 'NR==2{printf " Used: %s / Total: %s (%s)\n", $3, $2, $5}'
echo -e "${BLUE}==================================================${NC}"
}
main "$@"

123
plugins/todo.sh Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# Todo List Plugin for bootstrap CLI
TODO_FILE="$HOME/.local/share/bootstrap/todo.txt"
main() {
mkdir -p "$(dirname "$TODO_FILE")"
[ ! -f "$TODO_FILE" ] && touch "$TODO_FILE"
local action="${1:-list}"
case "$action" in
add)
shift
if [ -z "$*" ]; then
log_error "Please specify a task to add."
echo "Usage: b todo add <task description>"
return 1
fi
echo "[ ] $*" >> "$TODO_FILE"
log_success "Added task: $*"
;;
list)
if [ ! -s "$TODO_FILE" ]; then
log_info "Your todo list is empty. Add a task with: b todo add <task>"
return 0
fi
echo -e "${BLUE}--- YOUR TODO LIST ---${NC}"
local line_num=1
while IFS= read -r line || [ -n "$line" ]; do
# Highlight completed tasks
if [[ "$line" == "[\x]"* || "$line" == "[x]"* ]]; then
echo -e " ${line_num}. ${GREEN}${line}${NC}"
else
echo -e " ${line_num}. ${line}"
fi
line_num=$((line_num + 1))
done < "$TODO_FILE"
;;
done)
shift
local task_num="${1:-}"
if [[ ! "$task_num" =~ ^[0-9]+$ ]]; then
log_error "Please specify a valid task number."
echo "Usage: b todo done <number>"
return 1
fi
local total_tasks
total_tasks=$(wc -l < "$TODO_FILE")
if [ "$task_num" -lt 1 ] || [ "$task_num" -gt "$total_tasks" ]; then
log_error "Task number out of range (1-$total_tasks)."
return 1
fi
# Update the task at line task_num to be marked [x]
local temp_file
temp_file=$(mktemp)
local line_num=1
while IFS= read -r line || [ -n "$line" ]; do
if [ "$line_num" -eq "$task_num" ]; then
# Replace [ ] with [x]
echo "${line/\[ \]/\[x\]}" >> "$temp_file"
else
echo "$line" >> "$temp_file"
fi
line_num=$((line_num + 1))
done < "$TODO_FILE"
mv "$temp_file" "$TODO_FILE"
log_success "Marked task #$task_num as completed."
;;
rm|remove)
shift
local task_num="${1:-}"
if [[ ! "$task_num" =~ ^[0-9]+$ ]]; then
log_error "Please specify a valid task number to remove."
echo "Usage: b todo rm <number>"
return 1
fi
local total_tasks
total_tasks=$(wc -l < "$TODO_FILE")
if [ "$task_num" -lt 1 ] || [ "$task_num" -gt "$total_tasks" ]; then
log_error "Task number out of range (1-$total_tasks)."
return 1
fi
# Remove the line at task_num
local temp_file
temp_file=$(mktemp)
local line_num=1
while IFS= read -r line || [ -n "$line" ]; do
if [ "$line_num" -ne "$task_num" ]; then
echo "$line" >> "$temp_file"
fi
line_num=$((line_num + 1))
done < "$TODO_FILE"
mv "$temp_file" "$TODO_FILE"
log_success "Removed task #$task_num."
;;
clear)
> "$TODO_FILE"
log_success "Cleared all tasks from your todo list."
;;
--help|-h)
echo "Usage: b todo [action] [args]"
echo ""
echo "Actions:"
echo " list Show all tasks (default)"
echo " add <task> Add a new task"
echo " done <number> Mark a task as completed"
echo " rm <number> Remove a task"
echo " clear Delete all tasks"
return 0
;;
*)
log_error "Unknown action: $action"
echo "Run 'b todo --help' for usage instructions."
return 1
;;
esac
}
main "$@"

32
plugins/weather.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Weather Plugin for bootstrap CLI
main() {
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
echo "Usage: b weather [location]"
echo ""
echo "Fetches and displays a neat weather forecast."
echo "If no location is specified, it auto-detects based on your IP."
return 0
fi
local location="$*"
log_info "Fetching weather forecast..."
if [ -n "$location" ]; then
# URL encode the location (replace spaces with +)
local encoded_location
encoded_location=$(echo "$location" | tr ' ' '+')
if ! curl -sS "wttr.in/${encoded_location}?0&m"; then
log_error "Failed to fetch weather for '$location'."
return 1
fi
else
if ! curl -sS "wttr.in/?0&m"; then
log_error "Failed to fetch weather."
return 1
fi
fi
}
main "$@"

View File

@@ -109,6 +109,58 @@ b up
b up --force
```
## 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**.
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:
* **`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
```
### Adding Third-Party Plugins
To manage plugin repositories, run:
```bash
b plugin sources
```
This opens a configuration file in your `$EDITOR`. You can add raw URLs pointing to JSON plugin manifests from any repository. Once you close the editor, `bootstrap` automatically parses those manifests using its native JSON parser and generates a fast, zero-latency lookup cache.
You can then execute any plugin simply by calling its name:
```bash
b my_plugin
```
Plugins are automatically checked for updates and lazily re-downloaded whenever you run `b up`.
If you prefer to run a plugin strictly in **ephemeral mode** (meaning it will bypass the cache and execute directly in memory to guarantee the absolute latest version without leaving any footprint), simply pass the `-e` or `--ephemeral` flag:
```bash
b my_plugin -e
```
For documentation on how to develop and publish your own plugins, please see the [Plugin Development Guide](docs/plugin_development.md).
## Uninstallation
To uninstall the bootstrap helper tool but leave a lightweight `b back` function to easily reinstall it later:

View File

@@ -13,6 +13,7 @@ echo "==> Generating registry.sh..."
# Temporary arrays
declare -A tools_desc
declare -A tools_disp
declare -A tools_strat
keys=()
for f in "$INSTALLERS_DIR"/install_*.sh; do
@@ -20,10 +21,12 @@ for f in "$INSTALLERS_DIR"/install_*.sh; do
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
disp_name=$(grep -E "^# DisplayName:" "$f" | head -n1 | sed -E 's/^# DisplayName:\s*//I')
desc=$(grep -E "^# Description:" "$f" | head -n1 | sed -E 's/^# Description:\s*//I' | sed -E 's/^Install\s+//I')
strat=$(grep -E "^# Strategy:" "$f" | head -n1 | sed -E 's/^# Strategy:\s*//I')
if [ -n "$tool" ]; then
tools_desc["$tool"]="$desc"
tools_disp["$tool"]="${disp_name:-$tool}"
tools_strat["$tool"]="${strat:-unknown}"
keys+=("$tool")
fi
done
@@ -49,6 +52,13 @@ sorted_keys=($(printf '%s\n' "${keys[@]}" | sort))
done
echo ")"
echo ""
echo "declare -A INSTALLER_STRATEGIES=("
for k in "${sorted_keys[@]}"; do
escaped_strat=$(echo "${tools_strat[$k]}" | sed 's/"/\\"/g')
echo " [$k]=\"$escaped_strat\""
done
echo ")"
echo ""
# Format keys output as space-separated list in array declaration format
echo "INSTALLER_KEYS=(${sorted_keys[*]})"
} > "$REGISTRY_FILE"

View File

@@ -10,7 +10,7 @@ IFS='.' read -r cur_major cur_minor cur_patch <<< "${current#v}"
bump=""
auto_confirm=false
message=""
# Parse flags for non-interactive (agent) usage
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -18,6 +18,7 @@ while [[ $# -gt 0 ]]; do
--minor) bump="minor"; shift ;;
--major) bump="major"; shift ;;
-y|--yes) auto_confirm=true; shift ;;
-m|--message) message="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
@@ -58,7 +59,7 @@ if [[ "$confirm" =~ ^[Yy]$ ]]; then
echo "${new_ver#v}" > VERSION
git add VERSION
git commit -m "release: $new_ver"
git tag -a "$new_ver" -m "Release $new_ver"
git tag -a "$new_ver" -m "${message:-Release $new_ver}"
echo "Tagged $new_ver. Push with: git push origin master $new_ver"
else
echo "Aborted."