mirror of
https://github.com/sortedcord/bootstrap.git
synced 2026-07-22 20:22:50 +05:30
Compare commits
46 Commits
v2.1.0
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
| 506083b85a | |||
| 6e63e54f1e | |||
| 5ba08f3d20 | |||
| eefd792b6c | |||
| 4a03e42b9d | |||
| f6df53ae31 | |||
| 7d4e23e309 | |||
| 190f337f12 | |||
| 6e0f951623 | |||
| f9a25ff37e | |||
| 03e9c20e54 | |||
| 2ddd28d4d4 | |||
| db6ec1c1c8 | |||
| ed56ef95a9 | |||
| 671cf7f818 | |||
| f5227569b1 | |||
| 15d3a1a59d | |||
| 83c524441c | |||
| cee345e3f0 | |||
| 0c16640593 | |||
| d5c90d6e85 | |||
| 4c1c7de0b7 | |||
| 29de051b7d | |||
| a4e5bc1175 | |||
| 36c7be07b3 | |||
| 0eaea2c997 | |||
| 4eec27570e | |||
| f5a266ff70 | |||
| c42687a710 | |||
| 7f3ff45f05 | |||
| 780e79364f | |||
| f158c4e913 | |||
| fc4303bc99 | |||
| 6b0d07d70a | |||
| f8f41e4295 | |||
| a254001da8 | |||
| 9a7404a65f | |||
| b697fc5bba | |||
| 355588c7f9 | |||
| d108f14ce5 | |||
| 62a4759724 | |||
| fdb2e108ee | |||
| 9c86486ee6 | |||
| 33b98477bf | |||
| b813061e9a | |||
| 7fe9ac913b |
5
.agents/AGENTS.md
Normal file
5
.agents/AGENTS.md
Normal 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.
|
||||
@@ -1,248 +0,0 @@
|
||||
---
|
||||
name: add_installer
|
||||
description: Add a new installer script to the bootstrap CLI project. Use this skill whenever the user asks to create a new installer, add a new tool/package to bootstrap, or register a new `b <name>` command.
|
||||
---
|
||||
|
||||
# Add a New Installer to Bootstrap CLI
|
||||
|
||||
This skill provides everything needed to add a new installer to the bootstrap project without reading the entire codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Bootstrap CLI (`b`) is a bash-based tool installer and system bootstrapper. Users run `b <name>` or `b ware <name>` to install or edit tools (e.g., `b nvim`, `b ware bat`). The project lives at the workspace root.
|
||||
|
||||
### Key Directories
|
||||
|
||||
```
|
||||
bootstrap/
|
||||
├── installers/ # Individual installer scripts (install_<name>.sh)
|
||||
├── lib/ # Shared libraries and router sourced by all installers
|
||||
│ ├── 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
|
||||
│ ├── registry.sh # Dynamically generated installer registry
|
||||
│ └── routes.sh # Central router script
|
||||
├── commands/ # Non-installer commands (help, con, uninstall)
|
||||
├── assets/ # Assets (logo art, etc.)
|
||||
│ └── pixel_art.ansi
|
||||
├── bootstrap.sh # Metascript for environment setup + library loading
|
||||
├── b.sh # The `b` shell function and autocompletion
|
||||
└── VERSION
|
||||
```
|
||||
|
||||
## Step-by-Step Checklist
|
||||
|
||||
When adding a new installer named `<name>`:
|
||||
|
||||
### Step 1: Create the installer script
|
||||
|
||||
Create `installers/install_<name>.sh` using the template below.
|
||||
|
||||
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.
|
||||
|
||||
### Step 2: 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
|
||||
# Tool: <name>
|
||||
# DisplayName: <displayName>
|
||||
# Description: <description>
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
### Step 4: Verify (optional)
|
||||
|
||||
Verify that the installer works and appears in the help output:
|
||||
- Run `b all` to confirm it appears in the help list.
|
||||
- Run `b ware <name> -y` to test direct installation.
|
||||
- Run `b ware <name>` to test the interactive editing flow.
|
||||
|
||||
---
|
||||
|
||||
## Installer Script Template
|
||||
|
||||
Every installer follows this exact boilerplate structure. Copy this and fill in the tool-specific logic:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Tool: <name>
|
||||
# DisplayName: <ToolName>
|
||||
# Description: Short description of what it installs
|
||||
#
|
||||
# <ToolName> 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 ──────────────────────────────────────────────
|
||||
|
||||
install_<name>() {
|
||||
if has_command <command_name>; then
|
||||
if ! confirm "<ToolName> is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping <ToolName> installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install <ToolName>?"; then
|
||||
log_info "Skipping <ToolName> installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Tool-specific installation logic goes here ---
|
||||
# Use pkg_install for distro packages (it automatically handles rollback hooks!):
|
||||
# pkg_install "arch:<pkg_a>|debian:<pkg_d>|fedora:<pkg_f>"
|
||||
|
||||
# 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!
|
||||
}
|
||||
|
||||
# ─── Shell Configuration (if needed) ─────────────────────────────────
|
||||
|
||||
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>'"
|
||||
:
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
install_<name>
|
||||
configure_shell # Remove this line if no shell config is needed
|
||||
|
||||
echo
|
||||
log_success "<ToolName> installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Library Functions
|
||||
|
||||
These are pre-loaded by `bootstrap.sh` — no need to source them manually in installers.
|
||||
|
||||
### From `lib/common.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `log_info "msg"` | Blue `[INFO]` message to stdout |
|
||||
| `log_success "msg"` | Green `[SUCCESS]` message to stdout |
|
||||
| `log_warn "msg"` | Yellow `[WARNING]` message to stderr |
|
||||
| `log_error "msg"` | Red `[ERROR]` message to stderr |
|
||||
| `confirm "prompt"` | Interactive yes/no prompt, returns 0 for yes |
|
||||
| `has_command <cmd>` | Check if a command exists (returns 0/1) |
|
||||
| `make_temp_dir` | Create and echo a temp directory path |
|
||||
| `download_file <url> <dest>` | Resumable and cached download of `<url>` to `<dest>`. Uses `~/.local/state/bootstrap/cache/`. |
|
||||
|
||||
### From `lib/platform.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `detect_distro` | Echoes `arch`, `debian`, `fedora`, or `unknown` |
|
||||
| `detect_arch` | Echoes `x86_64` or `arm64` |
|
||||
| `pkg_install <pkg>...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. Automatically integrates with rollback context and handles package reference counting. |
|
||||
| `pkg_check <pkg>...` | Returns 0 if packages are installed. Supports identical mapping syntax. |
|
||||
|
||||
### From `lib/rollback.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `track_file <path>` | Registers a file for deletion during `b rb` rollback. |
|
||||
| `track_dir <path>` | Registers a directory for recursive deletion during rollback. |
|
||||
| `add_rollback_cmd <cmd>` | Adds a raw bash command to the uninstall manifest (e.g., `add_rollback_cmd "sudo npm uninstall -g <pkg>"`). |
|
||||
|
||||
### From `lib/shell_config.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `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. |
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Temp directory with cleanup
|
||||
|
||||
```bash
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
```
|
||||
|
||||
### Distro-specific mapping
|
||||
|
||||
```bash
|
||||
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "curl" "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)
|
||||
fi
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v1.0.0" # fallback
|
||||
log_warn "Failed to fetch latest version. Falling back to: $latest_tag"
|
||||
fi
|
||||
```
|
||||
|
||||
### Resumable Download and Extraction
|
||||
|
||||
```bash
|
||||
local url="https://github.com/owner/repo/releases/download/${version}/archive.tar.gz"
|
||||
local dest="$TMP_DIR/archive.tar.gz"
|
||||
|
||||
# Resumable, cached download
|
||||
download_file "$url" "$dest"
|
||||
|
||||
# Extract and install
|
||||
tar -xzf "$dest" -C "$TMP_DIR"
|
||||
sudo cp "$TMP_DIR/binary" /usr/local/bin/binary
|
||||
track_file "/usr/local/bin/binary"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules & Conventions
|
||||
|
||||
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`.
|
||||
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.
|
||||
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.
|
||||
261
.agents/skills/add_tool/SKILL.md
Normal file
261
.agents/skills/add_tool/SKILL.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
name: add_tool
|
||||
description: Add a new tool definition script to the bootstrap CLI project. Use this skill whenever the user asks to create a new tool, add a new tool/package to bootstrap, or register a new `b <name>` command.
|
||||
---
|
||||
|
||||
# Add a New Tool to Bootstrap CLI
|
||||
|
||||
This skill provides everything needed to add a new tool to the bootstrap project without reading the entire codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Bootstrap CLI (`b`) is a bash-based tool installer and system bootstrapper. Users run `b <name>` or `b ware <name>` to install or edit tools (e.g., `b nvim`, `b ware bat`). The project lives at the workspace root.
|
||||
|
||||
### Key Directories
|
||||
|
||||
```
|
||||
bootstrap/
|
||||
├── tools/ # Individual tool directories (tools/<name>/tool.sh)
|
||||
├── lib/ # Shared libraries and router sourced by all tools
|
||||
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
|
||||
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install(), pkg_check(), pkg_remove()
|
||||
│ ├── rollback.sh # Rollback tracking (track_file, track_dir, add_rollback_cmd)
|
||||
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet, write_completion_snippet
|
||||
│ ├── registry.sh # Dynamically generated tool registry
|
||||
│ └── routes.sh # Central router script
|
||||
├── commands/ # Non-tool commands (help, con, uninstall)
|
||||
├── assets/ # Assets (logo art, etc.)
|
||||
│ └── pixel_art.ansi
|
||||
├── bootstrap.sh # Metascript for environment setup + library loading
|
||||
├── b.sh # The `b` shell function and autocompletion
|
||||
└── VERSION
|
||||
```
|
||||
|
||||
## Step-by-Step Checklist
|
||||
|
||||
When adding a new tool named `<name>`:
|
||||
|
||||
### Step 1: Analyze user request & gather details
|
||||
|
||||
When the user asks you to add a tool, they often provide either an official `curl` install script or a link to a `.tar.gz` release.
|
||||
You MUST do the following before writing the tool script:
|
||||
|
||||
If the user provides an official install or curl script in the prompt, or a link to one:
|
||||
- Execute the `curl` command (or use `read_url_content` or `read_browser_page`) to fetch the script and analyze what it actually does under the hood.
|
||||
- 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), unnecessary shell configuration logic, and self-update logic.
|
||||
- Implement only the core, essential Linux installation logic inside the `install_<name>` function.
|
||||
- Do NOT add checks for `curl` or attempt to install it. Assume `curl` is installed and available.
|
||||
|
||||
**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 "GitHub Download and Extraction" below).
|
||||
|
||||
### Step 2: Create the tool script
|
||||
|
||||
Create `tools/<name>/tool.sh` using the template below.
|
||||
|
||||
### Step 3: Add metadata comments to the top of your tool script
|
||||
|
||||
At the top of your new tool script, right below `#!/usr/bin/env bash`, add the following metadata headers:
|
||||
```bash
|
||||
# Tool: <name>
|
||||
# DisplayName: <displayName>
|
||||
# Description: <description>
|
||||
# Strategy: <strategy> (binary | managed | system)
|
||||
```
|
||||
|
||||
The central router `lib/routes.sh` and autocomplete function in `b.sh` will dynamically parse this metadata from all `tools/*/tool.sh` scripts to register the tool and keys automatically! No manual edits to `lib/routes.sh` or `b.sh` are required.
|
||||
|
||||
### Step 4: Implement Rollback Tracking (Crucial)
|
||||
|
||||
To ensure the user can seamlessly use `b rb <name>` to uninstall or rollback a tool, all manual modifications must be tracked:
|
||||
- When extracting binaries, copy them to `$BOOTSTRAP_BIN` and use `track_file "$BOOTSTRAP_BIN/binary"`. Never use `sudo` or write to system folders like `/usr/local/bin`.
|
||||
- When creating directories (e.g., in `$BOOTSTRAP_OPT` or `$BOOTSTRAP_RUNTIMES`), use `track_dir` to register them.
|
||||
- When running manual commands (like compiling or registry changes), log their inverses: `add_rollback_cmd "rm -rf ..."` or equivalent cleanup.
|
||||
- When installing packages via `pkg_install`, always register them with `registry_add_sys_deps` for reference-counted rollback tracking.
|
||||
- Note: `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` will automatically track themselves.
|
||||
|
||||
### Step 5: Verify (optional)
|
||||
|
||||
Verify that the tool works and appears in the help output:
|
||||
- Run `b all` to confirm it appears in the help list.
|
||||
- Run `b ware <name> -y` to test direct installation.
|
||||
- Run `b ware <name>` to test the interactive editing flow.
|
||||
|
||||
---
|
||||
|
||||
## Tool Script Template
|
||||
|
||||
Every tool follows this exact structure. Copy this and fill in the tool-specific logic (notice there are no standalone execution guards or curl checks):
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Tool: <name>
|
||||
# DisplayName: <ToolName>
|
||||
# Description: Short description of what it installs
|
||||
# Strategy: <strategy> (binary | managed | system)
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Installation Logic ──────────────────────────────────────────────
|
||||
|
||||
install_<name>() {
|
||||
if has_command <command_name>; then
|
||||
if ! confirm "<ToolName> is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping <ToolName> installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install <ToolName>?"; then
|
||||
log_info "Skipping <ToolName> installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Tool-specific installation logic goes here ---
|
||||
# Use pkg_install for distro packages:
|
||||
# pkg_install "arch:<pkg_a>|debian:<pkg_d>|fedora:<pkg_f>"
|
||||
|
||||
# Or manual downloads (always use download_file for resumability!):
|
||||
# local url="https://..."
|
||||
# download_file "$url" "$TMP_DIR/binary"
|
||||
# cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary"
|
||||
# track_file "$BOOTSTRAP_BIN/binary" # Important for rollback!
|
||||
}
|
||||
|
||||
# ─── Shell Configuration (if needed) ─────────────────────────────────
|
||||
|
||||
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)"
|
||||
:
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
install_<name>
|
||||
configure_shell # Remove this line if no shell config is needed
|
||||
|
||||
echo
|
||||
log_success "<ToolName> installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Library Functions
|
||||
|
||||
These are pre-loaded by `bootstrap.sh` — no need to source them manually in tool scripts.
|
||||
|
||||
### From `lib/common.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `log_info "msg"` | Blue `[INFO]` message to stdout |
|
||||
| `log_success "msg"` | Green `[SUCCESS]` message to stdout |
|
||||
| `log_warn "msg"` | Yellow `[WARNING]` message to stderr |
|
||||
| `log_error "msg"` | Red `[ERROR]` message to stderr |
|
||||
| `confirm "prompt"` | Interactive yes/no prompt, returns 0 for yes |
|
||||
| `has_command <cmd>` | Check if a command exists (returns 0/1) |
|
||||
| `make_temp_dir` | Create and echo a temp directory path |
|
||||
| `download_file <url> <dest>` | Resumable and cached download of `<url>` to `<dest>`. Uses `$BOOTSTRAP_CACHE_DIR/downloads/`. |
|
||||
|
||||
### From `lib/platform.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `detect_distro` | Echoes `arch`, `debian`, `fedora`, or `unknown` |
|
||||
| `detect_arch` | Echoes `x86_64` or `arm64` |
|
||||
| `pkg_install <pkg>...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. |
|
||||
| `pkg_check <pkg>...` | Returns 0 if packages are installed. Supports identical mapping syntax. |
|
||||
|
||||
### From `lib/rollback.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `track_file <path>` | Registers a file for deletion during `b rb` rollback. |
|
||||
| `track_dir <path>` | Registers a directory for recursive deletion during rollback. |
|
||||
| `add_rollback_cmd <cmd>` | Adds a raw bash command to the uninstall manifest (e.g., `add_rollback_cmd "rm -rf <path>"`). |
|
||||
|
||||
### From `lib/shell_config.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `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. |
|
||||
|
||||
### From `lib/github.sh`
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `github_get_latest_release <repo>` | Fetches the latest release tag (e.g., `v1.0.0`) from the GitHub API. |
|
||||
| `github_get_download_url <repo> <tag> <regex>` | Finds an asset matching a regex in the specified release and prints its URL. |
|
||||
| `github_download_asset <repo> <tag> <regex> <dest>` | Resolves the URL for the matching asset and downloads it directly to `<dest>`. |
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Temp directory with cleanup
|
||||
|
||||
```bash
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
```
|
||||
|
||||
### Distro-specific mapping
|
||||
|
||||
```bash
|
||||
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "git"
|
||||
```
|
||||
|
||||
### Fetching latest GitHub release tag
|
||||
|
||||
```bash
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "owner/repo")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v1.0.0" # fallback
|
||||
log_warn "Failed to fetch latest version. Falling back to: $latest_tag"
|
||||
fi
|
||||
```
|
||||
|
||||
### GitHub Download and Extraction
|
||||
|
||||
```bash
|
||||
local archive="$TMP_DIR/archive.tar.gz"
|
||||
|
||||
# Download the asset matching a regex pattern for the specific release tag
|
||||
github_download_asset "owner/repo" "$latest_tag" "app-.*-linux-x86_64\.tar\.gz" "$archive"
|
||||
|
||||
# Extract and install
|
||||
tar -xzf "$archive" -C "$TMP_DIR"
|
||||
cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary"
|
||||
track_file "$BOOTSTRAP_BIN/binary"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules & Conventions
|
||||
|
||||
1. **File naming**: Always `tools/<name>/tool.sh`.
|
||||
2. **Confirmation prompts**: Always ask before installing. Check if already installed first.
|
||||
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `$BOOTSTRAP_BIN`, you MUST call `track_file`. If you build/install manually (e.g. via `makepkg -si`), you MUST call `add_rollback_cmd "sudo pacman -R --noconfirm <pkg>"` or equivalent.
|
||||
4. **Shell Drop-ins**: Always use `write_env_snippet`, `write_alias_snippet`, or `write_completion_snippet` instead of manually injecting code directly into `~/.bashrc`.
|
||||
5. **No hardcoded paths**: Use `$HOME`, `$BOOTSTRAP_BIN`, `$BOOTSTRAP_OPT`, `$BOOTSTRAP_RUNTIMES`, library functions, and `detect_*` helpers.
|
||||
6. **Error handling**: Use `set -euo pipefail`.
|
||||
7. **No Standalone Execution Guards**: Do NOT add guards to prevent running the installer directly; simplify code paths to focus on clean runs.
|
||||
8. **Clean Official Scripts**: When implementing official curl/install scripts, strip them of bloat, macOS/Windows support, and redundant shell setups. Do not check or install `curl`.
|
||||
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.
|
||||
@@ -20,17 +20,27 @@ When the user asks to "cut a release", "bump the version", or "tag a new version
|
||||
git log $(git describe --tags --abbrev=0)..HEAD --oneline
|
||||
```
|
||||
2. **Determine the bump level** using semantic versioning rules:
|
||||
- Skip any commits that only touch `installers/` or docs — those don't warrant a bump.
|
||||
- Skip any commits that only touch `tools/` or docs — those don't warrant a bump.
|
||||
- If any commit has `BREAKING CHANGE` or `!:` → **major**
|
||||
- If any core-CLI commit has `feat:` → **minor**
|
||||
- Otherwise (only `fix:`, `refactor:`, etc. in core-CLI) → **patch**
|
||||
- If *all* commits are installer-only or docs-only → inform the user no release is needed.
|
||||
3. **Run the release script** non-interactively (if an actual bump is needed):
|
||||
```bash
|
||||
./scripts/release.sh --<level> -y
|
||||
- If *all* commits are tool-only or docs-only → inform the user no release is needed.
|
||||
3. **Formulate a verbose, structured description of the changes** based on the analyzed commits. Group the changes into logical sections (such as "Breaking Changes & Major Features:" and "Other Updates:") and list the corresponding commit messages or summaries. Example format:
|
||||
```text
|
||||
Breaking Changes & Major Features:
|
||||
|
||||
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
37
.gitea/workflows/lint.yml
Normal 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
31
.github/workflows/lint.yml
vendored
Normal 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
|
||||
27
b.sh
27
b.sh
@@ -1,3 +1,4 @@
|
||||
# shellcheck shell=bash
|
||||
# >>> bootstrap-cli b function >>>
|
||||
# Shortcut for downloading and running bootstrap/install scripts
|
||||
b() {
|
||||
@@ -19,6 +20,7 @@ b() {
|
||||
_version_lt() {
|
||||
[ "$1" = "$2" ] && return 1
|
||||
local IFS=.
|
||||
# shellcheck disable=SC2206
|
||||
local i ver1=($1) ver2=($2)
|
||||
for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done
|
||||
for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done
|
||||
@@ -41,7 +43,7 @@ b() {
|
||||
|
||||
local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
local local_ver="0.0.0"
|
||||
[ -f "$routes_dir/VERSION" ] && local_ver=$(cat "$routes_dir/VERSION" 2>/dev/null | tr -d '[:space:]')
|
||||
[ -f "$routes_dir/VERSION" ] && local_ver=$(tr -d '[:space:]' < "$routes_dir/VERSION" 2>/dev/null)
|
||||
|
||||
local remote_ver
|
||||
if remote_ver=$(curl -fsSL "$base_url/VERSION" 2>/dev/null); then
|
||||
@@ -66,6 +68,16 @@ 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
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
return $ret
|
||||
}
|
||||
|
||||
# Autocompletion for the b command in Bash
|
||||
@@ -77,12 +89,12 @@ _b_completion() {
|
||||
|
||||
# If completing the first argument after 'b'
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
opts="all con gone up ware bware"
|
||||
opts="all con gone up ware bware me trust"
|
||||
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
local installer_keys=""
|
||||
if [ -d "$routes_dir/installers" ]; then
|
||||
for f in "$routes_dir/installers"/install_*.sh; do
|
||||
if [ -d "$routes_dir/tools" ]; then
|
||||
for f in "$routes_dir/tools"/*/tool.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
local tool
|
||||
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
|
||||
@@ -114,6 +126,7 @@ _b_completion() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2207
|
||||
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
@@ -122,8 +135,8 @@ _b_completion() {
|
||||
if [ "$COMP_CWORD" -eq 2 ] && { [ "$prev" = "ware" ] || [ "$prev" = "bware" ]; }; then
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
local installer_keys=""
|
||||
if [ -d "$routes_dir/installers" ]; then
|
||||
for f in "$routes_dir/installers"/install_*.sh; do
|
||||
if [ -d "$routes_dir/tools" ]; then
|
||||
for f in "$routes_dir/tools"/*/tool.sh; do
|
||||
if [ -f "$f" ]; then
|
||||
local tool
|
||||
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
|
||||
@@ -152,6 +165,7 @@ _b_completion() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2207
|
||||
COMPREPLY=( $(compgen -W "$installer_keys" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
@@ -161,6 +175,7 @@ _b_completion() {
|
||||
# List of directories in ~/.config/ to choose from
|
||||
local config_dirs
|
||||
config_dirs=$(find "$HOME/.config" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null)
|
||||
# shellcheck disable=SC2207
|
||||
COMPREPLY=( $(compgen -W "$config_dirs" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
|
||||
65
bootstrap.sh
65
bootstrap.sh
@@ -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
|
||||
@@ -25,7 +20,7 @@ fi
|
||||
|
||||
# Locate or download libraries so that sourced installers can use them
|
||||
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _SCRIPT_DIR="$(pwd)"
|
||||
|
||||
if [ -f "$_SCRIPT_DIR/lib/common.sh" ]; then
|
||||
# Dev/local mode: source directly from repo
|
||||
@@ -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,9 +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"
|
||||
mkdir -p "$routes_dir/completions.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=(
|
||||
@@ -82,12 +97,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
|
||||
@@ -97,10 +120,16 @@ install_bootstrap() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Also copy installers if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/installers" ]; then
|
||||
mkdir -p "$routes_dir/installers"
|
||||
cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/"
|
||||
# Also copy tools if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/tools" ]; then
|
||||
mkdir -p "$routes_dir/tools"
|
||||
cp -r "$_SCRIPT_DIR/tools/"* "$routes_dir/tools/"
|
||||
fi
|
||||
|
||||
# Also copy plugins if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/plugins" ]; then
|
||||
mkdir -p "$routes_dir/plugins"
|
||||
cp -r "$_SCRIPT_DIR/plugins/"* "$routes_dir/plugins/"
|
||||
fi
|
||||
else
|
||||
log_info "Downloading bootstrap scripts..."
|
||||
@@ -137,9 +166,17 @@ 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
|
||||
for f in "$BOOTSTRAP_DIR/completions.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
# <<< bootstrap-cli setup <<<
|
||||
EOF
|
||||
|
||||
@@ -239,7 +276,7 @@ EOF
|
||||
# Display centered version of bootstrap in bold
|
||||
_version=""
|
||||
if [ -f "$_version_file" ]; then
|
||||
_version=$(cat "$_version_file" | tr -d '\r\n')
|
||||
_version=$(tr -d '\r\n' < "$_version_file")
|
||||
fi
|
||||
if [ -z "$_version" ]; then
|
||||
_version="0.0.0" # Fallback if VERSION missing
|
||||
@@ -306,6 +343,7 @@ EOF
|
||||
|
||||
# Load the b function immediately in the current subshell
|
||||
if [ -f "$HOME/.config/bootstrap/b.sh" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.config/bootstrap/b.sh"
|
||||
fi
|
||||
|
||||
@@ -318,6 +356,7 @@ else
|
||||
# Sourced mode (e.g., when sourced by installers or manually by user)
|
||||
# Load the b function in the current shell context
|
||||
if [ -f "$HOME/.config/bootstrap/b.sh" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.config/bootstrap/b.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: con
|
||||
# Edits configurations in ~/.config/
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: help
|
||||
# Lists all available bootstrap commands and installers
|
||||
# Lists all available bootstrap commands and tools
|
||||
|
||||
echo "Available bootstrap commands:"
|
||||
# Non-installers first (aligned to 6 chars width)
|
||||
# Non-tools first (aligned to 6 chars width)
|
||||
printf " %-6s - %s\n" "all" "List all available commands"
|
||||
printf " %-6s - %s\n" "con" "Edit config (e.g. b con nvim)"
|
||||
printf " %-6s - %s\n" "up" "Check for updates and update Bootstrap CLI"
|
||||
printf " %-6s - %s\n" "ware" "Edit and run an installer (e.g. b ware nvim)"
|
||||
printf " %-6s - %s\n" "ware" "Edit and run a tool (e.g. b ware nvim)"
|
||||
printf " %-6s - %s\n" "gone" "Uninstall Bootstrap CLI helper"
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: uninstall (gone)
|
||||
# Removes bootstrap CLI and cleans up shell configuration files
|
||||
|
||||
@@ -45,7 +46,6 @@ if [ -f "$HOME/.bash_aliases" ]; then
|
||||
|
||||
# 2. Remove specific aliases added by bootstrap (e.g. vim -> nvim)
|
||||
if grep -q '^alias vim="nvim"$' "$HOME/.bash_aliases" 2>/dev/null; then
|
||||
local tmp_file
|
||||
tmp_file=$(mktemp)
|
||||
sed '/^alias vim="nvim"$/d' "$HOME/.bash_aliases" > "$tmp_file"
|
||||
cat "$tmp_file" > "$HOME/.bash_aliases"
|
||||
@@ -61,7 +61,6 @@ fi
|
||||
|
||||
# If force is false, leave a lightweight 'b back' shortcut function in shell config files
|
||||
if [ "$FORCE" = "false" ]; then
|
||||
local b_back_content
|
||||
b_back_content=$(cat << 'EOF'
|
||||
b() {
|
||||
if [ "${1:-}" = "back" ]; then
|
||||
@@ -78,6 +77,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
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# shellcheck shell=bash
|
||||
# Command: up
|
||||
# Manually checks for updates and runs the updater if a newer version is found.
|
||||
|
||||
# Source libraries if needed
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
_LIB_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib"
|
||||
# shellcheck source=/dev/null
|
||||
. "$_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
@@ -14,7 +16,7 @@ log_info "Checking for updates..."
|
||||
local_ver="0.0.0"
|
||||
version_file="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/VERSION"
|
||||
if [ -f "$version_file" ]; then
|
||||
local_ver=$(cat "$version_file" | tr -d '[:space:]')
|
||||
local_ver=$(tr -d '[:space:]' < "$version_file")
|
||||
fi
|
||||
|
||||
base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
@@ -48,6 +50,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
162
docs/client_spec_auth.md
Normal 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.
|
||||
389
docs/installation_strategies.md
Normal file
389
docs/installation_strategies.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Installation Strategies & Installer Lifecycle
|
||||
|
||||
This document explains how Bootstrap categorizes tool installations, the anatomy of an installer script, the full execution lifecycle from invocation to registry recording, and the helper infrastructure available to installers.
|
||||
|
||||
## 1. Installation Strategies
|
||||
|
||||
Every installer declares an explicit **strategy** that describes how it provisions software onto the system. The strategy is recorded in two places:
|
||||
|
||||
1. **At authoring time** — as a `# Strategy:` metadata comment in the installer script header.
|
||||
2. **At runtime** — inside `$BOOTSTRAP_STATE_DIR/registry.json` via `register_tool`.
|
||||
|
||||
### Strategy Types
|
||||
|
||||
| Strategy | Description | Validation (`registry_check`) |
|
||||
| :--- | :--- | :--- |
|
||||
| `binary` | Tool is downloaded as a prebuilt binary (typically from a GitHub release) and placed into `$BOOTSTRAP_BIN`. | Confirms the registered binary path exists and is executable. |
|
||||
| `managed` | Tool is installed through its own dedicated version manager or installer (e.g. NVM for Node.js, `rustup` for Rust). Bootstrap orchestrates the manager but doesn't own the binary directly. | Confirms the tool is available via `command -v`. |
|
||||
| `system` | Tool is installed entirely through the system package manager (`pacman`, `apt`, `dnf`). | Validates recorded system packages using `pkg_check`, falling back to `command -v` when no dependencies are recorded. |
|
||||
|
||||
### Which Strategy to Use
|
||||
|
||||
- **Prefer `binary`** when the upstream project provides standalone Linux binaries on GitHub (e.g. `bat`, `lazygit`, `hyperfine`, `yazi`, `nvim`).
|
||||
- **Use `managed`** when the tool has its own version/runtime manager that must be preserved for updates (e.g. `nvm` for Node.js, `rustup` for Rust, `zoxide` which uses its own install script).
|
||||
- **Use `system`** when the tool is only available through the distro's package manager and there is no upstream binary distribution (e.g. `docker`, `yay`).
|
||||
|
||||
## 2. Installer Script Anatomy
|
||||
|
||||
Every installer lives at `installers/install_<tool>.sh` and follows a strict structure.
|
||||
|
||||
### 2.1 Metadata Header
|
||||
|
||||
The first lines of the file are structured comments parsed by `scripts/generate_registry.sh` to auto-generate `lib/registry.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
# Strategy: binary
|
||||
#
|
||||
```
|
||||
|
||||
| Field | Purpose |
|
||||
| :--- | :--- |
|
||||
| `Tool` | The CLI key used to invoke the installer (`b bat`). Must be lowercase, no spaces. |
|
||||
| `DisplayName` | Human-readable name shown in `b ware` listings and log messages. |
|
||||
| `Description` | One-line description. The word "Install" at the start is stripped automatically by the registry generator. |
|
||||
| `Strategy` | One of `binary`, `managed`, or `system`. |
|
||||
|
||||
### 2.2 Script Body Structure
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
|
||||
# Temporary directory (auto-cleaned)
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# Core installation function
|
||||
install_<tool>() {
|
||||
# 1. Check if already installed, offer reinstall/upgrade
|
||||
# 2. Detect architecture, fetch latest version
|
||||
# 3. Download, extract, install
|
||||
# 4. Track files for rollback
|
||||
# 5. Register in the JSON registry
|
||||
}
|
||||
|
||||
# Optional: shell environment configuration
|
||||
configure_shell() {
|
||||
# Write env/alias/completion snippets
|
||||
}
|
||||
|
||||
main() {
|
||||
install_<tool>
|
||||
configure_shell # if needed
|
||||
|
||||
echo
|
||||
log_success "<Tool> installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
### 2.3 Key Conventions
|
||||
|
||||
- **No standalone execution guards**: Installers are always executed by the Bootstrap runtime, which sources all libraries before running the script. They must never check for or source libraries themselves.
|
||||
- **No `curl` availability checks**: The runtime guarantees `curl` is available.
|
||||
- **No direct `.bashrc` manipulation**: Use `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` to write shell configuration fragments into the snippet directories (`env.d/`, `aliases.d/`, `completions.d/`). The loader sources these automatically.
|
||||
- **Temp directories**: Use `make_temp_dir` (from `lib/common.sh`) for temporary work and clean up via a `trap cleanup EXIT`.
|
||||
|
||||
## 3. Available Helper Functions
|
||||
|
||||
Installers run inside a prepared environment where all Bootstrap libraries have been sourced. The following helper functions are available:
|
||||
|
||||
### 3.1 Common Utilities (`lib/common.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `log_info`, `log_success`, `log_warn`, `log_error` | Coloured logging to stdout/stderr. |
|
||||
| `confirm "<prompt>"` | Interactive yes/no prompt. Reads from `/dev/tty` to support piped execution. |
|
||||
| `has_command <cmd>` | Returns `0` if the command exists on `$PATH`. |
|
||||
| `make_temp_dir` | Creates a `mktemp -d` temporary directory and prints its path. |
|
||||
| `download_file <url> <dest>` | Cached, resumable download. Files are first stored in `$BOOTSTRAP_CACHE_DIR/downloads/` and then copied to `<dest>`. |
|
||||
| `version_lt <v1> <v2>` | Semver comparison. Returns `0` if `v1 < v2`. |
|
||||
|
||||
### 3.2 Platform & Package Management (`lib/platform.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `detect_distro` | Returns `arch`, `debian`, `fedora`, or `unknown`. |
|
||||
| `detect_arch` | Returns `x86_64` or `arm64`. |
|
||||
| `pkg_install <spec>...` | Installs system packages. Supports distro-specific mappings: `"arch:docker\|debian:docker.io\|fedora:docker"`. |
|
||||
| `pkg_check <spec>...` | Returns `0` if all specified packages are installed. |
|
||||
| `pkg_remove <spec>...` | Removes system packages. |
|
||||
|
||||
### 3.3 GitHub Release Helpers (`lib/github.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `github_get_latest_release <owner/repo>` | Queries the GitHub API and prints the `tag_name` of the latest release. |
|
||||
| `github_get_download_url <owner/repo> <tag> <regex>` | Finds the asset matching the regex pattern in the given release and prints its download URL. |
|
||||
| `github_download_asset <owner/repo> <tag> <regex> <dest>` | Resolves and downloads the matching asset to `<dest>`. |
|
||||
|
||||
**Why `github_get_latest_release` is separate from `github_download_asset`**: Asset filenames often embed the version string (e.g. `bat-v0.26.1-x86_64-unknown-linux-gnu.tar.gz`). The installer needs the concrete version *before* constructing the asset pattern and also passes it to `register_tool`.
|
||||
|
||||
### 3.4 Shell Configuration (`lib/shell_config.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `write_env_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/env.d/<name>.sh`. Auto-registers a rollback command to delete the snippet. |
|
||||
| `write_alias_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/aliases.d/<name>.sh`. Auto-registers rollback. |
|
||||
| `write_completion_snippet <name> <content>` | Writes content to `$BOOTSTRAP_DIR/completions.d/<name>.sh`. Auto-registers rollback. |
|
||||
|
||||
### 3.5 Rollback & File Tracking (`lib/rollback.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `track_file <path>` | Records `sudo rm -f '<path>'` in the uninstaller manifest for the current tool. |
|
||||
| `track_dir <path>` | Records `sudo rm -rf '<path>'` in the uninstaller manifest. |
|
||||
| `add_rollback_cmd <cmd>` | Prepends an arbitrary shell command to the manifest (LIFO order). |
|
||||
|
||||
### 3.6 Registry Helpers (`lib/registry_helpers.sh`)
|
||||
|
||||
| Function | Purpose |
|
||||
| :--- | :--- |
|
||||
| `register_tool <tool> <strategy> [version] [source]` | Records the tool in `registry.json` with its strategy, version, source, timestamp, and (for `binary` strategy) its binary path. |
|
||||
| `registry_add_sys_deps <tool> <dep1> <dep2>...` | Appends system package dependency names to the tool's `system_dependencies` array in the registry (used for reference counting during uninstallation). |
|
||||
| `registry_remove_tool <tool>` | Deletes the tool's entire entry from `registry.json`. |
|
||||
| `registry_check <tool>` | Validates that the tool is correctly installed according to its declared strategy. |
|
||||
|
||||
## 4. Execution Lifecycle
|
||||
|
||||
This is the complete flow from when a user types `b <tool>` to when the installation is recorded.
|
||||
|
||||
### 4.1 Invocation
|
||||
|
||||
```
|
||||
User types: b bat
|
||||
│
|
||||
▼
|
||||
b.sh (shell function)
|
||||
│
|
||||
├── Auto-update check (once per 24h)
|
||||
│
|
||||
▼
|
||||
bash routes.sh bat
|
||||
```
|
||||
|
||||
`b.sh` is a shell function sourced from `~/.bashrc`. It delegates to `routes.sh` in a subshell.
|
||||
|
||||
### 4.2 Library Loading
|
||||
|
||||
`routes.sh` sources the core libraries in order:
|
||||
|
||||
1. `lib/common.sh` — Logging, `download_file`, `make_temp_dir`, XDG path variables
|
||||
2. `lib/rollback.sh` — Manifest system, `track_file`, `track_dir`, `init_rollback_system`
|
||||
3. `lib/platform.sh` — Distro/arch detection, `pkg_install`/`pkg_remove`
|
||||
4. `lib/shell_config.sh` — `write_env_snippet`, `write_alias_snippet`, etc.
|
||||
5. `lib/registry.sh` — Auto-generated lookup tables (`INSTALLERS`, `INSTALLER_DISPLAYS`, `INSTALLER_STRATEGIES`, `INSTALLER_KEYS`)
|
||||
6. `lib/registry_helpers.sh` — `register_tool`, `registry_add_sys_deps`, etc.
|
||||
7. `lib/github.sh` — GitHub release API helpers
|
||||
8. `lib/plugins.sh` — Plugin system
|
||||
|
||||
All functions are exported to subshells so the installer script inherits them.
|
||||
|
||||
### 4.3 Routing
|
||||
|
||||
`routes.sh` resolves the command. If the argument matches a key in the `INSTALLERS` registry, it is treated as a tool name and routed to `run_ware`:
|
||||
|
||||
```
|
||||
routes.sh receives "bat"
|
||||
│
|
||||
├── Is it a built-in command (help, rb, fall, con, up, gone, etc.)? → No
|
||||
├── Is it a registered tool key in INSTALLERS? → Yes
|
||||
│
|
||||
▼
|
||||
run_ware "bat" "-y"
|
||||
```
|
||||
|
||||
### 4.4 Installer Resolution
|
||||
|
||||
`run_ware` locates the installer script:
|
||||
|
||||
1. **Local check**: Does `$BOOTSTRAP_DIR/installers/install_bat.sh` exist? If yes, use it directly.
|
||||
2. **Remote download**: If not local, download from the primary Git server (`git.adityagupta.dev`). On failure, fall back to the GitHub mirror.
|
||||
|
||||
### 4.5 Pre-Execution Setup
|
||||
|
||||
Before running the installer, `run_ware` calls `setup_uninstaller_context "bat"`, which:
|
||||
|
||||
- Sets `BOOTSTRAP_CURRENT_TOOL=bat`
|
||||
- Sets `BOOTSTRAP_UNINSTALLER_CMDS=$BOOTSTRAP_STATE_DIR/uninstallers/bat.cmds`
|
||||
- If a manifest already exists from a previous interrupted run, it preserves it (resume support).
|
||||
- Otherwise, creates a fresh empty manifest file.
|
||||
|
||||
Signal traps (`INT`, `TERM`) are installed to catch interruptions.
|
||||
|
||||
### 4.6 Installer Execution
|
||||
|
||||
The installer script runs in a `bash` subshell. During execution:
|
||||
|
||||
- **`track_file`/`track_dir`** calls prepend removal commands to `bat.cmds` (LIFO order).
|
||||
- **`write_env_snippet`/`write_alias_snippet`** automatically record their own rollback commands.
|
||||
- **`pkg_install`** installs system packages. If the tool uses `system` strategy, the installer calls `registry_add_sys_deps` to register these packages for reference counting.
|
||||
- **`register_tool "bat" "binary" "$version" "github:sharkdp/bat"`** writes the tool's metadata to `$BOOTSTRAP_STATE_DIR/registry.json` using thread-safe `flock`.
|
||||
|
||||
### 4.7 Post-Execution
|
||||
|
||||
If the installer exits successfully (`exit 0`), `run_ware`:
|
||||
|
||||
1. Calls `mark_install_success "bat"` — appends `INSTALL: bat` to `$BOOTSTRAP_STATE_DIR/history.log`.
|
||||
2. Re-sources `~/.bashrc` so new env/alias snippets take effect immediately.
|
||||
|
||||
If the installer fails or is interrupted, the user is offered a choice:
|
||||
|
||||
- **Rollback (`r`)**: Executes the manifest line-by-line to undo all recorded changes.
|
||||
- **Keep (`k`)**: Preserves partial changes so the user can resume or debug.
|
||||
|
||||
## 5. The Registry (`registry.json`)
|
||||
|
||||
The JSON registry is the central source of truth for installed tool state. It lives at `$BOOTSTRAP_STATE_DIR/registry.json`.
|
||||
|
||||
### 5.1 Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"bat": {
|
||||
"strategy": "binary",
|
||||
"version": "v0.26.1",
|
||||
"source": "github:sharkdp/bat",
|
||||
"installed_at": "2025-06-28T03:15:00Z",
|
||||
"bin": "/home/user/.local/share/bootstrap/bin/bat"
|
||||
},
|
||||
"docker": {
|
||||
"strategy": "system",
|
||||
"installed_at": "2025-06-28T03:20:00Z",
|
||||
"source": "os-package-manager",
|
||||
"system_dependencies": [
|
||||
"arch:docker|debian:docker.io|fedora:docker"
|
||||
]
|
||||
},
|
||||
"node": {
|
||||
"strategy": "managed",
|
||||
"version": "v0.40.5",
|
||||
"source": "github:nvm-sh/nvm",
|
||||
"installed_at": "2025-06-28T03:25:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Thread Safety
|
||||
|
||||
All mutations go through `registry_set`, which acquires an exclusive file lock (`flock -x`) on `registry.json.lock` before applying a `jq` filter. This prevents race conditions if multiple installers run simultaneously.
|
||||
|
||||
## 6. The Auto-Generated Registry (`lib/registry.sh`)
|
||||
|
||||
`lib/registry.sh` is a **build-time artifact**, not a runtime state file. It is auto-generated by `scripts/generate_registry.sh`, which parses the metadata headers from every `installers/install_*.sh` file and produces four Bash data structures:
|
||||
|
||||
| Variable | Type | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| `INSTALLERS` | `declare -A` | Maps tool key → description |
|
||||
| `INSTALLER_DISPLAYS` | `declare -A` | Maps tool key → human-readable display name |
|
||||
| `INSTALLER_STRATEGIES` | `declare -A` | Maps tool key → strategy (`binary`/`managed`/`system`) |
|
||||
| `INSTALLER_KEYS` | Array | Sorted list of all tool keys |
|
||||
|
||||
These are used by `routes.sh` for command routing, by `b ware` (with no arguments) for listing available tools, and by `b help` for the help output.
|
||||
|
||||
**Important**: `lib/registry.sh` must be regenerated whenever an installer is added, removed, or has its metadata header changed. Run:
|
||||
|
||||
```bash
|
||||
./scripts/generate_registry.sh
|
||||
```
|
||||
|
||||
## 7. Uninstallation Flow
|
||||
|
||||
### 7.1 Named Uninstall (`b rb <tool>`)
|
||||
|
||||
```
|
||||
b rb bat
|
||||
│
|
||||
▼
|
||||
uninstall_tool "bat"
|
||||
│
|
||||
├── 1. execute_rollback "bat"
|
||||
│ → Reads bat.cmds line-by-line (LIFO)
|
||||
│ → Executes: rm binary, rm env snippet, rm alias snippet, etc.
|
||||
│ → Deletes bat.cmds
|
||||
│
|
||||
├── 2. Reference-count system dependencies
|
||||
│ → For each dep in registry_get_sys_deps("bat"):
|
||||
│ → Count how many OTHER tools also list this dep
|
||||
│ → If count == 0: pkg_remove the dep
|
||||
│ → If count > 0: keep it
|
||||
│
|
||||
├── 3. registry_remove_tool "bat"
|
||||
│ → Deletes .tools.bat from registry.json
|
||||
│
|
||||
└── 4. Remove "INSTALL: bat" from history.log
|
||||
```
|
||||
|
||||
### 7.2 Multi-Tool Uninstall (`b rb bat,yazi,zoxide`)
|
||||
|
||||
The argument is split on commas and `uninstall_tool` is called for each tool sequentially.
|
||||
|
||||
### 7.3 Bare Rollback (`b rb`)
|
||||
|
||||
Reads the last line of `history.log`. If it is `INSTALL: <tool>`, calls `uninstall_tool` for that tool.
|
||||
|
||||
### 7.4 Savepoint Rollback (`b rb <savepoint>`)
|
||||
|
||||
If the argument doesn't match a registered tool, it is treated as a savepoint name. The system walks `history.log` backwards, uninstalling each tool encountered until it reaches the matching `SAVEPOINT:` entry.
|
||||
|
||||
## 8. Strategy-Specific Patterns
|
||||
|
||||
### 8.1 Binary Strategy Example (Bat)
|
||||
|
||||
```bash
|
||||
# Detect arch, fetch latest tag from GitHub
|
||||
latest_tag=$(github_get_latest_release "sharkdp/bat")
|
||||
|
||||
# Download the matching release asset
|
||||
github_download_asset "sharkdp/bat" "$latest_tag" "bat-${latest_tag}-${target}\.tar\.gz" "$archive"
|
||||
|
||||
# Extract, copy binary to $BOOTSTRAP_BIN, make executable
|
||||
cp "$extract_dir/bat" "$BOOTSTRAP_BIN/bat"
|
||||
chmod +x "$BOOTSTRAP_BIN/bat"
|
||||
track_file "$BOOTSTRAP_BIN/bat"
|
||||
|
||||
# Register
|
||||
register_tool "bat" "binary" "$latest_tag" "github:sharkdp/bat"
|
||||
```
|
||||
|
||||
### 8.2 Managed Strategy Example (Node.js via NVM)
|
||||
|
||||
```bash
|
||||
# Download and extract NVM into $BOOTSTRAP_RUNTIMES/nvm
|
||||
download_file "$nvm_url" "$TMP_DIR/nvm.tar.gz"
|
||||
tar -xzf "$TMP_DIR/nvm.tar.gz" -C "$BOOTSTRAP_RUNTIMES/nvm" --strip-components=1
|
||||
track_dir "$BOOTSTRAP_RUNTIMES/nvm"
|
||||
|
||||
# Write env snippet so NVM loads on shell startup
|
||||
write_env_snippet "node" "$content"
|
||||
|
||||
# Use NVM to install Node LTS
|
||||
nvm install --lts
|
||||
|
||||
# Register (NVM manages the actual node binary)
|
||||
register_tool "node" "managed" "$latest_tag" "github:nvm-sh/nvm"
|
||||
```
|
||||
|
||||
### 8.3 System Strategy Example (Docker)
|
||||
|
||||
```bash
|
||||
# Install via system package manager
|
||||
pkg_install "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Register the dependency for reference counting
|
||||
registry_add_sys_deps "docker" "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Additional system configuration (groups, systemd)
|
||||
sudo usermod -aG docker "$USER"
|
||||
add_rollback_cmd "sudo gpasswd -d $USER docker || true"
|
||||
|
||||
# Register
|
||||
register_tool "docker" "system" "" "os-package-manager"
|
||||
```
|
||||
69
docs/plugin_development.md
Normal file
69
docs/plugin_development.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Plugin Development Guide
|
||||
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike tools (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a sandboxed subshell**.
|
||||
|
||||
This means downloading and invoking a plugin makes no system modifications other than caching the `.sh` file itself. They are fetched only the very first time you invoke them.
|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -3,8 +3,10 @@
|
||||
## 1. Objective
|
||||
Provide a robust rollback mechanism by dynamically generating an uninstallation command list during the installation process. This avoids external parsers, keeps dependencies low, and leverages native Bash execution. It also includes a stateful savepoint system to revert complex environments.
|
||||
|
||||
## 2. Core Concept: Dynamic Command Manifests
|
||||
Instead of tracking state in data files (like JSON), the system procedurally builds a **Command Manifest** (`~/.local/state/bootstrap/uninstallers/<tool>.cmds`) as the installation progresses. Every helper action records its inverse command as an independent line in this manifest.
|
||||
## 2. Core Concept: Procedural Manifests & JSON Registry
|
||||
The system uses a hybrid approach:
|
||||
1. **Procedural Command Manifests**: As the installation progresses, a LIFO script manifest (`$BOOTSTRAP_STATE_DIR/uninstallers/<tool>.cmds`) is built by prepending the inverse commands for each helper action (files, directories, env/alias snippets).
|
||||
2. **Centralized JSON Registry**: Metadata, strategy, and system-level dependencies are tracked in a thread-safe `$BOOTSTRAP_STATE_DIR/registry.json` using `jq`. During uninstallation, this registry is used to reference-count and safely remove shared system dependencies.
|
||||
|
||||
## 3. History & Savepoints (`b fall` and `b rb`)
|
||||
To allow rolling back multiple installations or returning to a known good state, the system maintains a chronological **History Log** acting as a stack.
|
||||
@@ -33,22 +35,24 @@ INSTALL: nvim
|
||||
```
|
||||
|
||||
### C. Bare Rollback (`b rb`)
|
||||
When `b rb` is executed without arguments, it rolls back the single most recent change:
|
||||
When `b rb` is executed without arguments, it rolls back the single most recent installation:
|
||||
1. Reads the last line of the history log (e.g., `INSTALL: nvim`).
|
||||
2. Executes the command manifest for `nvim`.
|
||||
3. Deletes the last line from the history log.
|
||||
2. Runs `uninstall_tool nvim` which executes `nvim.cmds` and cleans up registry entries.
|
||||
|
||||
### D. Savepoint Rollback (`b rb <name>`)
|
||||
When `b rb init` is executed, it rolls back all changes made after that savepoint:
|
||||
### D. Named Rollback (`b rb <tool1>,<tool2>...`)
|
||||
Users can uninstall specific tools by name (e.g., `b rb nvim` or `b rb nvim,yazi`). The system runs the corresponding uninstaller manifests, reference-counts and removes any orphaned system dependencies, and cleans up the history log and registry.
|
||||
|
||||
### E. Savepoint Rollback (`b rb <savepoint>`)
|
||||
If the argument does not match an installed tool in the registry, it is treated as a savepoint:
|
||||
1. Parses the history log from bottom to top.
|
||||
2. For each `INSTALL: <tool>` encountered, it executes the rollback manifest for `<tool>`.
|
||||
3. Stops when it reaches `SAVEPOINT: init`.
|
||||
4. Truncates the history log back to the savepoint.
|
||||
2. For each `INSTALL: <tool>` encountered, it runs `uninstall_tool <tool>`.
|
||||
3. Stops when it reaches the specified `SAVEPOINT: <name>`.
|
||||
4. Truncates the history log back to that savepoint.
|
||||
|
||||
## 4. Required Abstractions & Helper Modifications
|
||||
|
||||
### A. Context Initialization
|
||||
Before executing an installer script, the `b` CLI initializes the command list:
|
||||
Before executing a tool script, the `b` CLI initializes the command list:
|
||||
```bash
|
||||
export BOOTSTRAP_UNINSTALLER_CMDS="$HOME/.local/state/bootstrap/uninstallers/nvim.cmds"
|
||||
mkdir -p "$(dirname "$BOOTSTRAP_UNINSTALLER_CMDS")"
|
||||
@@ -64,22 +68,18 @@ add_rollback_cmd() {
|
||||
}
|
||||
```
|
||||
|
||||
### C. Modifying Existing Helpers
|
||||
Existing helpers automatically generate their own inverse commands.
|
||||
- **`pkg_install`:**
|
||||
### C. Helper Operations
|
||||
Helper functions record their inverse actions directly to the manifest:
|
||||
- **`write_env_snippet` / `write_alias_snippet` / `write_completion_snippet`:**
|
||||
```bash
|
||||
add_rollback_cmd "pkg_remove $pkg"
|
||||
add_rollback_cmd "rm -f \"\$BOOTSTRAP_DIR/env.d/\$snippet_name.sh\""
|
||||
```
|
||||
- **`write_env_snippet` / `write_alias_snippet`:**
|
||||
- **`track_file` / `track_dir`**:
|
||||
```bash
|
||||
add_rollback_cmd "rm -f \"$HOME/.config/bootstrap/env.d/$snippet_name.sh\""
|
||||
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
|
||||
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
|
||||
```
|
||||
|
||||
### D. New File Tracking Helpers
|
||||
```bash
|
||||
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
|
||||
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
|
||||
```
|
||||
Note: distro packages are not tracked via `add_rollback_cmd`. Instead, installers declare them as system dependencies (see below).
|
||||
|
||||
## 5. The Rollback Execution (`b rollback <tool>`)
|
||||
Execution is line-by-line and fault-tolerant, allowing safe recovery even if a user injects a malformed command.
|
||||
@@ -97,21 +97,23 @@ log_success "Rollback complete."
|
||||
```
|
||||
|
||||
## 6. Resilience Against User Modifications
|
||||
Because `b ware <tool>` allows users to modify installation scripts:
|
||||
Because `b ware <tool>` allows users to modify tool scripts:
|
||||
1. **Dynamic Adaptation:** The manifest is built *during* execution, adapting to whatever packages the user manually added.
|
||||
2. **Fault Isolation:** The `eval` loop ensures that a syntax error in one custom rollback step doesn't crash the removal of other tracked packages.
|
||||
|
||||
## 7. Handling Shared Dependencies
|
||||
The `pkg_remove` helper utilizes reference counting via simple text files (e.g., `~/.local/state/bootstrap/packages/curl`).
|
||||
- **On `pkg_install`**: Append tool name.
|
||||
- **On `pkg_remove`**: Remove tool name. If empty, proceed with system uninstallation.
|
||||
System dependencies installed via `pkg_install` must be registered via `registry_add_sys_deps <tool> <dep1> <dep2>...`.
|
||||
When a tool is uninstalled:
|
||||
1. The registry is checked to see if any other installed tool still lists those dependencies.
|
||||
2. If the reference count drops to `0`, the dependency is automatically removed using `pkg_remove`.
|
||||
3. If other tools still depend on it, it is kept.
|
||||
|
||||
## 8. Fault Tolerance, Resumability, and Interrupted Installations
|
||||
|
||||
To handle failures during installation (e.g., network drops, script errors, or user cancellation via `Ctrl+C`), the CLI incorporates a transactional approach that balances **automatic rollback** and **resumability**:
|
||||
|
||||
### A. The Interruption Trap & Prompt
|
||||
When running an installer, the central router (`lib/routes.sh`) traps `SIGINT` and `SIGTERM` signals. If the installation fails or is interrupted:
|
||||
When running a tool, the central router (`lib/routes.sh`) traps `SIGINT` and `SIGTERM` signals. If the installation fails or is interrupted:
|
||||
1. The trap catches the event and stops execution.
|
||||
2. The user is prompted interactively:
|
||||
- **Rollback (r)**: Invokes `execute_rollback <tool>` immediately to clean up all partial modifications.
|
||||
@@ -128,6 +130,6 @@ If the user chooses to **keep** the partial state and runs `b <tool>` again:
|
||||
To make rerunning an interrupted script fast and efficient, installers use `download_file <url> <dest>` instead of raw `curl`:
|
||||
1. It downloads the payload to a central cache directory: `~/.local/state/bootstrap/cache/`.
|
||||
2. It uses `curl -C -` to continue the download from the byte offset where it was interrupted.
|
||||
3. Once completed, it copies the cached file to the installer's temp directory.
|
||||
3. Once completed, it copies the cached file to the tool's temp directory.
|
||||
4. Distro package manager commands (`pkg_install`) and shell snippets (`write_env_snippet`) are naturally idempotent, allowing the script to breeze through already completed steps in milliseconds and resume exactly where the heavy work failed.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ The Bootstrap CLI uses a semantic, git-tag-driven versioning workflow.
|
||||
|
||||
- **Do not manually edit the `VERSION` file.**
|
||||
- Develop normally using [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat: Add new tool`, `fix: Typo in script`).
|
||||
- **Note on Installers:** Adding or modifying installers in the `installers/` directory **does not** require a version bump. They are fetched dynamically from the registry when a user runs `b ware <tool>`.
|
||||
- **Note on Tools:** Adding or modifying tools in the `tools/` directory **does not** require a version bump. They are fetched dynamically from the registry when a user runs `b ware <tool>`.
|
||||
|
||||
## 2. When to Cut a Release
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
#
|
||||
# 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)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
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"
|
||||
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
|
||||
}
|
||||
|
||||
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'"
|
||||
}
|
||||
|
||||
main() {
|
||||
install_bat
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Bat installation and configuration complete."
|
||||
log_info "Please close and reopen your terminal or run: source ~/.bashrc to apply changes."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: starship
|
||||
# DisplayName: Starship
|
||||
# Description: Install Starship shell prompt
|
||||
#
|
||||
# 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)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_starship() {
|
||||
if has_command starship || [ -f "$HOME/.local/bin/starship" ]; then
|
||||
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)
|
||||
local arch=""
|
||||
case "$raw_arch" in
|
||||
x86_64) arch="x86_64" ;;
|
||||
arm64) arch="aarch64" ;;
|
||||
*) log_error "Unsupported Linux architecture: $raw_arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
local target="${arch}-unknown-linux-musl"
|
||||
|
||||
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="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}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
download_file "$download_url" "$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"
|
||||
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"
|
||||
}
|
||||
|
||||
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)"'
|
||||
}
|
||||
|
||||
main() {
|
||||
install_starship
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Starship installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: yazi
|
||||
# DisplayName: Yazi
|
||||
# Description: Install Yazi terminal file manager and dependencies
|
||||
#
|
||||
# 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)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
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'
|
||||
# Shell wrapper for yazi to change directory on exit
|
||||
y() {
|
||||
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")"
|
||||
yazi "$@" --cwd-file="$tmp"
|
||||
if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
|
||||
builtin cd -- "$cwd"
|
||||
fi
|
||||
rm -f -- "$tmp"
|
||||
}
|
||||
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."
|
||||
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
|
||||
}
|
||||
|
||||
main() {
|
||||
install_yazi
|
||||
add_y_wrapper
|
||||
echo
|
||||
log_success "Yazi installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -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
|
||||
@@ -72,6 +81,7 @@ make_temp_dir() {
|
||||
version_lt() {
|
||||
[ "$1" = "$2" ] && return 1
|
||||
local IFS=.
|
||||
# shellcheck disable=SC2206
|
||||
local i ver1=($1) ver2=($2)
|
||||
for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done
|
||||
for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done
|
||||
@@ -88,7 +98,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 +133,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
58
lib/github.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# GitHub API helper functions for Bootstrap tools
|
||||
|
||||
# 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
|
||||
@@ -11,6 +11,7 @@ if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
# Assumes common.sh is in the same directory as platform.sh
|
||||
# We resolve the directory of the current script
|
||||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
. "$_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
@@ -86,18 +87,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 +159,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
180
lib/plugins.sh
Normal 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
|
||||
parse_plugin_manifest < "$temp_file" >> "$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=$(tr -d '[:space:]' < "$BOOTSTRAP_DIR/VERSION")
|
||||
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
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
# shellcheck shell=bash
|
||||
# shellcheck disable=SC2034
|
||||
# This file is auto-generated by scripts/generate_registry.sh. Do not edit manually.
|
||||
|
||||
declare -A INSTALLERS=(
|
||||
[agy]="Antigravity CLI"
|
||||
[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"
|
||||
[pnpm]="pnpm package manager"
|
||||
@@ -19,6 +24,9 @@ declare -A INSTALLER_DISPLAYS=(
|
||||
[agy]="Antigravity"
|
||||
[asciicinema]="asciicinema"
|
||||
[bat]="Bat"
|
||||
[docker]="Docker"
|
||||
[hyperfine]="Hyperfine"
|
||||
[lazygit]="lazygit"
|
||||
[node]="Node"
|
||||
[nvim]="Neovim"
|
||||
[pnpm]="Pnpm"
|
||||
@@ -30,4 +38,22 @@ 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"
|
||||
[hyperfine]="binary"
|
||||
[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 hyperfine lazygit node nvim pnpm rust starship uv yay yazi zoxide)
|
||||
|
||||
133
lib/registry_helpers.sh
Normal file
133
lib/registry_helpers.sh
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Central routing script for bootstrap installers.
|
||||
# Central routing script for bootstrap tools.
|
||||
# This file is updated automatically by the 'b' command.
|
||||
|
||||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
|
||||
@@ -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"
|
||||
@@ -57,8 +69,8 @@ run_ware() {
|
||||
display_name="$(echo "${tool:0:1}" | tr '[:lower:]' '[:upper:]')${tool:1}"
|
||||
fi
|
||||
|
||||
# Check for local installer first
|
||||
local local_installer="$BOOTSTRAP_DIR/installers/install_${tool}.sh"
|
||||
# Check for local tool definition first
|
||||
local local_installer="$BOOTSTRAP_DIR/tools/${tool}/tool.sh"
|
||||
|
||||
if [ "$bypass_edit" = "true" ] && [ -f "$local_installer" ]; then
|
||||
log_info "Running ${display_name} installer..."
|
||||
@@ -74,7 +86,7 @@ run_ware() {
|
||||
else
|
||||
BOOTSTRAP_BASE_URL="${BOOTSTRAP_BASE_URL:-https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master}"
|
||||
BOOTSTRAP_FALLBACK_URL="${BOOTSTRAP_FALLBACK_URL:-https://raw.githubusercontent.com/sortedcord/bootstrap/refs/heads/master}"
|
||||
local installer_path="installers/install_${tool}.sh"
|
||||
local installer_path="tools/${tool}/tool.sh"
|
||||
local download_status=0
|
||||
|
||||
log_info "Downloading ${display_name} installer..."
|
||||
@@ -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"
|
||||
@@ -198,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" "$@"
|
||||
@@ -244,7 +270,7 @@ for script in "${SCRIPTS[@]}"; do
|
||||
fi
|
||||
;;
|
||||
fall)
|
||||
local savepoint_name="${1:-}"
|
||||
savepoint_name="${1:-}"
|
||||
if [ -z "$savepoint_name" ]; then
|
||||
log_error "Usage: b fall <savepoint_name>"
|
||||
exit 1
|
||||
@@ -253,18 +279,36 @@ for script in "${SCRIPTS[@]}"; do
|
||||
exit 0
|
||||
;;
|
||||
rb)
|
||||
local target="${1:-}"
|
||||
target="${1:-}"
|
||||
if [ -z "$target" ]; then
|
||||
rollback_bare
|
||||
else
|
||||
rollback_to_savepoint "$target"
|
||||
# Split comma-separated targets
|
||||
IFS=',' read -ra TARGETS <<< "$target"
|
||||
|
||||
if [ ${#TARGETS[@]} -gt 1 ]; then
|
||||
for t in "${TARGETS[@]}"; do
|
||||
uninstall_tool "$t"
|
||||
done
|
||||
else
|
||||
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
|
||||
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
|
||||
|
||||
@@ -9,6 +9,7 @@ _LIB_SHELL_CONFIG_SOURCED=1
|
||||
# Source common utilities if not already loaded
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
. "$_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
@@ -158,6 +159,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
|
||||
@@ -166,8 +195,15 @@ create_fd_symlink() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Source the bashrc file to reload configurations
|
||||
source_bashrc() {
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
log_info "Re-sourcing ~/.bashrc..."
|
||||
# shellcheck source=/dev/null
|
||||
. "$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 write_completion_snippet remove_completion_snippet source_bashrc
|
||||
|
||||
28
plugins.json
Normal file
28
plugins.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
242
plugins/auth.sh
Normal file
242
plugins/auth.sh
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Authentication & Provisioning Plugin for Bootstrap CLI
|
||||
# Handles requester (b me) and approver (b trust) flows.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure dependencies are met
|
||||
pkg_install "arch:openssh|debian:openssh-client|fedora:openssh-clients" "curl" "jq" "age"
|
||||
|
||||
|
||||
# Ensure public key exists next to private key for ssh-keygen -Y sign
|
||||
ensure_pubkey_exists() {
|
||||
local priv_key="$1"
|
||||
local pub_key="${priv_key}.pub"
|
||||
if [ ! -f "$pub_key" ]; then
|
||||
ssh-keygen -y -f "$priv_key" > "$pub_key"
|
||||
fi
|
||||
}
|
||||
|
||||
COMMAND="${1:-}"
|
||||
if [ -z "$COMMAND" ]; then
|
||||
echo "Usage: b auth <me|trust> [args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
|
||||
# Defaults
|
||||
SERVER_URL="https://b.adityagupta.dev/auth"
|
||||
KEY_DIR="$HOME/.config/bootstrap-client"
|
||||
POLL_INTERVAL=5
|
||||
ADMIN_KEY="$HOME/.ssh/id_ed25519"
|
||||
USER_CODE=""
|
||||
|
||||
if [ "$COMMAND" = "trust" ]; then
|
||||
if [ $# -lt 1 ]; then
|
||||
log_error "user_code is required for trust."
|
||||
echo "Usage: b trust <user_code> [--server <server_url>] [--admin-key <path>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
USER_CODE="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Parse remaining arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server)
|
||||
SERVER_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--key-dir)
|
||||
KEY_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
POLL_INTERVAL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--admin-key)
|
||||
ADMIN_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$COMMAND" = "me" ]; then
|
||||
mkdir -p "$KEY_DIR"
|
||||
local_key="$KEY_DIR/id_ed25519"
|
||||
|
||||
if [ ! -f "$local_key" ]; then
|
||||
log_info "Generating local Ed25519 key pair under $KEY_DIR..."
|
||||
ssh-keygen -t ed25519 -N "" -f "$local_key" >/dev/null
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$local_key"
|
||||
pub_key=$(cat "${local_key}.pub")
|
||||
hostname=$(hostname 2>/dev/null || uname -n)
|
||||
os=$(uname -s 2>/dev/null || echo "linux")
|
||||
|
||||
# Safely construct JSON payload
|
||||
json_payload=$(jq -n \
|
||||
--arg hn "$hostname" \
|
||||
--arg os "$os" \
|
||||
--arg pk "$pub_key" \
|
||||
'{hostname: $hn, os: $os, public_key: $pk}')
|
||||
|
||||
log_info "Registering device with $SERVER_URL..."
|
||||
|
||||
register_response=$(curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$json_payload" \
|
||||
"$SERVER_URL/api/register")
|
||||
|
||||
user_code=$(echo "$register_response" | jq -r '.user_code // empty')
|
||||
challenge_nonce=$(echo "$register_response" | jq -r '.challenge_nonce // empty')
|
||||
|
||||
if [ -z "$user_code" ] || [ -z "$challenge_nonce" ]; then
|
||||
log_error "Failed to retrieve registration codes from server response."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
log_success "Device registration initiated successfully!"
|
||||
echo "Please authorize this device on your administrator machine using:"
|
||||
echo " b trust $user_code --server $SERVER_URL"
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Verification Code: $user_code"
|
||||
echo "--------------------------------------------------------"
|
||||
log_info "Waiting for administrator approval (polling every ${POLL_INTERVAL}s)..."
|
||||
|
||||
# Prepare challenge poll file signing
|
||||
temp_nonce_file=$(mktemp)
|
||||
temp_sig_file="${temp_nonce_file}.sig"
|
||||
echo -n "$challenge_nonce" > "$temp_nonce_file"
|
||||
|
||||
# Ensure cleanup of temp files
|
||||
cleanup() {
|
||||
rm -f "$temp_nonce_file" "$temp_sig_file"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
rm -f "$temp_sig_file"
|
||||
|
||||
# Sign challenge nonce
|
||||
if ! ssh-keygen -Y sign -f "$local_key" -n "bootstrap" "$temp_nonce_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing of challenge nonce failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get raw base64 from armored signature file
|
||||
signature_b64=$(grep -v '^-' "$temp_sig_file" | tr -d '\n')
|
||||
|
||||
poll_payload=$(jq -n \
|
||||
--arg uc "$user_code" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, signature: $sig}')
|
||||
|
||||
poll_out=$(mktemp)
|
||||
http_code=$(curl -s -o "$poll_out" -w "%{http_code}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$poll_payload" \
|
||||
"$SERVER_URL/api/challenge/poll")
|
||||
|
||||
poll_body=$(cat "$poll_out")
|
||||
rm -f "$poll_out"
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
enc_secrets=$(echo "$poll_body" | jq -r '.encrypted_secrets // empty')
|
||||
if [ -n "$enc_secrets" ] && [ "$enc_secrets" != "null" ]; then
|
||||
log_success "Device approved by administrator! Decrypting secrets payload..."
|
||||
|
||||
decrypted_file="$KEY_DIR/secrets.decrypted"
|
||||
if echo "$enc_secrets" | base64 -d | age --decrypt -i "$local_key" > "$decrypted_file" 2>/dev/null; then
|
||||
log_success "Secrets successfully provisioned and written to: $decrypted_file"
|
||||
cat "$decrypted_file"
|
||||
break
|
||||
else
|
||||
log_error "Decryption using age failed. Please ensure the private key has not been altered."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
|
||||
elif [ "$COMMAND" = "trust" ]; then
|
||||
if [ ! -f "$ADMIN_KEY" ]; then
|
||||
log_error "Admin private key not found at: $ADMIN_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$ADMIN_KEY"
|
||||
|
||||
log_info "Fetching pending device details for user code: $USER_CODE"
|
||||
pending_response=$(curl -fsSL "$SERVER_URL/api/pending/$USER_CODE")
|
||||
|
||||
requester_pub_key=$(echo "$pending_response" | jq -r '.public_key // empty')
|
||||
if [ -z "$requester_pub_key" ]; then
|
||||
log_error "No pending registration found for code '$USER_CODE'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Pending Device Public Key:"
|
||||
echo "$requester_pub_key"
|
||||
echo "--------------------------------------------------------"
|
||||
|
||||
# Prompt for confirmation (read from tty to support pipeline scenarios)
|
||||
read -r -p "Do you trust and approve this device? [y/N]: " confirm_choice </dev/tty || confirm_choice="N"
|
||||
if [[ ! "$confirm_choice" =~ ^[Yy]$ ]]; then
|
||||
log_warn "Approval aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate signature of the requester's public key
|
||||
temp_pubkey_file=$(mktemp)
|
||||
temp_pubkey_sig_file="${temp_pubkey_file}.sig"
|
||||
echo -n "$requester_pub_key" > "$temp_pubkey_file"
|
||||
|
||||
# Cleanup trap
|
||||
cleanup_trust() {
|
||||
rm -f "$temp_pubkey_file" "$temp_pubkey_sig_file"
|
||||
}
|
||||
trap cleanup_trust EXIT INT TERM
|
||||
|
||||
if ! ssh-keygen -Y sign -f "$ADMIN_KEY" -n "bootstrap" "$temp_pubkey_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing using administrator key failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
signature_b64=$(grep -v '^-' "$temp_pubkey_sig_file" | tr -d '\n')
|
||||
|
||||
# Get fingerprint
|
||||
admin_pubkey_str=$(ssh-keygen -y -f "$ADMIN_KEY")
|
||||
temp_admin_pub=$(mktemp)
|
||||
echo "$admin_pubkey_str" > "$temp_admin_pub"
|
||||
approver_fingerprint=$(ssh-keygen -lf "$temp_admin_pub" | awk '{print $2}')
|
||||
rm -f "$temp_admin_pub"
|
||||
|
||||
# Prepare payload
|
||||
approve_payload=$(jq -n \
|
||||
--arg uc "$USER_CODE" \
|
||||
--arg fp "$approver_fingerprint" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, approver_public_key_fingerprint: $fp, signature: $sig}')
|
||||
|
||||
log_info "Submitting cryptographic approval to server..."
|
||||
curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$approve_payload" \
|
||||
"$SERVER_URL/api/approve"
|
||||
|
||||
log_success "Device with code $USER_CODE has been approved."
|
||||
fi
|
||||
74
plugins/sysinfo.sh
Normal file
74
plugins/sysinfo.sh
Normal 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
123
plugins/todo.sh
Normal 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)
|
||||
true > "$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
32
plugins/weather.sh
Normal 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 "$@"
|
||||
107
readme.md
107
readme.md
@@ -79,7 +79,7 @@ It automatically fuzzy-finds the folder in case there is no exact match. Also, i
|
||||
|
||||
### Rollbacks and Savepoints (`b rb` and `b fall`)
|
||||
|
||||
Bootstrap CLI features a powerful, procedural rollback system. It strictly tracks every extracted binary, configuration snippet, and package manager transaction to ensure your environment stays clean.
|
||||
Bootstrap CLI features a robust rollback and uninstallation system driven by procedural manifests and a centralized JSON registry.
|
||||
|
||||
To safely uninstall the very last tool you installed (including wiping its shell paths and aliases):
|
||||
|
||||
@@ -87,11 +87,21 @@ To safely uninstall the very last tool you installed (including wiping its shell
|
||||
b rb
|
||||
```
|
||||
|
||||
To uninstall specific tools by name (supporting single or comma-separated lists of tools):
|
||||
|
||||
```bash
|
||||
b rb nvim
|
||||
b rb bat,yazi,zoxide
|
||||
```
|
||||
|
||||
Shared system package dependencies are reference-counted in the registry and will be automatically cleaned up only when the last tool depending on them is removed.
|
||||
|
||||
To create a named savepoint before experimenting with your setup:
|
||||
|
||||
```bash
|
||||
b fall pre_dev_setup
|
||||
```
|
||||
*(Note: Savepoint names cannot conflict with the names of any available tools).*
|
||||
|
||||
To completely roll back all installations made after that savepoint, restoring your system back to that exact state:
|
||||
|
||||
@@ -109,6 +119,82 @@ b up
|
||||
b up --force
|
||||
```
|
||||
|
||||
### Client Authentication and Provisioning (`b me` and `b trust`)
|
||||
|
||||
Bootstrap CLI supports a secure, cryptographic client onboarding and secrets provisioning flow. This allows you to securely register new requester devices and authorize them via an administrator device.
|
||||
|
||||
For the complete protocol specification, including the underlying REST API and cryptographic details, see the [Client Authentication and Provisioning Specification](docs/client_spec_auth.md).
|
||||
|
||||
#### 1. Device Registration (`b me`)
|
||||
To initiate registration on a new, unprovisioned machine, run:
|
||||
|
||||
```bash
|
||||
b me [--server <server_url>] [--key-dir <dir>] [--poll-interval <seconds>]
|
||||
```
|
||||
|
||||
This generates a local SSH Ed25519 key pair, registers your device in a pending state with the authentication server, displays a short `user_code`, and starts polling for administrator approval. Once approved, the encrypted payload containing secrets is retrieved, decrypted locally via `age`, and saved to `<key-dir>/secrets.decrypted`.
|
||||
|
||||
#### 2. Request Approval (`b trust`)
|
||||
To authorize a pending device, run this command on an already provisioned administrator machine:
|
||||
|
||||
```bash
|
||||
b trust <user_code> [--server <server_url>] [--admin-key <path_to_admin_private_key>]
|
||||
```
|
||||
|
||||
This retrieves the pending device's public key, prompts the administrator for confirmation, cryptographically signs the public key using the admin key, and submits the approval signature back to the authentication server.
|
||||
|
||||
## Plugins (`b <plugin_name>`)
|
||||
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike tools (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a subshell**.
|
||||
|
||||
Downloading and invoking a plugin makes no system modifications other than caching the `.sh` file itself. They are fetched only the very first time you invoke them.
|
||||
|
||||
### Official Plugins
|
||||
|
||||
Bootstrap comes pre-configured with a set of official plugins ready to use out-of-the-box:
|
||||
|
||||
* **`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:
|
||||
@@ -125,6 +211,20 @@ b gone -f
|
||||
|
||||
Then reload your shell configuration or run `unset -f b` to clear the function definition from your current terminal session.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
Bootstrap isolates all of its components and installed software inside XDG-compliant directories. You can override these variables in your shell environment, but they default to the following:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| `BOOTSTRAP_DIR` | `~/.config/bootstrap` | Core configuration, libraries, shell snippets (`env.d`, `aliases.d`, `completions.d`), and local installers |
|
||||
| `BOOTSTRAP_DATA_DIR` | `~/.local/share/bootstrap` | Active installation bin files, optional components, and runtime installations |
|
||||
| `BOOTSTRAP_STATE_DIR` | `~/.local/state/bootstrap` | Stateful registry, logs, and rollback/uninstall manifests |
|
||||
| `BOOTSTRAP_CACHE_DIR` | `~/.cache/bootstrap` | Downloaded files cache and temporary workspace files |
|
||||
| `BOOTSTRAP_BIN` | `$BOOTSTRAP_DATA_DIR/bin` | Target directory for all installed tools' binaries |
|
||||
| `BOOTSTRAP_OPT` | `$BOOTSTRAP_DATA_DIR/opt` | Destination for complex, multi-file software distributions |
|
||||
| `BOOTSTRAP_RUNTIMES` | `$BOOTSTRAP_DATA_DIR/runtimes` | Workspace for programming language runtime managers (e.g. NVM) |
|
||||
|
||||
## Development
|
||||
|
||||
If you are developing this tool locally:
|
||||
@@ -149,7 +249,6 @@ The scripts are intentionally straightforward Bash scripts that can be inspected
|
||||
|
||||
Potential additions include:
|
||||
|
||||
* RSA based authentication
|
||||
* Development environment bootstrap
|
||||
* Workstation setup
|
||||
* Server provisioning
|
||||
@@ -164,8 +263,8 @@ Running scripts directly from the internet is convenient but should always be ap
|
||||
### Why Bootstrap is More Secure
|
||||
Instead of blindly piping large, third-party installation scripts (often hundreds or thousands of lines of unscrutinized code) directly from the internet into your shell, Bootstrap uses a **scrutinized and simplified** approach:
|
||||
|
||||
1. **Audited & Minimised**: Every script in `installers/` has been scrutinized, refactored, and stripped of redundant compatibility layers (like Windows/macOS support or complex environment checks), leaving only the essential, readable logic required for your setup.
|
||||
2. **Controlled Execution**: Since installer scripts are hosted locally in your cloned repository, you can review every line of code before executing it. You are never subject to silent, upstream changes to installers.
|
||||
1. **Audited & Minimised**: Every script in `tools/` has been scrutinized, refactored, and stripped of redundant compatibility layers (like Windows/macOS support or complex environment checks), leaving only the essential, readable logic required for your setup.
|
||||
2. **Controlled Execution**: Since tool scripts are hosted locally in your cloned repository, you can review every line of code before executing it. You are never subject to silent, upstream changes to tools.
|
||||
3. **No Raw Pipes**: We download official binary releases directly to verified temporary locations rather than running arbitrary third-party script pipelines.
|
||||
|
||||
If you want to audit the core Bootstrap CLI itself before running it:
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
INSTALLERS_DIR="$REPO_DIR/installers"
|
||||
TOOLS_DIR="$REPO_DIR/tools"
|
||||
REGISTRY_FILE="$REPO_DIR/lib/registry.sh"
|
||||
|
||||
echo "==> Generating registry.sh..."
|
||||
@@ -13,42 +13,54 @@ 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
|
||||
for f in "$TOOLS_DIR"/*/tool.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
|
||||
disp_name=$(grep -E "^# DisplayName:" "$f" | head -n1 | sed -E 's/^# DisplayName:\s*//I')
|
||||
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
|
||||
|
||||
# Sort keys alphabetically
|
||||
sorted_keys=($(printf '%s\n' "${keys[@]}" | sort))
|
||||
mapfile -t sorted_keys < <(printf '%s\n' "${keys[@]}" | sort)
|
||||
|
||||
{
|
||||
echo "# shellcheck shell=bash"
|
||||
echo "# shellcheck disable=SC2034"
|
||||
echo "# This file is auto-generated by scripts/generate_registry.sh. Do not edit manually."
|
||||
echo ""
|
||||
echo "declare -A INSTALLERS=("
|
||||
for k in "${sorted_keys[@]}"; do
|
||||
# Escape any double quotes in description
|
||||
escaped_desc=$(echo "${tools_desc[$k]}" | sed 's/"/\\"/g')
|
||||
escaped_desc="${tools_desc[$k]//\"/\\\"}"
|
||||
echo " [$k]=\"$escaped_desc\""
|
||||
done
|
||||
echo ")"
|
||||
echo ""
|
||||
echo "declare -A INSTALLER_DISPLAYS=("
|
||||
for k in "${sorted_keys[@]}"; do
|
||||
escaped_disp=$(echo "${tools_disp[$k]}" | sed 's/"/\\"/g')
|
||||
escaped_disp="${tools_disp[$k]//\"/\\\"}"
|
||||
echo " [$k]=\"$escaped_disp\""
|
||||
done
|
||||
echo ")"
|
||||
echo ""
|
||||
echo "declare -A INSTALLER_STRATEGIES=("
|
||||
for k in "${sorted_keys[@]}"; do
|
||||
escaped_strat="${tools_strat[$k]//\"/\\\"}"
|
||||
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"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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() {
|
||||
@@ -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() {
|
||||
79
tools/bat/tool.sh
Normal file
79
tools/bat/tool.sh
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
# Strategy: binary
|
||||
#
|
||||
# Bat Installer Script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_bat() {
|
||||
if has_command bat; then
|
||||
if ! confirm "Bat is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Bat installation."
|
||||
return
|
||||
fi
|
||||
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() {
|
||||
|
||||
write_alias_snippet "bat" "alias cat='bat --paging=never -p'"
|
||||
}
|
||||
|
||||
main() {
|
||||
install_bat
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Bat installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
69
tools/docker/tool.sh
Normal file
69
tools/docker/tool.sh
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/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 and explicitly register them for reference-counted rollback
|
||||
pkg_install "arch:docker|debian:docker.io|fedora:docker"
|
||||
registry_add_sys_deps "docker" "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 "$@"
|
||||
103
tools/hyperfine/tool.sh
Normal file
103
tools/hyperfine/tool.sh
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: hyperfine
|
||||
# DisplayName: Hyperfine
|
||||
# Description: Command-line benchmarking tool
|
||||
# Strategy: binary
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_hyperfine() {
|
||||
if has_command 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
|
||||
|
||||
# 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=$(github_get_latest_release "sharkdp/hyperfine")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v1.20.0"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
else
|
||||
log_info "Latest Hyperfine version found: $latest_tag"
|
||||
fi
|
||||
|
||||
local archive="$TMP_DIR/hyperfine.tar.gz"
|
||||
github_download_asset "sharkdp/hyperfine" "$latest_tag" "hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz" "$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
|
||||
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 $BOOTSTRAP_BIN
|
||||
local target_dir="$BOOTSTRAP_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
|
||||
|
||||
register_tool "hyperfine" "binary" "$latest_tag" "github:sharkdp/hyperfine"
|
||||
}
|
||||
|
||||
main() {
|
||||
install_hyperfine
|
||||
|
||||
echo
|
||||
log_success "Hyperfine installation complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
72
tools/lazygit/tool.sh
Executable file
72
tools/lazygit/tool.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/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
|
||||
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 "$@"
|
||||
@@ -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
|
||||
|
||||
@@ -29,12 +24,13 @@ install_nvm() {
|
||||
if ! has_command tar; then
|
||||
log_info "tar not found. Installing tar..."
|
||||
pkg_install tar
|
||||
registry_add_sys_deps "node" "tar"
|
||||
fi
|
||||
|
||||
# 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 +43,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 +67,11 @@ 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"
|
||||
# shellcheck source=/dev/null
|
||||
. "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh"
|
||||
else
|
||||
log_error "Could not load NVM to install Node.js."
|
||||
return 1
|
||||
@@ -95,6 +87,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 +99,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 || grep '"version":' "$BOOTSTRAP_RUNTIMES/nvm/package.json" | 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
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# 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 +30,18 @@ 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" \
|
||||
"arch:gcc|debian:build-essential|fedora:gcc" \
|
||||
"arch:python|debian:python3|fedora:python3" \
|
||||
"debian:python3-pip|fedora:python3-pip" \
|
||||
"debian:python3-venv" \
|
||||
"fedora:gcc-c++"
|
||||
|
||||
registry_add_sys_deps "nvim" \
|
||||
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 +84,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 +119,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"'
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# 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 +16,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 +49,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 +78,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() {
|
||||
76
tools/starship/tool.sh
Normal file
76
tools/starship/tool.sh
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# Tool: starship
|
||||
# DisplayName: Starship
|
||||
# Description: Install Starship shell prompt
|
||||
# Strategy: binary
|
||||
#
|
||||
# Starship Installer Script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_starship() {
|
||||
if has_command starship || [ -f "$HOME/.local/bin/starship" ]; then
|
||||
log_info "Starship is already installed."
|
||||
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
|
||||
|
||||
local target="${arch}-unknown-linux-musl"
|
||||
|
||||
log_info "Fetching latest Starship version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "starship/starship")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="latest"
|
||||
fi
|
||||
|
||||
log_info "Downloading Starship ${latest_tag}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
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="$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() {
|
||||
|
||||
|
||||
write_env_snippet "starship" 'eval "$(starship init bash)"'
|
||||
}
|
||||
|
||||
main() {
|
||||
install_starship
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Starship installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,17 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# 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 +24,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 +44,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 +67,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)"'
|
||||
}
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────
|
||||
@@ -43,6 +38,7 @@ install_yay() {
|
||||
else
|
||||
log_info "Dependencies (git and base-devel) are already present. Skipping package installation."
|
||||
fi
|
||||
registry_add_sys_deps "yay" "git" "base-devel"
|
||||
|
||||
log_info "Cloning yay-bin repository..."
|
||||
local clone_dir
|
||||
@@ -66,6 +62,7 @@ install_yay() {
|
||||
cd "$orig_dir"
|
||||
log_info "Cleaning up installer directory..."
|
||||
rm -rf "$clone_dir"
|
||||
register_tool "yay" "system" "" "aur:yay-bin"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
105
tools/yazi/tool.sh
Executable file
105
tools/yazi/tool.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: yazi
|
||||
# DisplayName: Yazi
|
||||
# Description: Install Yazi terminal file manager and dependencies
|
||||
# Strategy: binary
|
||||
#
|
||||
# Yazi Installer Script
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
add_y_wrapper() {
|
||||
|
||||
local wrapper_content
|
||||
wrapper_content=$(cat << 'EOF'
|
||||
# Shell wrapper for yazi to change directory on exit
|
||||
y() {
|
||||
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")"
|
||||
yazi "$@" --cwd-file="$tmp"
|
||||
if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
|
||||
builtin cd -- "$cwd"
|
||||
fi
|
||||
rm -f -- "$tmp"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
write_alias_snippet "yazi" "$wrapper_content"
|
||||
}
|
||||
|
||||
install_yazi() {
|
||||
if has_command yazi; then
|
||||
if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Yazi installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure required extraction tools are installed
|
||||
if ! has_command unzip; then
|
||||
log_info "unzip not found. Installing unzip..."
|
||||
pkg_install unzip
|
||||
registry_add_sys_deps "yazi" "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() {
|
||||
install_yazi
|
||||
add_y_wrapper
|
||||
echo
|
||||
log_success "Yazi installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,25 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
# 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
|
||||
@@ -29,6 +19,7 @@ install_fzf() {
|
||||
|
||||
log_info "fzf not found. Installing fzf..."
|
||||
pkg_install fzf
|
||||
registry_add_sys_deps "zoxide" "fzf"
|
||||
}
|
||||
|
||||
install_zoxide() {
|
||||
@@ -36,24 +27,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)"'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user