Compare commits
5 Commits
fix/refere
...
feat/auth-
| Author | SHA1 | Date | |
|---|---|---|---|
| db6ec1c1c8 | |||
| ed56ef95a9 | |||
| 671cf7f818 | |||
| f5227569b1 | |||
| 15d3a1a59d |
@@ -20,7 +20,7 @@ bootstrap/
|
||||
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
|
||||
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install(), pkg_check(), pkg_remove()
|
||||
│ ├── rollback.sh # Rollback tracking (track_file, track_dir, add_rollback_cmd)
|
||||
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet
|
||||
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet, write_completion_snippet
|
||||
│ ├── registry.sh # Dynamically generated installer registry
|
||||
│ └── routes.sh # Central router script
|
||||
├── commands/ # Non-installer commands (help, con, uninstall)
|
||||
@@ -35,17 +35,26 @@ bootstrap/
|
||||
|
||||
When adding a new installer named `<name>`:
|
||||
|
||||
### Step 1: Create the installer script
|
||||
### Step 1: Analyze user request & gather details
|
||||
|
||||
Create `installers/install_<name>.sh` using the template below.
|
||||
When the user asks you to create an installer, they often provide either an official `curl` install script or a link to a `.tar.gz` release.
|
||||
You MUST do the following before writing the script:
|
||||
|
||||
If the user provides an official install or curl script in the prompt:
|
||||
- Read and analyze the script.
|
||||
- Remove redundant parts like macOS and Windows compatibility.
|
||||
- Strip unnecessary shell boilerplate, self-update logic, and other bloat.
|
||||
- Implement only the essential Linux installation logic inside the `install_<name>` function.
|
||||
**If the user provides a `curl` command or link to an install script:**
|
||||
- Execute the `curl` command (or use `read_url_content`) to fetch the script and analyze what it actually does under the hood.
|
||||
- Do NOT simply execute the official script blindly in your installer.
|
||||
- Re-write its functionality according to the conventions of the bootstrap installer.
|
||||
- Strip away redundant code, OS checks for macOS/Windows (we only target Linux), and unnecessary shell configuration logic.
|
||||
- Implement only the core, essential Linux installation logic inside the `install_<name>` function.
|
||||
|
||||
### Step 2: Add metadata comments to the top of your installer script
|
||||
**If the user provides a link to a `.tar.gz` (or `.zip`):**
|
||||
- First, download the archive to a temporary directory and extract it to inspect its contents.
|
||||
- Analyze the extracted folder structure to decide what needs to be installed (e.g., binaries, man pages, completions) and what should be ignored/deleted.
|
||||
- Write the `install_<name>` function to download, extract, and copy only those essential files. (Use `download_file` and temporary directories, see "Resumable Download and Extraction" below).
|
||||
|
||||
### Step 2: Create the installer script
|
||||
|
||||
### Step 3: Add metadata comments to the top of your installer script
|
||||
|
||||
At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following three metadata headers:
|
||||
```bash
|
||||
@@ -56,15 +65,15 @@ At the top of your new installer script, right below `#!/usr/bin/env bash`, add
|
||||
|
||||
The central router `lib/routes.sh` and autocomplete function in `b.sh` will dynamically parse this metadata from all `install_*.sh` scripts to register the installer and keys automatically! No manual edits to `lib/routes.sh` or `b.sh` are required.
|
||||
|
||||
### Step 3: Implement Rollback Tracking (Crucial)
|
||||
### Step 4: Implement Rollback Tracking (Crucial)
|
||||
|
||||
To ensure the user can seamlessly use `b rb <name>`, all manual modifications must be tracked:
|
||||
- When extracting binaries to `~/.local/bin/`, use `track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary"`.
|
||||
- When extracting binaries to `~/.local/bin/`, use `track_file "$HOME/.local/bin/binary"`.
|
||||
- When creating directories like `~/.config/tool/`, use `track_dir "$HOME/.config/tool"`.
|
||||
- When running manual apt/dnf/npm commands, log their inverses: `add_rollback_cmd "sudo npm uninstall -g package"`.
|
||||
Note: `pkg_install`, `write_env_snippet`, and `write_alias_snippet` will automatically track themselves.
|
||||
Note: `pkg_install`, `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` will automatically track themselves.
|
||||
|
||||
### Step 4: Verify (optional)
|
||||
### Step 5: Verify (optional)
|
||||
|
||||
Verify that the installer works and appears in the help output:
|
||||
- Run `b all` to confirm it appears in the help list.
|
||||
@@ -116,8 +125,8 @@ install_<name>() {
|
||||
# Or manual downloads (always use download_file for resumability!):
|
||||
# local url="https://..."
|
||||
# download_file "$url" "$TMP_DIR/binary"
|
||||
# cp "$TMP_DIR/binary" "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary"
|
||||
# track_file "${BOOTSTRAP_BIN:-$HOME/.local/share/bootstrap/bin}/binary" # Important for rollback!
|
||||
# cp "$TMP_DIR/binary" "$HOME/.local/bin/binary"
|
||||
# track_file "$HOME/.local/bin/binary" # Important for rollback!
|
||||
}
|
||||
|
||||
# ─── Shell Configuration (if needed) ─────────────────────────────────
|
||||
@@ -126,6 +135,7 @@ configure_shell() {
|
||||
# Use drop-in snippets for shell configuration (they auto-rollback)
|
||||
# write_env_snippet "<name>" "export VAR_NAME=value\neval \"\$(<name> init bash)\""
|
||||
# write_alias_snippet "<name>" "alias <name>='<command>'"
|
||||
# write_completion_snippet "<name>" "source <(<command> completion bash)"
|
||||
:
|
||||
}
|
||||
|
||||
@@ -184,6 +194,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
|---|---|
|
||||
| `write_env_snippet <name> <content>` | Creates an isolated `env.d/` shell drop-in snippet and registers it for rollback. |
|
||||
| `write_alias_snippet <name> <content>` | Creates an isolated `aliases.d/` shell drop-in snippet and registers it for rollback. |
|
||||
| `write_completion_snippet <name> <content>` | Creates an isolated `completions.d/` bash completion snippet and registers it for rollback. |
|
||||
|
||||
---
|
||||
|
||||
@@ -200,13 +211,14 @@ trap cleanup EXIT
|
||||
### Distro-specific mapping
|
||||
|
||||
```bash
|
||||
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "git"
|
||||
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)
|
||||
@@ -240,7 +252,7 @@ track_file "/usr/local/bin/binary"
|
||||
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
|
||||
2. **Confirmation prompts**: Always ask before installing. Check if already installed first.
|
||||
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `~/.local/bin/`, you MUST call `track_file`. If you run `makepkg`, you MUST call `add_rollback_cmd` for `pacman -R`.
|
||||
4. **Shell Drop-ins**: Always use `write_env_snippet` or `write_alias_snippet` instead of manually injecting code directly into `~/.bashrc`.
|
||||
4. **Shell Drop-ins**: Always use `write_env_snippet`, `write_alias_snippet`, or `write_completion_snippet` instead of manually injecting code directly into `~/.bashrc`.
|
||||
5. **No hardcoded paths**: Use `$HOME`, library functions, and `detect_*` helpers.
|
||||
6. **Error handling**: Use `set -euo pipefail` after the guard block.
|
||||
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.
|
||||
|
||||
2
b.sh
2
b.sh
@@ -86,7 +86,7 @@ _b_completion() {
|
||||
|
||||
# If completing the first argument after 'b'
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
opts="all con gone up ware bware"
|
||||
opts="all con gone up ware bware me trust"
|
||||
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
local installer_keys=""
|
||||
|
||||
51
bootstrap.sh
51
bootstrap.sh
@@ -8,6 +8,11 @@ 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
|
||||
@@ -36,7 +41,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" "lib/plugins.sh" "lib/registry_helpers.sh" "lib/github.sh")
|
||||
_LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "lib/json.sh" "lib/plugins.sh")
|
||||
|
||||
_curl_args=()
|
||||
for _lib in "${_LIBS[@]}"; do
|
||||
@@ -51,8 +56,6 @@ 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
|
||||
@@ -65,26 +68,10 @@ install_bootstrap() {
|
||||
[ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc")
|
||||
|
||||
local routes_dir="$HOME/.config/bootstrap"
|
||||
mkdir -p "$routes_dir"
|
||||
mkdir -p "$routes_dir/env.d"
|
||||
mkdir -p "$routes_dir/aliases.d"
|
||||
|
||||
# Initialize XDG directories
|
||||
mkdir -p "$HOME/.local/share/bootstrap/bin"
|
||||
mkdir -p "$HOME/.local/share/bootstrap/opt"
|
||||
mkdir -p "$HOME/.local/share/bootstrap/runtimes"
|
||||
mkdir -p "$HOME/.local/state/bootstrap/logs"
|
||||
mkdir -p "$HOME/.local/state/bootstrap/rollback"
|
||||
mkdir -p "$HOME/.cache/bootstrap/downloads"
|
||||
mkdir -p "$HOME/.cache/bootstrap/tmp"
|
||||
|
||||
# Create the universal binary PATH snippet
|
||||
cat << 'EOF' > "$routes_dir/env.d/bootstrap-bin.sh"
|
||||
export BOOTSTRAP_BIN="$BOOTSTRAP_BIN"
|
||||
case ":$PATH:" in
|
||||
*":$BOOTSTRAP_BIN:"*) ;;
|
||||
*) export PATH="$BOOTSTRAP_BIN:$PATH" ;;
|
||||
esac
|
||||
EOF
|
||||
mkdir -p "$routes_dir/completions.d"
|
||||
|
||||
# List of all files to download/copy
|
||||
local files=(
|
||||
@@ -96,8 +83,7 @@ EOF
|
||||
"lib/rollback.sh"
|
||||
"lib/platform.sh"
|
||||
"lib/shell_config.sh"
|
||||
"lib/registry_helpers.sh"
|
||||
"lib/github.sh"
|
||||
"lib/json.sh"
|
||||
"lib/plugins.sh"
|
||||
"commands/help.sh"
|
||||
"commands/con.sh"
|
||||
@@ -105,11 +91,6 @@ EOF
|
||||
"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
|
||||
@@ -124,6 +105,12 @@ EOF
|
||||
mkdir -p "$routes_dir/installers"
|
||||
cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/"
|
||||
fi
|
||||
|
||||
# Also copy plugins if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/plugins" ]; then
|
||||
mkdir -p "$routes_dir/plugins"
|
||||
cp -r "$_SCRIPT_DIR/plugins/"* "$routes_dir/plugins/"
|
||||
fi
|
||||
else
|
||||
log_info "Downloading bootstrap scripts..."
|
||||
local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
@@ -159,16 +146,10 @@ EOF
|
||||
|
||||
# >>> 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
|
||||
|
||||
|
||||
@@ -78,9 +78,6 @@ 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
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
# 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="$BOOTSTRAP_BIN"
|
||||
TARGET_DIR="$HOME/.local/bin"
|
||||
BINARY_PATH="$TARGET_DIR/agy"
|
||||
|
||||
install_agy() {
|
||||
@@ -50,12 +55,19 @@ 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=$(echo "$manifest_json" | jq -r '.version // empty')
|
||||
url=$(echo "$manifest_json" | jq -r '.url // empty')
|
||||
sha512=$(echo "$manifest_json" | jq -r '.sha512 // empty')
|
||||
version=$(parse_json_key "$manifest_json" "version")
|
||||
url=$(parse_json_key "$manifest_json" "url")
|
||||
sha512=$(parse_json_key "$manifest_json" "sha512")
|
||||
|
||||
if [ -z "$url" ] || [ -z "$sha512" ]; then
|
||||
log_error "Failed to parse release manifest."
|
||||
@@ -122,11 +134,16 @@ 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,11 +2,16 @@
|
||||
# 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)"
|
||||
@@ -16,9 +21,12 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
install_asciicinema() {
|
||||
local latest_tag=""
|
||||
if has_command curl; then
|
||||
log_info "Fetching latest asciinema version from GitHub..."
|
||||
latest_tag=$(github_get_latest_release "asciinema/asciinema")
|
||||
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)
|
||||
fi
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
@@ -65,21 +73,22 @@ 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}..."
|
||||
github_download_asset "asciinema/asciinema" "$latest_tag" "asciinema-${asciinema_arch}" "$TMP_DIR/asciinema"
|
||||
download_file "$download_url" "$TMP_DIR/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"
|
||||
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"
|
||||
|
||||
# Create compatibility symlink matching the installer name spelling
|
||||
log_info "Creating compatibility symlink for asciicinema..."
|
||||
ln -sf "$BOOTSTRAP_BIN/asciinema" /usr/local/bin/asciicinema
|
||||
sudo ln -sf /usr/local/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() {
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
# Tool: bat
|
||||
# DisplayName: Bat
|
||||
# Description: Install Bat (alternative to cat) and configure alias
|
||||
# Strategy: binary
|
||||
#
|
||||
# Bat Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -16,54 +21,69 @@ cleanup() {
|
||||
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
|
||||
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
|
||||
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
|
||||
# Remove leading 'v' for file name version
|
||||
local version="${latest_tag#v}"
|
||||
|
||||
log_info "Fetching latest Bat version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "sharkdp/bat")
|
||||
# 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"
|
||||
|
||||
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"
|
||||
log_error "Unsupported distribution."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Downloading Bat ${latest_tag}..."
|
||||
local archive="$TMP_DIR/bat.tar.gz"
|
||||
github_download_asset "sharkdp/bat" "$latest_tag" "bat-${latest_tag}-${target}\.tar\.gz" "$archive"
|
||||
|
||||
log_info "Extracting Bat binary..."
|
||||
tar -xzf "$archive" -C "$TMP_DIR"
|
||||
|
||||
local extract_dir="$TMP_DIR/bat-${latest_tag}-${target}"
|
||||
|
||||
local target_dir="$BOOTSTRAP_BIN"
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
log_info "Installing Bat to $target_dir/bat..."
|
||||
cp "$extract_dir/bat" "$target_dir/bat"
|
||||
chmod +x "$target_dir/bat"
|
||||
track_file "$target_dir/bat"
|
||||
|
||||
register_tool "bat" "binary" "$latest_tag" "github:sharkdp/bat"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "bat alias"
|
||||
done
|
||||
if [ -f "$HOME/.bash_aliases" ]; then
|
||||
remove_block "$HOME/.bash_aliases" "bat alias"
|
||||
fi
|
||||
|
||||
write_alias_snippet "bat" "alias cat='bat --paging=never -p'"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
# Tool: docker
|
||||
# DisplayName: Docker
|
||||
# Description: Container runtime and orchestration platform
|
||||
# Strategy: system
|
||||
#
|
||||
# Docker 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 ──────────────────────────────────────────────
|
||||
@@ -53,7 +58,6 @@ install_docker() {
|
||||
sudo systemctl enable --now docker.service || true
|
||||
sudo systemctl enable --now containerd.service || true
|
||||
fi
|
||||
register_tool "docker" "system" "" "os-package-manager"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
132
installers/install_hyperfine.sh
Normal file
132
installers/install_hyperfine.sh
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: hyperfine
|
||||
# DisplayName: Hyperfine
|
||||
# Description: Command-line benchmarking tool
|
||||
#
|
||||
# Hyperfine Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_hyperfine() {
|
||||
if has_command hyperfine || [ -f "$HOME/.local/bin/hyperfine" ]; then
|
||||
if ! confirm "Hyperfine is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Hyperfine installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install Hyperfine?"; then
|
||||
log_info "Skipping Hyperfine installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure curl is installed
|
||||
if ! has_command curl; then
|
||||
log_info "curl not found. Installing curl..."
|
||||
pkg_install curl
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
local raw_arch
|
||||
raw_arch=$(detect_arch)
|
||||
local arch=""
|
||||
case "$raw_arch" in
|
||||
x86_64) arch="x86_64" ;;
|
||||
arm64) arch="aarch64" ;;
|
||||
*) log_error "Unsupported Linux architecture: $raw_arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
log_info "Fetching latest Hyperfine version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/sharkdp/hyperfine/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
|
||||
local download_url
|
||||
if [ -n "$latest_tag" ]; then
|
||||
log_info "Latest Hyperfine version found: $latest_tag"
|
||||
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
|
||||
else
|
||||
latest_tag="v1.20.0"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
download_url="https://github.com/sharkdp/hyperfine/releases/download/${latest_tag}/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu.tar.gz"
|
||||
fi
|
||||
|
||||
log_info "Downloading Hyperfine from ${download_url}..."
|
||||
local archive="$TMP_DIR/hyperfine.tar.gz"
|
||||
download_file "$download_url" "$archive"
|
||||
|
||||
# Extract the archive
|
||||
log_info "Extracting Hyperfine archive..."
|
||||
tar -xzf "$archive" -C "$TMP_DIR"
|
||||
|
||||
local extract_dir="$TMP_DIR/hyperfine-${latest_tag}-${arch}-unknown-linux-gnu"
|
||||
if [ ! -d "$extract_dir" ]; then
|
||||
# Handle case where directory name might differ (e.g. without leading v in directory name or tag)
|
||||
extract_dir=$(find "$TMP_DIR" -maxdepth 1 -type d -name "hyperfine-*" | head -n1)
|
||||
fi
|
||||
|
||||
if [ -z "$extract_dir" ] || [ ! -d "$extract_dir" ]; then
|
||||
log_error "Failed to locate extracted Hyperfine directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install binary to ~/.local/bin
|
||||
local target_dir="$HOME/.local/bin"
|
||||
mkdir -p "$target_dir"
|
||||
log_info "Installing Hyperfine to $target_dir/hyperfine..."
|
||||
cp "$extract_dir/hyperfine" "$target_dir/hyperfine"
|
||||
chmod +x "$target_dir/hyperfine"
|
||||
track_file "$target_dir/hyperfine"
|
||||
|
||||
# Install man page if present
|
||||
if [ -f "$extract_dir/hyperfine.1" ]; then
|
||||
local man_dir="$HOME/.local/share/man/man1"
|
||||
mkdir -p "$man_dir"
|
||||
log_info "Installing man page to $man_dir/hyperfine.1..."
|
||||
cp "$extract_dir/hyperfine.1" "$man_dir/hyperfine.1"
|
||||
track_file "$man_dir/hyperfine.1"
|
||||
fi
|
||||
|
||||
# Install autocomplete if present
|
||||
if [ -f "$extract_dir/autocomplete/hyperfine.bash" ]; then
|
||||
log_info "Installing bash completions..."
|
||||
local comp_content
|
||||
comp_content=$(cat "$extract_dir/autocomplete/hyperfine.bash")
|
||||
write_completion_snippet "hyperfine" "$comp_content"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Add ~/.local/bin to PATH for the current process
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "local-bin path"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
}
|
||||
|
||||
main() {
|
||||
install_hyperfine
|
||||
configure_shell
|
||||
|
||||
echo
|
||||
log_success "Hyperfine installation and configuration complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -2,11 +2,16 @@
|
||||
# Tool: lazygit
|
||||
# DisplayName: lazygit
|
||||
# Description: Simple terminal UI for git commands
|
||||
# Strategy: binary
|
||||
#
|
||||
# lazygit 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 ──────────────────────────────────────────────
|
||||
@@ -25,7 +30,11 @@ install_lazygit() {
|
||||
fi
|
||||
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "jesseduffield/lazygit")
|
||||
if has_command curl; then
|
||||
latest_tag=$(curl -sL https://api.github.com/repos/jesseduffield/lazygit/releases/latest \
|
||||
| grep '"tag_name":' | head -n1 \
|
||||
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.62.2" # fallback
|
||||
@@ -40,6 +49,8 @@ install_lazygit() {
|
||||
arch_str="arm64"
|
||||
fi
|
||||
|
||||
local url="https://github.com/jesseduffield/lazygit/releases/download/${latest_tag}/lazygit_${version}_linux_${arch_str}.tar.gz"
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
@@ -47,16 +58,15 @@ install_lazygit() {
|
||||
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"
|
||||
download_file "$url" "$dest"
|
||||
|
||||
log_info "Extracting..."
|
||||
tar -xzf "$dest" -C "$TMP_DIR"
|
||||
|
||||
mkdir -p "$BOOTSTRAP_BIN"
|
||||
mkdir -p "$HOME/.local/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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
# 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)"
|
||||
@@ -16,7 +21,7 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
install_nvm() {
|
||||
if has_command nvm || [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then
|
||||
if has_command nvm || [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||
log_info "NVM is already installed."
|
||||
fi
|
||||
|
||||
@@ -29,7 +34,7 @@ install_nvm() {
|
||||
# Try to fetch the latest version of NVM from GitHub API
|
||||
log_info "Fetching the latest NVM version..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "nvm-sh/nvm")
|
||||
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)
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.40.5" # Fallback version if API request fails
|
||||
@@ -42,20 +47,25 @@ 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 $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
|
||||
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
|
||||
|
||||
track_dir "$BOOTSTRAP_RUNTIMES/nvm"
|
||||
track_dir "$HOME/.nvm"
|
||||
|
||||
log_success "NVM source files successfully extracted to $BOOTSTRAP_RUNTIMES/nvm."
|
||||
log_success "NVM source files successfully extracted to $HOME/.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="$BOOTSTRAP_RUNTIMES/nvm"
|
||||
export NVM_DIR="$HOME/.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
|
||||
@@ -66,10 +76,10 @@ EOF
|
||||
|
||||
install_node() {
|
||||
# Ensure NVM is loaded in this script context
|
||||
if [ -s "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh" ]; then
|
||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||
# Temporarily disable nounset as nvm.sh does not support set -u
|
||||
set +u
|
||||
. "$BOOTSTRAP_RUNTIMES/nvm/nvm.sh"
|
||||
. "$HOME/.nvm/nvm.sh"
|
||||
else
|
||||
log_error "Could not load NVM to install Node.js."
|
||||
return 1
|
||||
@@ -85,7 +95,6 @@ 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() {
|
||||
@@ -97,7 +106,7 @@ 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 "$BOOTSTRAP_RUNTIMES/nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")"
|
||||
log_info "Installed NVM version: $(nvm --version 2>/dev/null || cat "$HOME/.nvm/package.json" | grep '"version":' | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' || echo "unknown")"
|
||||
else
|
||||
log_success "Installation complete."
|
||||
fi
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
# 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="$BOOTSTRAP_OPT/nvim"
|
||||
NVIM_BIN_DIR="$BOOTSTRAP_BIN"
|
||||
NVIM_INSTALL_DIR="/opt/nvim"
|
||||
NVIM_CONFIG_REPO="https://git.adityagupta.dev/sortedcord/editor.git"
|
||||
NVIM_CONFIG_DIR="$HOME/.config/nvim"
|
||||
|
||||
@@ -29,7 +33,7 @@ check_config_dir() {
|
||||
install_packages() {
|
||||
log_info "Detecting distribution and installing dependencies..."
|
||||
pkg_install \
|
||||
git tar unzip ripgrep fzf nodejs npm xclip wl-clipboard \
|
||||
git tar curl 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" \
|
||||
@@ -72,23 +76,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}..."
|
||||
github_download_asset "neovim/neovim" "v${NVIM_VERSION}" "nvim-${nvim_arch}\.tar\.gz" "$TMP_DIR/nvim.tar.gz"
|
||||
download_file "$nvim_url" "$TMP_DIR/nvim.tar.gz"
|
||||
|
||||
tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR"
|
||||
|
||||
rm -rf "$NVIM_INSTALL_DIR"
|
||||
mkdir -p "$(dirname "$NVIM_INSTALL_DIR")"
|
||||
mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
|
||||
sudo rm -rf "$NVIM_INSTALL_DIR"
|
||||
sudo mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
|
||||
|
||||
ln -sf "$NVIM_INSTALL_DIR/bin/nvim" "$NVIM_BIN_DIR/nvim"
|
||||
sudo ln -sf "$NVIM_INSTALL_DIR/bin/nvim" /usr/local/bin/nvim
|
||||
|
||||
track_dir "$NVIM_INSTALL_DIR"
|
||||
track_file "$NVIM_BIN_DIR/nvim"
|
||||
track_file "/usr/local/bin/nvim"
|
||||
|
||||
log_success "Installed:"
|
||||
nvim --version | head -n1
|
||||
register_tool "nvim" "binary" "$NVIM_VERSION" "github:neovim/neovim"
|
||||
}
|
||||
|
||||
install_config() {
|
||||
@@ -107,6 +111,24 @@ 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,7 +2,6 @@
|
||||
# Tool: pnpm
|
||||
# DisplayName: Pnpm
|
||||
# Description: Install pnpm package manager
|
||||
# Strategy: binary
|
||||
#
|
||||
# pnpm Installer Script
|
||||
#
|
||||
@@ -18,6 +17,12 @@
|
||||
# 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)"
|
||||
@@ -122,17 +127,14 @@ install_pnpm() {
|
||||
}
|
||||
libc_suffix="$(detect_libc_suffix)"
|
||||
|
||||
# Fetch the latest version from GitHub, or use PNPM_VERSION if set
|
||||
# Fetch the latest version from the npm registry, or use PNPM_VERSION if set
|
||||
if [ -z "${PNPM_VERSION:-}" ]; then
|
||||
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."
|
||||
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."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
version="$(echo "$version_json" | grep -o '"latest":[[:space:]]*"[0-9.]*"' | grep -o '[0-9.]*')"
|
||||
else
|
||||
version="${PNPM_VERSION}"
|
||||
fi
|
||||
@@ -149,7 +151,7 @@ install_pnpm() {
|
||||
|
||||
if [ "$major_version" -ge 11 ]; then
|
||||
# v11+: distributed as tarballs containing the binary and dist/ directory
|
||||
github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}\.tar\.gz" "$TMP_DIR/pnpm.tar.gz" || {
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}.tar.gz" "$TMP_DIR/pnpm.tar.gz" || {
|
||||
log_error "Failed to download pnpm tarball."
|
||||
return 1
|
||||
}
|
||||
@@ -164,7 +166,7 @@ install_pnpm() {
|
||||
}
|
||||
else
|
||||
# Older versions: distributed as a single executable binary
|
||||
github_download_asset "pnpm/pnpm" "v${version}" "${asset_base}" "$TMP_DIR/pnpm" || {
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}" "$TMP_DIR/pnpm" || {
|
||||
log_error "Failed to download pnpm binary."
|
||||
return 1
|
||||
}
|
||||
@@ -177,19 +179,23 @@ 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="$BOOTSTRAP_RUNTIMES/pnpm"
|
||||
export PNPM_HOME="$HOME/.local/share/pnpm"
|
||||
case ":$PATH:" in
|
||||
*":$PNPM_HOME:"*) ;;
|
||||
*) export PATH="$PNPM_HOME:$PATH" ;;
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
# 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)"
|
||||
@@ -15,6 +20,14 @@ 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)"
|
||||
@@ -48,14 +61,11 @@ detect_target_triple() {
|
||||
}
|
||||
|
||||
install_rust() {
|
||||
export CARGO_HOME="$BOOTSTRAP_RUNTIMES/cargo"
|
||||
export RUSTUP_HOME="$BOOTSTRAP_RUNTIMES/rustup"
|
||||
|
||||
if has_command rustup || [ -f "$BOOTSTRAP_RUNTIMES/cargo/bin/rustup" ]; then
|
||||
if has_command rustup || [ -f "$HOME/.cargo/bin/rustup" ]; then
|
||||
log_info "Rust (rustup) is already installed."
|
||||
fi
|
||||
|
||||
|
||||
install_downloader
|
||||
|
||||
local target
|
||||
target=$(detect_target_triple)
|
||||
@@ -77,19 +87,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
|
||||
|
||||
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"
|
||||
write_env_snippet "rust" '. "$HOME/.cargo/env"'
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
# Tool: starship
|
||||
# DisplayName: Starship
|
||||
# Description: Install Starship shell prompt
|
||||
# Strategy: binary
|
||||
#
|
||||
# Starship Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -20,6 +25,12 @@ install_starship() {
|
||||
log_info "Starship is already installed."
|
||||
fi
|
||||
|
||||
# Ensure curl is installed
|
||||
if ! has_command curl; then
|
||||
log_info "curl not found. Installing curl..."
|
||||
pkg_install curl
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
local raw_arch
|
||||
raw_arch=$(detect_arch)
|
||||
@@ -34,33 +45,47 @@ install_starship() {
|
||||
|
||||
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=$(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 ${latest_tag}..."
|
||||
log_info "Downloading Starship from ${download_url}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
github_download_asset "starship/starship" "$latest_tag" "starship-${target}\.tar\.gz" "$archive"
|
||||
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="$BOOTSTRAP_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"
|
||||
register_tool "starship" "binary" "$latest_tag" "github:starship/starship"
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Add ~/.local/bin to PATH for the current process
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "local-bin path"
|
||||
remove_block "$config_file" "starship init"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
write_env_snippet "starship" 'eval "$(starship init bash)"'
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
# 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)"
|
||||
@@ -23,6 +28,12 @@ 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)
|
||||
@@ -43,22 +54,28 @@ install_uv() {
|
||||
|
||||
log_info "Fetching latest uv version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "astral-sh/uv")
|
||||
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)
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
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
|
||||
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 ${latest_tag}..."
|
||||
log_info "Downloading uv from ${download_url}..."
|
||||
local archive="$TMP_DIR/uv.tar.gz"
|
||||
github_download_asset "astral-sh/uv" "$latest_tag" "uv-${target}\.tar\.gz" "$archive"
|
||||
download_file "$download_url" "$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="$BOOTSTRAP_BIN"
|
||||
local target_dir="$HOME/.local/bin"
|
||||
mkdir -p "$target_dir"
|
||||
log_info "Installing uv and uvx to $target_dir..."
|
||||
cp "$TMP_DIR/uv" "$target_dir/uv"
|
||||
@@ -66,12 +83,20 @@ 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,11 +2,16 @@
|
||||
# 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 ──────────────────────────────────────────────
|
||||
@@ -61,7 +66,6 @@ install_yay() {
|
||||
cd "$orig_dir"
|
||||
log_info "Cleaning up installer directory..."
|
||||
rm -rf "$clone_dir"
|
||||
register_tool "yay" "system" "" "aur:yay-bin"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
# Tool: yazi
|
||||
# DisplayName: Yazi
|
||||
# Description: Install Yazi terminal file manager and dependencies
|
||||
# Strategy: binary
|
||||
#
|
||||
# Yazi Installer Script
|
||||
#
|
||||
|
||||
# Prevent standalone execution
|
||||
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
|
||||
echo "Error: This script must be run through the 'b' CLI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(make_temp_dir)"
|
||||
@@ -16,6 +21,11 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
add_y_wrapper() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
for config_file in "${target_files[@]}"; do
|
||||
remove_block "$config_file" "yazi wrapper"
|
||||
done
|
||||
|
||||
local wrapper_content
|
||||
wrapper_content=$(cat << 'EOF'
|
||||
@@ -30,68 +40,77 @@ y() {
|
||||
}
|
||||
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
|
||||
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
|
||||
|
||||
# Ensure required extraction tools are installed
|
||||
if ! has_command unzip; then
|
||||
log_info "unzip not found. Installing unzip..."
|
||||
pkg_install unzip
|
||||
fi
|
||||
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
local target=""
|
||||
case "$arch" in
|
||||
x86_64) target="x86_64-unknown-linux-gnu" ;;
|
||||
arm64) target="aarch64-unknown-linux-gnu" ;;
|
||||
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
|
||||
esac
|
||||
|
||||
log_info "Fetching latest Yazi version from GitHub..."
|
||||
local latest_tag=""
|
||||
latest_tag=$(github_get_latest_release "sxyazi/yazi")
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v0.3.3"
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
fi
|
||||
|
||||
log_info "Downloading Yazi ${latest_tag}..."
|
||||
local archive="$TMP_DIR/yazi.zip"
|
||||
github_download_asset "sxyazi/yazi" "$latest_tag" "yazi-${target}\.zip" "$archive"
|
||||
|
||||
log_info "Extracting Yazi binaries..."
|
||||
unzip -q "$archive" -d "$TMP_DIR"
|
||||
|
||||
local extract_dir="$TMP_DIR/yazi-${target}"
|
||||
local target_dir="$BOOTSTRAP_BIN"
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
log_info "Installing Yazi to $target_dir..."
|
||||
cp "$extract_dir/yazi" "$target_dir/yazi"
|
||||
cp "$extract_dir/ya" "$target_dir/ya"
|
||||
chmod +x "$target_dir/yazi" "$target_dir/ya"
|
||||
track_file "$target_dir/yazi"
|
||||
track_file "$target_dir/ya"
|
||||
|
||||
log_info "Installing system dependencies for Yazi..."
|
||||
pkg_install ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find"
|
||||
|
||||
create_fd_symlink
|
||||
|
||||
register_tool "yazi" "binary" "$latest_tag" "github:sxyazi/yazi"
|
||||
|
||||
# Add the system dependencies to the registry for uninstallation tracking
|
||||
registry_add_sys_deps "yazi" ffmpeg jq ripgrep fzf zoxide resvg imagemagick "arch:7zip|debian:7zip|fedora:p7zip" "arch:poppler|debian:poppler-utils|fedora:poppler-utils" "arch:fd|debian:fd-find|fedora:fd-find"
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -2,13 +2,24 @@
|
||||
# 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
|
||||
@@ -25,17 +36,24 @@ 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)"'
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,6 @@ 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
|
||||
@@ -97,7 +88,7 @@ version_lt() {
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local cache_dir="$BOOTSTRAP_CACHE_DIR/downloads"
|
||||
local cache_dir="$HOME/.local/state/bootstrap/cache"
|
||||
|
||||
mkdir -p "$cache_dir"
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# GitHub API helper functions for Bootstrap installers
|
||||
|
||||
# Usage: github_get_latest_release <owner/repo>
|
||||
# Prints the tag_name of the latest release.
|
||||
|
||||
# Installers still use this function instead of just directly invoking download_asset function:
|
||||
# - Asset names often contain the version
|
||||
# - Installers may compare the latest tag from github against the locally installed version before doing any work.
|
||||
# - We need concrete version string so we can pass it to the reigster_tool function.
|
||||
github_get_latest_release() {
|
||||
local repo="$1"
|
||||
local tag
|
||||
tag=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" | jq -r '.tag_name // empty')
|
||||
echo "$tag"
|
||||
}
|
||||
|
||||
# Usage: github_get_download_url <owner/repo> <tag> <regex_pattern>
|
||||
# Finds the asset matching the regex pattern in the specified release tag and prints its download URL.
|
||||
github_get_download_url() {
|
||||
local repo="$1"
|
||||
local tag="$2"
|
||||
local pattern="$3"
|
||||
|
||||
# If the tag is exactly 'latest', fetch the latest release asset list
|
||||
local endpoint
|
||||
if [ "$tag" = "latest" ]; then
|
||||
endpoint="https://api.github.com/repos/$repo/releases/latest"
|
||||
else
|
||||
endpoint="https://api.github.com/repos/$repo/releases/tags/$tag"
|
||||
fi
|
||||
|
||||
local url
|
||||
url=$(curl -fsSL "$endpoint" | jq -r --arg regex "$pattern" '.assets[] | select(.name | test($regex; "i")) | .browser_download_url' | head -n1)
|
||||
echo "$url"
|
||||
}
|
||||
|
||||
# Usage: github_download_asset <owner/repo> <tag> <regex_pattern> <dest_file>
|
||||
# Resolves the URL for the matching asset and downloads it to dest_file.
|
||||
github_download_asset() {
|
||||
local repo="$1"
|
||||
local tag="$2"
|
||||
local pattern="$3"
|
||||
local dest="$4"
|
||||
|
||||
local url
|
||||
url=$(github_get_download_url "$repo" "$tag" "$pattern")
|
||||
|
||||
if [ -z "$url" ]; then
|
||||
log_error "Could not find asset matching regex '$pattern' for $repo@$tag"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Downloading $url ..."
|
||||
download_file "$url" "$dest"
|
||||
}
|
||||
|
||||
export -f github_get_latest_release github_get_download_url github_download_asset
|
||||
68
lib/json.sh
Normal file
68
lib/json.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# generic JSON parser in pure bash and awk.
|
||||
# reads JSON from stdin and outputs a flattened list of key-value pairs.
|
||||
# example input: {"plugins": {"my_plugin": {"version": "1.0", "arr": [1, 2]}}}
|
||||
# example output:
|
||||
# plugins.my_plugin.version="1.0"
|
||||
# plugins.my_plugin.arr[0]=1
|
||||
# plugins.my_plugin.arr[1]=2
|
||||
|
||||
# pardon my french
|
||||
parse_json() {
|
||||
# Tokenize the JSON using grep
|
||||
grep -oE '"([^"\\]|\\.)*"|true|false|null|[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?|[][}{:,]' | \
|
||||
awk '
|
||||
BEGIN {
|
||||
depth=0;
|
||||
key=""
|
||||
}
|
||||
{
|
||||
token = $0
|
||||
if (token == "{") {
|
||||
depth++
|
||||
is_key[depth] = 1
|
||||
array_idx[depth] = ""
|
||||
} else if (token == "}") {
|
||||
delete path[depth]
|
||||
delete array_idx[depth]
|
||||
depth--
|
||||
} else if (token == "[") {
|
||||
depth++
|
||||
is_key[depth] = 0
|
||||
array_idx[depth] = 0
|
||||
} else if (token == "]") {
|
||||
delete array_idx[depth]
|
||||
delete path[depth]
|
||||
depth--
|
||||
} else if (token == ":") {
|
||||
is_key[depth] = 0
|
||||
} else if (token == ",") {
|
||||
if (array_idx[depth] != "") {
|
||||
array_idx[depth]++
|
||||
} else {
|
||||
is_key[depth] = 1
|
||||
}
|
||||
} else {
|
||||
if (is_key[depth] == 1) {
|
||||
# Remove quotes from the key
|
||||
gsub(/^"|"$/, "", token)
|
||||
path[depth] = token
|
||||
} else {
|
||||
# It is a value
|
||||
p = ""
|
||||
for (i=1; i<=depth; i++) {
|
||||
if (array_idx[i] != "") {
|
||||
p = p "[" array_idx[i] "]"
|
||||
} else if (path[i] != "") {
|
||||
p = p "." path[i]
|
||||
}
|
||||
}
|
||||
# Remove leading dot
|
||||
sub(/^\./, "", p)
|
||||
print p "=" token
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
@@ -86,7 +86,18 @@ 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
|
||||
@@ -158,15 +169,21 @@ pkg_remove() {
|
||||
|
||||
local to_remove=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
local is_installed=0
|
||||
if pkg_check "$pkg"; then
|
||||
is_installed=1
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
if [ "$is_installed" -eq 1 ]; then
|
||||
to_remove+=("$pkg")
|
||||
fi
|
||||
to_remove+=("$pkg")
|
||||
done
|
||||
|
||||
if [ ${#to_remove[@]} -eq 0 ]; then
|
||||
|
||||
@@ -1,13 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Parses a plugin manifest using jq and outputs bash array assignments
|
||||
if [ -f "$BOOTSTRAP_DIR/lib/json.sh" ]; then
|
||||
. "$BOOTSTRAP_DIR/lib/json.sh"
|
||||
fi
|
||||
|
||||
# Parses a plugin manifest using the generic json parser 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)
|
||||
'
|
||||
# The generic parser outputs lines like:
|
||||
# plugins.myplugin.version="1.0"
|
||||
# plugins.myplugin.url="https://..."
|
||||
# We want to extract myplugin and the keys to build:
|
||||
# PLUGIN_VERSIONS["myplugin"]="1.0"
|
||||
# PLUGIN_URLS["myplugin"]="https://..."
|
||||
|
||||
parse_json | awk -F'=' '
|
||||
{
|
||||
path = $1
|
||||
val = $2
|
||||
|
||||
# Remove quotes around value for bash array assignment
|
||||
gsub(/^"|"$/, "", val)
|
||||
|
||||
# Match paths starting with "plugins."
|
||||
if (match(path, /^plugins\./)) {
|
||||
rest = substr(path, RLENGTH + 1)
|
||||
# Find the last dot to separate plugin name from the property key
|
||||
last_dot = 0
|
||||
for (i=length(rest); i>0; i--) {
|
||||
if (substr(rest, i, 1) == ".") {
|
||||
last_dot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (last_dot > 0) {
|
||||
plugin_name = substr(rest, 1, last_dot - 1)
|
||||
prop = substr(rest, last_dot + 1)
|
||||
if (prop == "version") {
|
||||
print "PLUGIN_VERSIONS[\"" plugin_name "\"]=\"" val "\""
|
||||
} else if (prop == "url") {
|
||||
print "PLUGIN_URLS[\"" plugin_name "\"]=\"" val "\""
|
||||
} else if (prop == "bootstrap") {
|
||||
print "PLUGIN_BOOTSTRAP_VERSIONS[\"" plugin_name "\"]=\"" val "\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
# Ensures that the plugin sources file exists, initializing it with the official repository by default
|
||||
|
||||
@@ -5,6 +5,7 @@ declare -A INSTALLERS=(
|
||||
[asciicinema]="asciinema terminal recorder"
|
||||
[bat]="Bat (alternative to cat) and configure alias"
|
||||
[docker]="Container runtime and orchestration platform"
|
||||
[hyperfine]="Command-line benchmarking tool"
|
||||
[lazygit]="Simple terminal UI for git commands"
|
||||
[node]="Node.js (LTS) and NVM"
|
||||
[nvim]="Neovim 0.12.0 and configuration"
|
||||
@@ -22,6 +23,7 @@ declare -A INSTALLER_DISPLAYS=(
|
||||
[asciicinema]="asciicinema"
|
||||
[bat]="Bat"
|
||||
[docker]="Docker"
|
||||
[hyperfine]="Hyperfine"
|
||||
[lazygit]="lazygit"
|
||||
[node]="Node"
|
||||
[nvim]="Neovim"
|
||||
@@ -34,21 +36,4 @@ declare -A INSTALLER_DISPLAYS=(
|
||||
[zoxide]="Zoxide"
|
||||
)
|
||||
|
||||
declare -A INSTALLER_STRATEGIES=(
|
||||
[agy]="binary"
|
||||
[asciicinema]="binary"
|
||||
[bat]="binary"
|
||||
[docker]="system"
|
||||
[lazygit]="binary"
|
||||
[node]="managed"
|
||||
[nvim]="binary"
|
||||
[pnpm]="binary"
|
||||
[rust]="managed"
|
||||
[starship]="binary"
|
||||
[uv]="binary"
|
||||
[yay]="system"
|
||||
[yazi]="binary"
|
||||
[zoxide]="managed"
|
||||
)
|
||||
|
||||
INSTALLER_KEYS=(agy asciicinema bat docker lazygit node nvim pnpm rust starship uv yay yazi zoxide)
|
||||
INSTALLER_KEYS=(agy asciicinema bat docker hyperfine lazygit node nvim pnpm rust starship uv yay yazi zoxide)
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/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,13 +51,6 @@ 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."
|
||||
}
|
||||
@@ -91,41 +84,6 @@ 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."
|
||||
@@ -137,7 +95,9 @@ rollback_bare() {
|
||||
|
||||
if [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
uninstall_tool "$tool"
|
||||
execute_rollback "$tool"
|
||||
# Remove the last line efficiently
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
elif [[ "$last_line" == SAVEPOINT:* ]]; then
|
||||
local sp="${last_line#SAVEPOINT: }"
|
||||
log_warn "Last action was savepoint '$sp'. Cannot bare-rollback a savepoint."
|
||||
@@ -162,7 +122,8 @@ rollback_to_savepoint() {
|
||||
break
|
||||
elif [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
uninstall_tool "$tool"
|
||||
execute_rollback "$tool"
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
elif [[ "$last_line" == SAVEPOINT:* ]]; then
|
||||
local sp="${last_line#SAVEPOINT: }"
|
||||
log_info "Removing intermediate savepoint '$sp'..."
|
||||
@@ -174,4 +135,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 uninstall_tool 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 rollback_bare rollback_to_savepoint
|
||||
|
||||
@@ -216,6 +216,14 @@ for script in "${SCRIPTS[@]}"; do
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
me)
|
||||
run_plugin "auth" "me" "$@"
|
||||
exit $?
|
||||
;;
|
||||
trust)
|
||||
run_plugin "auth" "trust" "$@"
|
||||
exit $?
|
||||
;;
|
||||
con)
|
||||
if [ -f "$BOOTSTRAP_DIR/commands/con.sh" ]; then
|
||||
. "$BOOTSTRAP_DIR/commands/con.sh" "$@"
|
||||
@@ -275,12 +283,7 @@ for script in "${SCRIPTS[@]}"; do
|
||||
if [ -z "$target" ]; then
|
||||
rollback_bare
|
||||
else
|
||||
local registry_file="$BOOTSTRAP_STATE_DIR/registry.json"
|
||||
if [ -f "$registry_file" ] && jq -e --arg t "$target" '.tools | has($t)' "$registry_file" >/dev/null; then
|
||||
uninstall_tool "$target"
|
||||
else
|
||||
rollback_to_savepoint "$target"
|
||||
fi
|
||||
rollback_to_savepoint "$target"
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
|
||||
@@ -158,6 +158,34 @@ remove_alias_snippet() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Write completion snippet to completions.d/
|
||||
# Usage: write_completion_snippet <name> <content>
|
||||
write_completion_snippet() {
|
||||
local name="$1"
|
||||
local content="$2"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
|
||||
|
||||
mkdir -p "$dir"
|
||||
log_info "Writing completion snippet '$name' to $dir/${name}.sh"
|
||||
echo "$content" > "$dir/${name}.sh"
|
||||
|
||||
if type add_rollback_cmd >/dev/null 2>&1; then
|
||||
add_rollback_cmd "rm -f \"$dir/${name}.sh\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove completion snippet from completions.d/
|
||||
# Usage: remove_completion_snippet <name>
|
||||
remove_completion_snippet() {
|
||||
local name="$1"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/completions.d"
|
||||
|
||||
if [ -f "$dir/${name}.sh" ]; then
|
||||
log_info "Removing completion snippet '$name'"
|
||||
rm -f "$dir/${name}.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup fd symlink for Debian/Ubuntu (fdfind -> fd)
|
||||
create_fd_symlink() {
|
||||
if ! has_command fd && has_command fdfind; then
|
||||
@@ -176,8 +204,4 @@ source_bashrc() {
|
||||
|
||||
# Export functions and variables for subshells
|
||||
export _LIB_SHELL_CONFIG_SOURCED=1
|
||||
export -f get_shell_configs remove_block inject_block add_alias_if_missing add_env_if_missing create_fd_symlink write_env_snippet write_alias_snippet remove_env_snippet remove_alias_snippet source_bashrc
|
||||
|
||||
|
||||
|
||||
|
||||
export -f get_shell_configs remove_block inject_block add_alias_if_missing add_env_if_missing create_fd_symlink write_env_snippet write_alias_snippet remove_env_snippet remove_alias_snippet write_completion_snippet remove_completion_snippet source_bashrc
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"plugins": {
|
||||
"auth": {
|
||||
"version": "1.0.0",
|
||||
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/auth.sh",
|
||||
"bootstrap": "2.2.0",
|
||||
"description": "Client Authentication and Provisioning Plugin"
|
||||
},
|
||||
"weather": {
|
||||
"version": "1.0.0",
|
||||
"url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/weather.sh",
|
||||
|
||||
242
plugins/auth.sh
Normal file
242
plugins/auth.sh
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Authentication & Provisioning Plugin for Bootstrap CLI
|
||||
# Handles requester (b me) and approver (b trust) flows.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure dependencies are met
|
||||
pkg_install "arch:openssh|debian:openssh-client|fedora:openssh-clients" "curl" "jq" "age"
|
||||
|
||||
|
||||
# Ensure public key exists next to private key for ssh-keygen -Y sign
|
||||
ensure_pubkey_exists() {
|
||||
local priv_key="$1"
|
||||
local pub_key="${priv_key}.pub"
|
||||
if [ ! -f "$pub_key" ]; then
|
||||
ssh-keygen -y -f "$priv_key" > "$pub_key"
|
||||
fi
|
||||
}
|
||||
|
||||
COMMAND="${1:-}"
|
||||
if [ -z "$COMMAND" ]; then
|
||||
echo "Usage: b auth <me|trust> [args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
|
||||
# Defaults
|
||||
SERVER_URL="https://b.adityagupta.dev/auth"
|
||||
KEY_DIR="$HOME/.config/bootstrap-client"
|
||||
POLL_INTERVAL=5
|
||||
ADMIN_KEY="$HOME/.ssh/id_ed25519"
|
||||
USER_CODE=""
|
||||
|
||||
if [ "$COMMAND" = "trust" ]; then
|
||||
if [ $# -lt 1 ]; then
|
||||
log_error "user_code is required for trust."
|
||||
echo "Usage: b trust <user_code> [--server <server_url>] [--admin-key <path>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
USER_CODE="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Parse remaining arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server)
|
||||
SERVER_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--key-dir)
|
||||
KEY_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
POLL_INTERVAL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--admin-key)
|
||||
ADMIN_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$COMMAND" = "me" ]; then
|
||||
mkdir -p "$KEY_DIR"
|
||||
local_key="$KEY_DIR/id_ed25519"
|
||||
|
||||
if [ ! -f "$local_key" ]; then
|
||||
log_info "Generating local Ed25519 key pair under $KEY_DIR..."
|
||||
ssh-keygen -t ed25519 -N "" -f "$local_key" >/dev/null
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$local_key"
|
||||
pub_key=$(cat "${local_key}.pub")
|
||||
hostname=$(hostname 2>/dev/null || uname -n)
|
||||
os=$(uname -s 2>/dev/null || echo "linux")
|
||||
|
||||
# Safely construct JSON payload
|
||||
json_payload=$(jq -n \
|
||||
--arg hn "$hostname" \
|
||||
--arg os "$os" \
|
||||
--arg pk "$pub_key" \
|
||||
'{hostname: $hn, os: $os, public_key: $pk}')
|
||||
|
||||
log_info "Registering device with $SERVER_URL..."
|
||||
|
||||
register_response=$(curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$json_payload" \
|
||||
"$SERVER_URL/api/register")
|
||||
|
||||
user_code=$(echo "$register_response" | jq -r '.user_code // empty')
|
||||
challenge_nonce=$(echo "$register_response" | jq -r '.challenge_nonce // empty')
|
||||
|
||||
if [ -z "$user_code" ] || [ -z "$challenge_nonce" ]; then
|
||||
log_error "Failed to retrieve registration codes from server response."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
log_success "Device registration initiated successfully!"
|
||||
echo "Please authorize this device on your administrator machine using:"
|
||||
echo " b trust $user_code --server $SERVER_URL"
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Verification Code: $user_code"
|
||||
echo "--------------------------------------------------------"
|
||||
log_info "Waiting for administrator approval (polling every ${POLL_INTERVAL}s)..."
|
||||
|
||||
# Prepare challenge poll file signing
|
||||
temp_nonce_file=$(mktemp)
|
||||
temp_sig_file="${temp_nonce_file}.sig"
|
||||
echo -n "$challenge_nonce" > "$temp_nonce_file"
|
||||
|
||||
# Ensure cleanup of temp files
|
||||
cleanup() {
|
||||
rm -f "$temp_nonce_file" "$temp_sig_file"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
rm -f "$temp_sig_file"
|
||||
|
||||
# Sign challenge nonce
|
||||
if ! ssh-keygen -Y sign -f "$local_key" -n "bootstrap" "$temp_nonce_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing of challenge nonce failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get raw base64 from armored signature file
|
||||
signature_b64=$(grep -v '^-' "$temp_sig_file" | tr -d '\n')
|
||||
|
||||
poll_payload=$(jq -n \
|
||||
--arg uc "$user_code" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, signature: $sig}')
|
||||
|
||||
poll_out=$(mktemp)
|
||||
http_code=$(curl -s -o "$poll_out" -w "%{http_code}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$poll_payload" \
|
||||
"$SERVER_URL/api/challenge/poll")
|
||||
|
||||
poll_body=$(cat "$poll_out")
|
||||
rm -f "$poll_out"
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
enc_secrets=$(echo "$poll_body" | jq -r '.encrypted_secrets // empty')
|
||||
if [ -n "$enc_secrets" ] && [ "$enc_secrets" != "null" ]; then
|
||||
log_success "Device approved by administrator! Decrypting secrets payload..."
|
||||
|
||||
decrypted_file="$KEY_DIR/secrets.decrypted"
|
||||
if echo "$enc_secrets" | base64 -d | age --decrypt -i "$local_key" > "$decrypted_file" 2>/dev/null; then
|
||||
log_success "Secrets successfully provisioned and written to: $decrypted_file"
|
||||
cat "$decrypted_file"
|
||||
break
|
||||
else
|
||||
log_error "Decryption using age failed. Please ensure the private key has not been altered."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
|
||||
elif [ "$COMMAND" = "trust" ]; then
|
||||
if [ ! -f "$ADMIN_KEY" ]; then
|
||||
log_error "Admin private key not found at: $ADMIN_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_pubkey_exists "$ADMIN_KEY"
|
||||
|
||||
log_info "Fetching pending device details for user code: $USER_CODE"
|
||||
pending_response=$(curl -fsSL "$SERVER_URL/api/pending/$USER_CODE")
|
||||
|
||||
requester_pub_key=$(echo "$pending_response" | jq -r '.public_key // empty')
|
||||
if [ -z "$requester_pub_key" ]; then
|
||||
log_error "No pending registration found for code '$USER_CODE'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--------------------------------------------------------"
|
||||
echo "Pending Device Public Key:"
|
||||
echo "$requester_pub_key"
|
||||
echo "--------------------------------------------------------"
|
||||
|
||||
# Prompt for confirmation (read from tty to support pipeline scenarios)
|
||||
read -r -p "Do you trust and approve this device? [y/N]: " confirm_choice </dev/tty || confirm_choice="N"
|
||||
if [[ ! "$confirm_choice" =~ ^[Yy]$ ]]; then
|
||||
log_warn "Approval aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate signature of the requester's public key
|
||||
temp_pubkey_file=$(mktemp)
|
||||
temp_pubkey_sig_file="${temp_pubkey_file}.sig"
|
||||
echo -n "$requester_pub_key" > "$temp_pubkey_file"
|
||||
|
||||
# Cleanup trap
|
||||
cleanup_trust() {
|
||||
rm -f "$temp_pubkey_file" "$temp_pubkey_sig_file"
|
||||
}
|
||||
trap cleanup_trust EXIT INT TERM
|
||||
|
||||
if ! ssh-keygen -Y sign -f "$ADMIN_KEY" -n "bootstrap" "$temp_pubkey_file" >/dev/null 2>&1; then
|
||||
log_error "Cryptographic signing using administrator key failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
signature_b64=$(grep -v '^-' "$temp_pubkey_sig_file" | tr -d '\n')
|
||||
|
||||
# Get fingerprint
|
||||
admin_pubkey_str=$(ssh-keygen -y -f "$ADMIN_KEY")
|
||||
temp_admin_pub=$(mktemp)
|
||||
echo "$admin_pubkey_str" > "$temp_admin_pub"
|
||||
approver_fingerprint=$(ssh-keygen -lf "$temp_admin_pub" | awk '{print $2}')
|
||||
rm -f "$temp_admin_pub"
|
||||
|
||||
# Prepare payload
|
||||
approve_payload=$(jq -n \
|
||||
--arg uc "$USER_CODE" \
|
||||
--arg fp "$approver_fingerprint" \
|
||||
--arg sig "$signature_b64" \
|
||||
'{user_code: $uc, approver_public_key_fingerprint: $fp, signature: $sig}')
|
||||
|
||||
log_info "Submitting cryptographic approval to server..."
|
||||
curl -fsSL -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$approve_payload" \
|
||||
"$SERVER_URL/api/approve"
|
||||
|
||||
log_success "Device with code $USER_CODE has been approved."
|
||||
fi
|
||||
@@ -13,7 +13,6 @@ 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
|
||||
@@ -21,12 +20,10 @@ for f in "$INSTALLERS_DIR"/install_*.sh; do
|
||||
tool=$(grep -E "^# Tool:" "$f" | head -n1 | sed -E 's/^# Tool:\s*//I')
|
||||
disp_name=$(grep -E "^# DisplayName:" "$f" | head -n1 | sed -E 's/^# DisplayName:\s*//I')
|
||||
desc=$(grep -E "^# Description:" "$f" | head -n1 | sed -E 's/^# Description:\s*//I' | sed -E 's/^Install\s+//I')
|
||||
strat=$(grep -E "^# Strategy:" "$f" | head -n1 | sed -E 's/^# Strategy:\s*//I')
|
||||
|
||||
if [ -n "$tool" ]; then
|
||||
tools_desc["$tool"]="$desc"
|
||||
tools_disp["$tool"]="${disp_name:-$tool}"
|
||||
tools_strat["$tool"]="${strat:-unknown}"
|
||||
keys+=("$tool")
|
||||
fi
|
||||
done
|
||||
@@ -52,13 +49,6 @@ sorted_keys=($(printf '%s\n' "${keys[@]}" | sort))
|
||||
done
|
||||
echo ")"
|
||||
echo ""
|
||||
echo "declare -A INSTALLER_STRATEGIES=("
|
||||
for k in "${sorted_keys[@]}"; do
|
||||
escaped_strat=$(echo "${tools_strat[$k]}" | sed 's/"/\\"/g')
|
||||
echo " [$k]=\"$escaped_strat\""
|
||||
done
|
||||
echo ")"
|
||||
echo ""
|
||||
# Format keys output as space-separated list in array declaration format
|
||||
echo "INSTALLER_KEYS=(${sorted_keys[*]})"
|
||||
} > "$REGISTRY_FILE"
|
||||
|
||||
Reference in New Issue
Block a user