Compare commits
38 Commits
v1.2.0
...
feat/auth-
| Author | SHA1 | Date | |
|---|---|---|---|
| db6ec1c1c8 | |||
| ed56ef95a9 | |||
| 671cf7f818 | |||
| f5227569b1 | |||
| 15d3a1a59d | |||
| f158c4e913 | |||
| fc4303bc99 | |||
| 6b0d07d70a | |||
| f8f41e4295 | |||
| a254001da8 | |||
| 9a7404a65f | |||
| b697fc5bba | |||
| 355588c7f9 | |||
| d108f14ce5 | |||
| 62a4759724 | |||
| fdb2e108ee | |||
| 9c86486ee6 | |||
| 33b98477bf | |||
| b813061e9a | |||
| 7fe9ac913b | |||
| 53e98c7542 | |||
| 02d3c9241c | |||
| c88839d3e0 | |||
| c5e11891a8 | |||
| 393868610f | |||
| 368dea1bbd | |||
| b31a326ca1 | |||
| dc73804416 | |||
| f118d66ec1 | |||
| 0486755771 | |||
| 725e3879d8 | |||
| 234112f304 | |||
| 6fde048250 | |||
| 9ce16a1f2b | |||
| 57a11e16a3 | |||
| b66fb4a354 | |||
| a56bee0b9c | |||
| 7e48f01cca |
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.
|
||||
@@ -18,8 +18,9 @@ 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()
|
||||
│ ├── shell_config.sh # get_shell_configs(), inject_block(), remove_block(), add_alias_if_missing(), add_env_if_missing()
|
||||
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install(), pkg_check(), pkg_remove()
|
||||
│ ├── rollback.sh # Rollback tracking (track_file, track_dir, add_rollback_cmd)
|
||||
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet, write_completion_snippet
|
||||
│ ├── registry.sh # Dynamically generated installer registry
|
||||
│ └── routes.sh # Central router script
|
||||
├── commands/ # Non-installer commands (help, con, uninstall)
|
||||
@@ -34,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
|
||||
@@ -55,7 +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: Verify (optional)
|
||||
### Step 4: Implement Rollback Tracking (Crucial)
|
||||
|
||||
To ensure the user can seamlessly use `b rb <name>`, all manual modifications must be tracked:
|
||||
- When extracting binaries to `~/.local/bin/`, use `track_file "$HOME/.local/bin/binary"`.
|
||||
- When creating directories like `~/.config/tool/`, use `track_dir "$HOME/.config/tool"`.
|
||||
- When running manual apt/dnf/npm commands, log their inverses: `add_rollback_cmd "sudo npm uninstall -g package"`.
|
||||
Note: `pkg_install`, `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` will automatically track themselves.
|
||||
|
||||
### 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.
|
||||
@@ -77,20 +95,10 @@ Every installer follows this exact boilerplate structure. Copy this and fill in
|
||||
# <ToolName> Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -111,35 +119,24 @@ install_<name>() {
|
||||
fi
|
||||
|
||||
# --- Tool-specific installation logic goes here ---
|
||||
# Use pkg_install for distro packages:
|
||||
# pkg_install <package>
|
||||
# Use detect_distro for distro-specific logic:
|
||||
# local distro; distro=$(detect_distro)
|
||||
# Use detect_arch for arch-specific logic:
|
||||
# local arch; arch=$(detect_arch)
|
||||
# For GitHub releases, use curl pattern (see bat installer for reference)
|
||||
# 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() {
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
log_info "Configuring <ToolName> in $config_file..."
|
||||
|
||||
# Use inject_block to add shell init/aliases/env vars:
|
||||
# inject_block "$config_file" "<name> init" "<content>"
|
||||
# Use add_alias_if_missing for simple aliases:
|
||||
# add_alias_if_missing "$config_file" "<alias>" "<value>"
|
||||
# Use add_env_if_missing for environment variables:
|
||||
# add_env_if_missing "$config_file" "VAR_NAME" "value"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
# 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 ─────────────────────────────────────────────────────────────
|
||||
@@ -172,6 +169,7 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
| `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`
|
||||
|
||||
@@ -179,18 +177,24 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|
||||
|---|---|
|
||||
| `detect_distro` | Echoes `arch`, `debian`, `fedora`, or `unknown` |
|
||||
| `detect_arch` | Echoes `x86_64` or `arm64` |
|
||||
| `pkg_install <pkg>...` | Install packages via the system package manager. Supports distro-specific mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"` |
|
||||
| `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 |
|
||||
|---|---|
|
||||
| `get_shell_configs` | Space-separated list of existing RC files (`~/.bashrc`) |
|
||||
| `inject_block <file> <name> <content>` | Idempotently inject a named block into a config file (removes old block first) |
|
||||
| `remove_block <file> <name>` | Remove a named block from a config file |
|
||||
| `add_alias_if_missing <file> <alias> <value>` | Add an alias line if not already present |
|
||||
| `add_env_if_missing <file> <var> <value>` | Add an `export VAR="value"` line if not already present |
|
||||
| `create_fd_symlink` | Symlink `fdfind` → `fd` on Debian/Ubuntu |
|
||||
| `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. |
|
||||
|
||||
---
|
||||
|
||||
@@ -204,27 +208,10 @@ cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
```
|
||||
|
||||
### Distro-specific installation (e.g., GitHub .deb for Debian, pacman for Arch)
|
||||
### Distro-specific mapping
|
||||
|
||||
```bash
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
case "$distro" in
|
||||
arch)
|
||||
pkg_install <package>
|
||||
;;
|
||||
debian)
|
||||
# Download .deb from GitHub releases
|
||||
;;
|
||||
fedora)
|
||||
pkg_install <package>
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported distribution."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "curl" "git"
|
||||
```
|
||||
|
||||
### Fetching latest GitHub release tag
|
||||
@@ -243,11 +230,19 @@ if [ -z "$latest_tag" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
### Shell block injection (idempotent)
|
||||
### Resumable Download and Extraction
|
||||
|
||||
```bash
|
||||
# Block name should be unique and descriptive
|
||||
inject_block "$config_file" "<tool> init" 'eval "$(tool init 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"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -255,11 +250,11 @@ inject_block "$config_file" "<tool> init" 'eval "$(tool init bash)"'
|
||||
## Rules & Conventions
|
||||
|
||||
1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
|
||||
2. **Registry generation**: The registry in `lib/registry.sh` is automatically generated by `scripts/generate_registry.sh` (run automatically on commit by the git pre-commit hook).
|
||||
3. **Confirmation prompts**: Always ask before installing. Check if already installed first.
|
||||
4. **Idempotent**: Installers must be safe to re-run. Use `inject_block` (not append) for shell configs.
|
||||
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`, `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 sourcing `bootstrap.sh`.
|
||||
7. **Metascript boilerplate**: The first 22 lines of every installer are identical — always copy them verbatim.
|
||||
8. **`main "$@"`**: Always end with this pattern to pass through CLI arguments.
|
||||
9. **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.
|
||||
6. **Error handling**: Use `set -euo pipefail` after the guard block.
|
||||
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.
|
||||
8. **Clean Official Scripts**: When implementing official curl/install scripts provided in the prompt, strip them of bloat, macOS/Windows support, and redundant shell setups before writing the script.
|
||||
9. **No manual shell re-sourcing**: Do NOT manually run `source ~/.bashrc` or print instructions asking the user to run it. Sourcing of the shell configuration is handled automatically by the central router and CLI at the end of the installation.
|
||||
|
||||
@@ -25,12 +25,22 @@ When the user asks to "cut a release", "bump the version", or "tag a new version
|
||||
- If any core-CLI commit has `feat:` → **minor**
|
||||
- Otherwise (only `fix:`, `refactor:`, etc. in core-CLI) → **patch**
|
||||
- If *all* commits are installer-only or docs-only → inform the user no release is needed.
|
||||
3. **Run the release script** non-interactively (if an actual bump is needed):
|
||||
```bash
|
||||
./scripts/release.sh --<level> -y
|
||||
3. **Formulate a verbose, structured description of the changes** based on the analyzed commits. Group the changes into logical sections (such as "Breaking Changes & Major Features:" and "Other Updates:") and list the corresponding commit messages or summaries. Example format:
|
||||
```text
|
||||
Breaking Changes & Major Features:
|
||||
|
||||
feat: Resumable Download Helper and Manifest Preservation
|
||||
|
||||
Other Updates:
|
||||
|
||||
docs: Update readme
|
||||
feat(skills): Add Installer to use rollback and savepoint hooks
|
||||
```
|
||||
(e.g., `./scripts/release.sh --minor -y`)
|
||||
4. **Push** the tag and commit (ask for user confirmation before pushing):
|
||||
4. **Run the release script** non-interactively, passing the compiled description:
|
||||
```bash
|
||||
./scripts/release.sh --<level> -y -m "<verbose description>"
|
||||
```
|
||||
5. **Push** the tag and commit (ask for user confirmation before pushing):
|
||||
```bash
|
||||
git push origin master <tag>
|
||||
```
|
||||
|
||||
37
.gitea/workflows/lint.yml
Normal file
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
|
||||
11
b.sh
11
b.sh
@@ -66,6 +66,15 @@ b() {
|
||||
|
||||
# Execute the routes file
|
||||
bash "$routes_file" "$@"
|
||||
local ret=$?
|
||||
|
||||
# Sourced again in the parent shell after successfully running the command
|
||||
if [ $ret -eq 0 ]; then
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
. "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
return $ret
|
||||
}
|
||||
|
||||
# Autocompletion for the b command in Bash
|
||||
@@ -77,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=""
|
||||
|
||||
71
bootstrap.sh
71
bootstrap.sh
@@ -18,6 +18,10 @@ is_sourced=false
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then
|
||||
is_sourced=true
|
||||
fi
|
||||
# Detect eval from installers based on presence of specific variables
|
||||
if [ -n "${METASCRIPT_URL:-}" ]; then
|
||||
is_sourced=true
|
||||
fi
|
||||
|
||||
# Locate or download libraries so that sourced installers can use them
|
||||
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
|
||||
@@ -25,22 +29,19 @@ _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
|
||||
|
||||
if [ -f "$_SCRIPT_DIR/lib/common.sh" ]; then
|
||||
# Dev/local mode: source directly from repo
|
||||
. "$_SCRIPT_DIR/lib/common.sh"
|
||||
. "$_SCRIPT_DIR/lib/platform.sh"
|
||||
. "$_SCRIPT_DIR/lib/shell_config.sh"
|
||||
BOOTSTRAP_SOURCE_DIR="$_SCRIPT_DIR"
|
||||
elif [ -f "$BOOTSTRAP_DIR/lib/common.sh" ]; then
|
||||
# Installed mode: source from bootstrap dir
|
||||
. "$BOOTSTRAP_DIR/lib/common.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/platform.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/shell_config.sh"
|
||||
BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_DIR"
|
||||
else
|
||||
# Standalone/remote mode: download to a temp directory and source
|
||||
export BOOTSTRAP_TMP_DIR
|
||||
BOOTSTRAP_TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$BOOTSTRAP_TMP_DIR"' EXIT
|
||||
BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR"
|
||||
|
||||
_BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
_LIBS=("lib/common.sh" "lib/platform.sh" "lib/shell_config.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
|
||||
@@ -48,15 +49,17 @@ else
|
||||
_curl_args+=("-o" "$BOOTSTRAP_TMP_DIR/$_lib" "$_BASE_URL/$_lib")
|
||||
done
|
||||
curl -fsSL "${_curl_args[@]}" 2>/dev/null
|
||||
|
||||
if [ -f "$BOOTSTRAP_TMP_DIR/lib/common.sh" ]; then
|
||||
. "$BOOTSTRAP_TMP_DIR/lib/common.sh"
|
||||
. "$BOOTSTRAP_TMP_DIR/lib/platform.sh"
|
||||
. "$BOOTSTRAP_TMP_DIR/lib/shell_config.sh"
|
||||
else
|
||||
echo "Error: Failed to download bootstrap libraries." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" ]; then
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/common.sh"
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/rollback.sh"
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/platform.sh"
|
||||
. "$BOOTSTRAP_SOURCE_DIR/lib/shell_config.sh"
|
||||
init_rollback_system
|
||||
else
|
||||
echo "Error: Failed to locate or download bootstrap libraries." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install/update the bootstrap loader and download all necessary files
|
||||
@@ -66,6 +69,9 @@ install_bootstrap() {
|
||||
|
||||
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"
|
||||
|
||||
# List of all files to download/copy
|
||||
local files=(
|
||||
@@ -74,8 +80,11 @@ install_bootstrap() {
|
||||
"lib/routes.sh"
|
||||
"lib/registry.sh"
|
||||
"lib/common.sh"
|
||||
"lib/rollback.sh"
|
||||
"lib/platform.sh"
|
||||
"lib/shell_config.sh"
|
||||
"lib/json.sh"
|
||||
"lib/plugins.sh"
|
||||
"commands/help.sh"
|
||||
"commands/con.sh"
|
||||
"commands/uninstall.sh"
|
||||
@@ -96,6 +105,12 @@ install_bootstrap() {
|
||||
mkdir -p "$routes_dir/installers"
|
||||
cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/"
|
||||
fi
|
||||
|
||||
# Also copy plugins if they exist locally
|
||||
if [ -d "$_SCRIPT_DIR/plugins" ]; then
|
||||
mkdir -p "$routes_dir/plugins"
|
||||
cp -r "$_SCRIPT_DIR/plugins/"* "$routes_dir/plugins/"
|
||||
fi
|
||||
else
|
||||
log_info "Downloading bootstrap scripts..."
|
||||
local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
|
||||
@@ -125,13 +140,16 @@ install_bootstrap() {
|
||||
# 2. Clean up old loader block if it exists
|
||||
remove_block "$config_file" "bootstrap-cli setup"
|
||||
|
||||
# 3. Append the new lightweight loader block
|
||||
# 3. Append the new lightweight loader block that sources modular configs
|
||||
log_info "Adding bootstrap loader to $config_file..."
|
||||
cat << 'EOF' >> "$config_file"
|
||||
|
||||
# >>> bootstrap-cli setup >>>
|
||||
export BOOTSTRAP_DIR="$HOME/.config/bootstrap"
|
||||
[ -f "$BOOTSTRAP_DIR/b.sh" ] && . "$BOOTSTRAP_DIR/b.sh"
|
||||
for f in "$BOOTSTRAP_DIR/env.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
for f in "$BOOTSTRAP_DIR/aliases.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
for f in "$BOOTSTRAP_DIR/completions.d/"*.sh; do [ -r "$f" ] && . "$f"; done
|
||||
# <<< bootstrap-cli setup <<<
|
||||
EOF
|
||||
|
||||
@@ -172,19 +190,14 @@ if [ "$is_sourced" = false ]; then
|
||||
clear 2>/dev/null || true
|
||||
|
||||
# Locate or download pixel_art.ansi and VERSION
|
||||
_art_file="$_SCRIPT_DIR/assets/pixel_art.ansi"
|
||||
_version_file="$_SCRIPT_DIR/VERSION"
|
||||
_art_file="$BOOTSTRAP_SOURCE_DIR/assets/pixel_art.ansi"
|
||||
_version_file="$BOOTSTRAP_SOURCE_DIR/VERSION"
|
||||
|
||||
if [ ! -f "$_art_file" ]; then
|
||||
if [ -n "${BOOTSTRAP_TMP_DIR:-}" ] && [ -d "$BOOTSTRAP_TMP_DIR" ]; then
|
||||
_art_file="$BOOTSTRAP_TMP_DIR/pixel_art.ansi"
|
||||
_version_file="$BOOTSTRAP_TMP_DIR/VERSION"
|
||||
_base_url="${_BASE_URL:-https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master}"
|
||||
if [ ! -f "$_art_file" ]; then
|
||||
curl -fsSL -o "$_art_file" "$_base_url/assets/pixel_art.ansi" 2>/dev/null
|
||||
curl -fsSL -o "$_version_file" "$_base_url/VERSION" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
if [ ! -f "$_art_file" ] && [ -n "${BOOTSTRAP_TMP_DIR:-}" ]; then
|
||||
_base_url="${_BASE_URL:-https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master}"
|
||||
mkdir -p "$(dirname "$_art_file")"
|
||||
curl -fsSL -o "$_art_file" "$_base_url/assets/pixel_art.ansi" 2>/dev/null || true
|
||||
curl -fsSL -o "$_version_file" "$_base_url/VERSION" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -f "$_art_file" ]; then
|
||||
|
||||
@@ -48,6 +48,13 @@ if version_lt "$local_ver" "$remote_ver" || [ "$force_update" = true ]; then
|
||||
if bash "$tmp_bootstrap"; then
|
||||
# Update the last update timestamp
|
||||
date +%s > "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/.last_b_update" 2>/dev/null || true
|
||||
|
||||
# Update plugin cache
|
||||
if [ -f "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib/plugins.sh" ]; then
|
||||
. "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib/plugins.sh"
|
||||
update_plugin_cache
|
||||
fi
|
||||
|
||||
log_success "Bootstrap CLI successfully updated to version $remote_ver!"
|
||||
else
|
||||
log_error "Failed to execute bootstrap installer."
|
||||
|
||||
162
docs/client_spec_auth.md
Normal file
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.
|
||||
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 installers (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a sandboxed subshell**.
|
||||
|
||||
This means downloading and invoking a plugin makes no system modifications other than caching the `.sh` file itself. They are fetched only the very first time you invoke them.
|
||||
|
||||
## 1. Writing a Plugin Script
|
||||
|
||||
A plugin is fundamentally a single Bash script. When executed by a user via `b <plugin_name>`, `bootstrap` runs the script in a subshell. This guarantees that any variables or state changes your plugin makes to the shell environment will not leak into the parent shell, preserving the integrity of the user's terminal.
|
||||
|
||||
Because plugins execute within the `bootstrap` context, you automatically have access to all internal library functions (e.g., `lib/common.sh`, `lib/platform.sh`). For example, you can safely use logging functions like `log_info`, `log_success`, and `log_error`.
|
||||
|
||||
Example `my_plugin.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# My Awesome Plugin
|
||||
|
||||
# You can use bootstrap's built-in functions natively:
|
||||
log_info "Initializing awesome plugin..."
|
||||
|
||||
if [ "${1:-}" == "--help" ]; then
|
||||
echo "Usage: b my_plugin [args]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log_success "Task completed successfully!"
|
||||
```
|
||||
|
||||
## 2. Creating a Manifest
|
||||
|
||||
For your plugin to be discoverable and dynamically updatable by `bootstrap`, you must provide a JSON manifest. `bootstrap` uses a robust, native Bash-based JSON parser to read this manifest.
|
||||
|
||||
Create a JSON file (e.g., `plugins.json`) and host it publicly (e.g., as a GitHub raw URL).
|
||||
|
||||
Example `plugins.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"my_plugin": {
|
||||
"version": "1.0.0",
|
||||
"url": "https://raw.githubusercontent.com/yourusername/repo/main/my_plugin.sh",
|
||||
"bootstrap": "2.1.0",
|
||||
"description": "An awesome plugin that prints logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **`version`**: The current semantic version of your plugin. When `bootstrap` detects a version change during `b up`, it automatically clears the cached `.sh` file, forcing a lazy re-download on the next invocation.
|
||||
* **`url`**: The raw, direct URL to your `.sh` plugin script.
|
||||
* **`bootstrap`**: The latest version of `bootstrap` that this plugin has been tested against and is compatible with. If the user's `bootstrap` version is newer than this value, a warning is displayed notifying them of potential incompatibility.
|
||||
|
||||
## 3. Distribution
|
||||
|
||||
To let users install your plugin, simply provide them the raw URL to your JSON manifest.
|
||||
|
||||
Users will add it by running:
|
||||
|
||||
```bash
|
||||
b plugin sources
|
||||
```
|
||||
|
||||
They simply append your URL as a new line in the sources file. Once saved, `bootstrap` will automatically fetch your manifest and build a fast-lookup cache. The user can then immediately invoke your plugin:
|
||||
|
||||
```bash
|
||||
b my_plugin
|
||||
```
|
||||
133
docs/rollback_design.md
Normal file
133
docs/rollback_design.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Bootstrap CLI: Procedural Rollback System Design
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
**Location:** `~/.local/state/bootstrap/history.log`
|
||||
|
||||
### A. Creating a Savepoint (`b fall <name>`)
|
||||
The `b fall` command simply appends a marker to the history log.
|
||||
```bash
|
||||
echo "SAVEPOINT: $name" >> "$HOME/.local/state/bootstrap/history.log"
|
||||
```
|
||||
|
||||
### B. Tracking Installations
|
||||
Whenever an installation successfully completes, the `b` CLI appends an install marker:
|
||||
```bash
|
||||
echo "INSTALL: nvim" >> "$HOME/.local/state/bootstrap/history.log"
|
||||
```
|
||||
**Example History Log:**
|
||||
```text
|
||||
SAVEPOINT: init
|
||||
INSTALL: rust
|
||||
INSTALL: node
|
||||
SAVEPOINT: dev_setup
|
||||
INSTALL: yazi
|
||||
INSTALL: nvim
|
||||
```
|
||||
|
||||
### C. Bare Rollback (`b rb`)
|
||||
When `b rb` is executed without arguments, it rolls back the single most recent change:
|
||||
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.
|
||||
|
||||
### D. Savepoint Rollback (`b rb <name>`)
|
||||
When `b rb init` is executed, it rolls back all changes made after that 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.
|
||||
|
||||
## 4. Required Abstractions & Helper Modifications
|
||||
|
||||
### A. Context Initialization
|
||||
Before executing an installer 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")"
|
||||
touch "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
```
|
||||
|
||||
### B. Recording Commands (LIFO Execution)
|
||||
Rollback steps are safest when executed in reverse order. A helper prepends commands to the top of the manifest.
|
||||
```bash
|
||||
add_rollback_cmd() {
|
||||
local cmd="$1"
|
||||
sed -i "1i $cmd" "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
}
|
||||
```
|
||||
|
||||
### C. Modifying Existing Helpers
|
||||
Existing helpers automatically generate their own inverse commands.
|
||||
- **`pkg_install`:**
|
||||
```bash
|
||||
add_rollback_cmd "pkg_remove $pkg"
|
||||
```
|
||||
- **`write_env_snippet` / `write_alias_snippet`:**
|
||||
```bash
|
||||
add_rollback_cmd "rm -f \"$HOME/.config/bootstrap/env.d/$snippet_name.sh\""
|
||||
```
|
||||
|
||||
### D. New File Tracking Helpers
|
||||
```bash
|
||||
track_file() { add_rollback_cmd "sudo rm -f '$1'"; }
|
||||
track_dir() { add_rollback_cmd "sudo rm -rf '$1'"; }
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```bash
|
||||
log_info "Rolling back..."
|
||||
while IFS= read -r cmd; do
|
||||
[ -z "$cmd" ] && continue
|
||||
log_info "Executing: $cmd"
|
||||
eval "$cmd" || log_warn "Failed to execute rollback step: $cmd"
|
||||
done < "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
|
||||
rm -f "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
log_success "Rollback complete."
|
||||
```
|
||||
|
||||
## 6. Resilience Against User Modifications
|
||||
Because `b ware <tool>` allows users to modify installation 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.
|
||||
|
||||
## 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:
|
||||
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.
|
||||
- **Keep (k)**: Preserves the partial changes and leaves the `.cmds` manifest intact.
|
||||
3. In non-interactive environments (e.g., CI/CD or scripts), the CLI defaults to **automatic rollback** to keep the system clean.
|
||||
|
||||
### B. Resuming via Preserved Manifests
|
||||
If the user chooses to **keep** the partial state and runs `b <tool>` again:
|
||||
1. `setup_uninstaller_context` detects that a manifest already exists and that the tool was *not* successfully installed (no `INSTALL: <tool>` in the history log).
|
||||
2. It **preserves** the existing manifest instead of wiping it.
|
||||
3. As the script runs again from the top, new rollback commands are prepended to the existing manifest, maintaining the correct LIFO order without losing the tracking of previously completed steps.
|
||||
|
||||
### C. Resumable Downloads (Caching Layer)
|
||||
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.
|
||||
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,20 +6,10 @@
|
||||
# Antigravity CLI Installer Script (Linux Only)
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -106,7 +96,7 @@ install_agy() {
|
||||
fi
|
||||
|
||||
log_info "Downloading release package..."
|
||||
curl -fsSL "$url" -o "$staging_payload"
|
||||
download_file "$url" "$staging_payload"
|
||||
|
||||
# Verify SHA512 Checksum
|
||||
local actual_hash
|
||||
@@ -140,27 +130,20 @@ install_agy() {
|
||||
cp "$extracted_binary" "$BINARY_PATH"
|
||||
chmod +x "$BINARY_PATH"
|
||||
rm -rf "$staging_dir"
|
||||
|
||||
track_file "$BINARY_PATH"
|
||||
|
||||
log_success "Antigravity CLI successfully installed to $BINARY_PATH."
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Ensure $TARGET_DIR is in PATH for shell configurations if not present
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
|
||||
local path_content='export PATH="$HOME/.local/bin:$PATH"'
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
if [ -f "$config_file" ] && ! grep -q '\.local/bin' "$config_file" 2>/dev/null; then
|
||||
log_info "Adding ~/.local/bin to PATH in $config_file..."
|
||||
inject_block "$config_file" "local-bin path" "$path_content"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
remove_block "$config_file" "local-bin path"
|
||||
done
|
||||
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
}
|
||||
|
||||
run_handoff() {
|
||||
|
||||
101
installers/install_asciicinema.sh
Normal file
101
installers/install_asciicinema.sh
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: asciicinema
|
||||
# DisplayName: asciicinema
|
||||
# Description: Install asciinema terminal recorder
|
||||
#
|
||||
# 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)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
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)
|
||||
fi
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
latest_tag="v3.2.1" # fallback
|
||||
log_warn "Failed to fetch latest version from GitHub. Falling back to: $latest_tag"
|
||||
else
|
||||
log_info "Latest asciinema version found: $latest_tag"
|
||||
fi
|
||||
|
||||
|
||||
if has_command asciinema; then
|
||||
local current_version
|
||||
current_version=$(asciinema --version | head -n1 | awk '{print $2}')
|
||||
if [[ "$current_version" != v* ]]; then
|
||||
current_version="v${current_version}"
|
||||
fi
|
||||
|
||||
if [[ "$current_version" == "$latest_tag" ]]; then
|
||||
log_info "asciinema ${latest_tag} is already installed."
|
||||
if ! confirm "Reinstall/Upgrade asciinema?"; then
|
||||
log_info "Skipping asciinema installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Detecting asciinema ${current_version}. Upgrade to ${latest_tag}?"; then
|
||||
log_info "Skipping asciinema installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install asciinema ${latest_tag}?"; then
|
||||
log_info "Skipping asciinema installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
local asciinema_arch=""
|
||||
case "$arch" in
|
||||
x86_64) asciinema_arch="x86_64-unknown-linux-gnu" ;;
|
||||
arm64) asciinema_arch="aarch64-unknown-linux-gnu" ;;
|
||||
*) 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"
|
||||
|
||||
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..."
|
||||
sudo ln -sf /usr/local/bin/asciinema /usr/local/bin/asciicinema
|
||||
track_file "/usr/local/bin/asciicinema"
|
||||
|
||||
log_success "asciinema ${latest_tag} installed."
|
||||
}
|
||||
|
||||
main() {
|
||||
install_asciicinema
|
||||
|
||||
echo
|
||||
log_success "asciinema installation complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -6,20 +6,10 @@
|
||||
# Bat Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -73,10 +63,11 @@ install_bat() {
|
||||
|
||||
local deb_url="https://github.com/sharkdp/bat/releases/download/${latest_tag}/bat_${version}_${deb_arch}.deb"
|
||||
log_info "Downloading Bat from ${deb_url}..."
|
||||
curl -fsSL "$deb_url" -o "$TMP_DIR/bat.deb"
|
||||
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."
|
||||
@@ -85,31 +76,16 @@ install_bat() {
|
||||
}
|
||||
|
||||
configure_shell() {
|
||||
# Clean up legacy in-place configuration blocks
|
||||
IFS=' ' read -ra target_files <<< "$(get_shell_configs)"
|
||||
|
||||
local content="alias cat='bat --paging=never -p'"
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
local target_file="$config_file"
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
# Clean up old block from ~/.bashrc if present to avoid duplication
|
||||
remove_block "$config_file" "bat alias"
|
||||
target_file="$HOME/.bash_aliases"
|
||||
# Ensure the file exists
|
||||
if [ ! -f "$target_file" ]; then
|
||||
touch "$target_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Adding bat alias to $target_file..."
|
||||
inject_block "$target_file" "bat alias" "$content"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
log_info "Sourcing $config_file..."
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
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() {
|
||||
@@ -118,7 +94,6 @@ main() {
|
||||
|
||||
echo
|
||||
log_success "Bat installation and configuration complete."
|
||||
log_info "Please close and reopen your terminal or run: source ~/.bashrc to apply changes."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
72
installers/install_docker.sh
Normal file
72
installers/install_docker.sh
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: docker
|
||||
# DisplayName: Docker
|
||||
# Description: Container runtime and orchestration platform
|
||||
#
|
||||
# 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 ──────────────────────────────────────────────
|
||||
|
||||
install_docker() {
|
||||
if has_command docker; then
|
||||
if ! confirm "Docker is already installed. Reinstall/Upgrade?"; then
|
||||
log_info "Skipping Docker installation."
|
||||
return
|
||||
fi
|
||||
else
|
||||
if ! confirm "Install Docker?"; then
|
||||
log_info "Skipping Docker installation."
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use pkg_install for distro packages (it automatically handles rollback hooks for the packages!)
|
||||
pkg_install "arch:docker|debian:docker.io|fedora:docker"
|
||||
|
||||
# Ensure docker group exists (some distros might not create it immediately)
|
||||
if ! getent group docker >/dev/null 2>&1; then
|
||||
log_info "Creating docker group..."
|
||||
add_rollback_cmd "sudo groupdel docker || true"
|
||||
sudo groupadd docker
|
||||
fi
|
||||
|
||||
# Configure user group
|
||||
if ! groups "$USER" | grep -q "\bdocker\b"; then
|
||||
log_info "Adding $USER to the docker group..."
|
||||
add_rollback_cmd "sudo gpasswd -d $USER docker || true"
|
||||
sudo usermod -aG docker "$USER"
|
||||
log_warn "You will need to log out and log back in, or run 'newgrp docker' for the group changes to take effect."
|
||||
fi
|
||||
|
||||
# Enable and start systemd services
|
||||
if has_command systemctl; then
|
||||
log_info "Enabling and starting Docker services..."
|
||||
|
||||
# Add rollback cmds for systemd
|
||||
add_rollback_cmd "sudo systemctl disable --now docker.service || true"
|
||||
add_rollback_cmd "sudo systemctl disable --now containerd.service || true"
|
||||
|
||||
sudo systemctl enable --now docker.service || true
|
||||
sudo systemctl enable --now containerd.service || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
install_docker
|
||||
|
||||
echo
|
||||
log_success "Docker installation and configuration complete."
|
||||
}
|
||||
|
||||
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 "$@"
|
||||
81
installers/install_lazygit.sh
Executable file
81
installers/install_lazygit.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: lazygit
|
||||
# DisplayName: lazygit
|
||||
# Description: Simple terminal UI for git commands
|
||||
#
|
||||
# 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 ──────────────────────────────────────────────
|
||||
|
||||
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=""
|
||||
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
|
||||
log_warn "Failed to fetch latest version. Falling back to: $latest_tag"
|
||||
fi
|
||||
|
||||
local version="${latest_tag#v}"
|
||||
|
||||
local arch=$(detect_arch)
|
||||
local arch_str="x86_64"
|
||||
if [ "$arch" = "arm64" ]; then
|
||||
arch_str="arm64"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
local dest="$TMP_DIR/lazygit.tar.gz"
|
||||
|
||||
log_info "Downloading lazygit ${latest_tag}..."
|
||||
download_file "$url" "$dest"
|
||||
|
||||
log_info "Extracting..."
|
||||
tar -xzf "$dest" -C "$TMP_DIR"
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
install_lazygit
|
||||
|
||||
echo
|
||||
log_success "lazygit installation complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -6,20 +6,10 @@
|
||||
# Node.js and NVM Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -55,18 +45,23 @@ install_nvm() {
|
||||
|
||||
local nvm_url="https://github.com/nvm-sh/nvm/archive/refs/tags/${latest_tag}.tar.gz"
|
||||
log_info "Downloading NVM from $nvm_url..."
|
||||
|
||||
curl -fsSL "$nvm_url" -o "$TMP_DIR/nvm.tar.gz"
|
||||
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
|
||||
|
||||
track_dir "$HOME/.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'
|
||||
@@ -76,16 +71,7 @@ export NVM_DIR="$HOME/.nvm"
|
||||
EOF
|
||||
)
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
log_info "Adding NVM configuration block to $config_file..."
|
||||
inject_block "$config_file" "nvm setup" "$content"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
log_info "Sourcing $config_file..."
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
write_env_snippet "node" "$content"
|
||||
}
|
||||
|
||||
install_node() {
|
||||
@@ -123,7 +109,6 @@ main() {
|
||||
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."
|
||||
log_info "Please close and reopen your terminal or run: source ~/.bashrc to verify."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool: nvim
|
||||
# DisplayName: Neovim
|
||||
# Description: Install Neovim 0.11.7 and configuration
|
||||
# Description: Install Neovim 0.12.0 and configuration
|
||||
#
|
||||
# Neovim Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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.11.7"
|
||||
NVIM_VERSION="0.12.0"
|
||||
NVIM_INSTALL_DIR="/opt/nvim"
|
||||
NVIM_CONFIG_REPO="https://git.adityagupta.dev/sortedcord/editor.git"
|
||||
NVIM_CONFIG_DIR="$HOME/.config/nvim"
|
||||
@@ -54,6 +44,10 @@ install_packages() {
|
||||
"fedora:gcc-c++"
|
||||
|
||||
create_fd_symlink
|
||||
|
||||
log_info "Installing tree-sitter-cli globally..."
|
||||
sudo npm install -g tree-sitter-cli
|
||||
add_rollback_cmd "sudo npm uninstall -g tree-sitter-cli"
|
||||
}
|
||||
|
||||
install_nvim() {
|
||||
@@ -85,7 +79,7 @@ install_nvim() {
|
||||
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}..."
|
||||
curl -fsSL "$nvim_url" -o "$TMP_DIR/nvim.tar.gz"
|
||||
download_file "$nvim_url" "$TMP_DIR/nvim.tar.gz"
|
||||
|
||||
tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR"
|
||||
|
||||
@@ -93,6 +87,9 @@ install_nvim() {
|
||||
sudo mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR"
|
||||
|
||||
sudo ln -sf "$NVIM_INSTALL_DIR/bin/nvim" /usr/local/bin/nvim
|
||||
|
||||
track_dir "$NVIM_INSTALL_DIR"
|
||||
track_file "/usr/local/bin/nvim"
|
||||
|
||||
log_success "Installed:"
|
||||
nvim --version | head -n1
|
||||
@@ -109,28 +106,32 @@ install_config() {
|
||||
|
||||
log_info "Cloning configuration to $NVIM_CONFIG_DIR..."
|
||||
git clone "$NVIM_CONFIG_REPO" "$NVIM_CONFIG_DIR"
|
||||
track_dir "$NVIM_CONFIG_DIR"
|
||||
log_success "Configuration installed."
|
||||
}
|
||||
|
||||
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
|
||||
local modified=false
|
||||
|
||||
if add_alias_if_missing "$config_file" "vim" "nvim"; then
|
||||
modified=true
|
||||
fi
|
||||
|
||||
if add_env_if_missing "$config_file" "EDITOR" "nvim"; then
|
||||
modified=true
|
||||
fi
|
||||
|
||||
# Source if modified (only for bashrc, and not when sourced to prevent recursion)
|
||||
if [ "$modified" = true ] && [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
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"'
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -17,20 +17,10 @@
|
||||
# curl -fsSL https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -44,7 +34,11 @@ trap cleanup EXIT
|
||||
# ─── Helper Functions ─────────────────────────────────────────────────
|
||||
|
||||
download() {
|
||||
if [ -n "${2:-}" ]; then
|
||||
download_file "$1" "$2"
|
||||
else
|
||||
curl -fsSL "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
is_glibc_compatible() {
|
||||
@@ -157,7 +151,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" || {
|
||||
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
|
||||
}
|
||||
@@ -172,7 +166,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" || {
|
||||
download "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset_base}" "$TMP_DIR/pnpm" || {
|
||||
log_error "Failed to download pnpm binary."
|
||||
return 1
|
||||
}
|
||||
@@ -183,13 +177,18 @@ install_pnpm() {
|
||||
}
|
||||
fi
|
||||
|
||||
track_dir "$HOME/.local/share/pnpm"
|
||||
log_success "pnpm v${version} installed successfully!"
|
||||
}
|
||||
|
||||
# ─── 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.
|
||||
@@ -204,15 +203,7 @@ esac
|
||||
EOF
|
||||
)
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
log_info "Configuring pnpm in $config_file..."
|
||||
inject_block "$config_file" "pnpm setup" "$content"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
write_env_snippet "pnpm" "$content"
|
||||
}
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────
|
||||
@@ -227,7 +218,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
|
||||
}
|
||||
|
||||
|
||||
@@ -6,24 +6,20 @@
|
||||
# Rust Installer Script (Simplified Local Rustup Init)
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
|
||||
# Ensure we have curl
|
||||
install_downloader() {
|
||||
if ! has_command curl; then
|
||||
@@ -77,17 +73,10 @@ install_rust() {
|
||||
|
||||
local url="https://static.rust-lang.org/rustup/dist/${target}/rustup-init"
|
||||
|
||||
local tmpdir
|
||||
tmpdir="$(make_temp_dir)"
|
||||
cleanup() {
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
local dest="$tmpdir/rustup-init"
|
||||
local dest="$TMP_DIR/rustup-init"
|
||||
|
||||
log_info "Downloading rustup-init..."
|
||||
curl -fsSL \"$url\" -o \"$dest\"| curl -fsSL \"$url\" -o \"$dest\"
|
||||
download_file "$url" "$dest"
|
||||
|
||||
chmod +x "$dest"
|
||||
|
||||
@@ -96,25 +85,21 @@ install_rust() {
|
||||
# -y: skip prompts (we already confirmed)
|
||||
# --no-modify-path: let bootstrap manage the shell paths
|
||||
"$dest" -y --no-modify-path
|
||||
|
||||
add_rollback_cmd "rustup self uninstall -y"
|
||||
}
|
||||
|
||||
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
|
||||
log_info "Configuring Rust environment in $config_file..."
|
||||
local content='. "$HOME/.cargo/env"'
|
||||
|
||||
inject_block "$config_file" "rust init" "$content"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
remove_block "$config_file" "rust init"
|
||||
done
|
||||
|
||||
write_env_snippet "rust" '. "$HOME/.cargo/env"'
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
# Starship Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -69,7 +59,7 @@ install_starship() {
|
||||
|
||||
log_info "Downloading Starship from ${download_url}..."
|
||||
local archive="$TMP_DIR/starship.tar.gz"
|
||||
curl -fsSL "$download_url" -o "$archive"
|
||||
download_file "$download_url" "$archive"
|
||||
|
||||
# Extract the binary
|
||||
log_info "Extracting Starship binary..."
|
||||
@@ -81,29 +71,22 @@ install_starship() {
|
||||
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"
|
||||
|
||||
local config_file="$HOME/.bashrc"
|
||||
if [ -f "$config_file" ]; then
|
||||
# Ensure ~/.local/bin is in PATH for this file if not already present
|
||||
if ! grep -q '\.local/bin' "$config_file" 2>/dev/null; then
|
||||
log_info "Adding ~/.local/bin to PATH in $config_file..."
|
||||
local path_content='export PATH="$HOME/.local/bin:$PATH"'
|
||||
inject_block "$config_file" "local-bin path" "$path_content"
|
||||
fi
|
||||
# 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
|
||||
|
||||
log_info "Adding starship initialization to $config_file..."
|
||||
local content='eval "$(starship init bash)"'
|
||||
|
||||
inject_block "$config_file" "starship init" "$content"
|
||||
|
||||
# Source to apply changes in the current context
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
write_env_snippet "local-bin" 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
write_env_snippet "starship" 'eval "$(starship init bash)"'
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
# uv Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -78,7 +68,7 @@ install_uv() {
|
||||
|
||||
log_info "Downloading uv from ${download_url}..."
|
||||
local archive="$TMP_DIR/uv.tar.gz"
|
||||
curl -fsSL "$download_url" -o "$archive"
|
||||
download_file "$download_url" "$archive"
|
||||
|
||||
# Extract the binaries
|
||||
log_info "Extracting uv binaries..."
|
||||
@@ -91,31 +81,23 @@ install_uv() {
|
||||
cp "$TMP_DIR/uv" "$target_dir/uv"
|
||||
cp "$TMP_DIR/uvx" "$target_dir/uvx"
|
||||
chmod +x "$target_dir/uv" "$target_dir/uvx"
|
||||
track_file "$target_dir/uv"
|
||||
track_file "$target_dir/uvx"
|
||||
}
|
||||
|
||||
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
|
||||
# Ensure ~/.local/bin is in PATH for this file if not already present
|
||||
if ! grep -q '\.local/bin' "$config_file" 2>/dev/null; then
|
||||
log_info "Adding ~/.local/bin to PATH in $config_file..."
|
||||
local path_content='export PATH="$HOME/.local/bin:$PATH"'
|
||||
inject_block "$config_file" "local-bin path" "$path_content"
|
||||
fi
|
||||
|
||||
log_info "Adding uv completion to $config_file..."
|
||||
local content='eval "$(uv generate-shell-completion bash)"'
|
||||
inject_block "$config_file" "uv completion" "$content"
|
||||
|
||||
# Source to apply changes in the current context
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
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)"'
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
# Yay Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -40,10 +30,10 @@ install_yay() {
|
||||
fi
|
||||
|
||||
local needs_install=false
|
||||
if ! pacman -Qq git &>/dev/null; then
|
||||
if ! pkg_check git; then
|
||||
needs_install=true
|
||||
fi
|
||||
if ! pacman -Qq base-devel &>/dev/null && ! pacman -Qg base-devel &>/dev/null; then
|
||||
if ! pkg_check base-devel && ! pacman -Qg base-devel &>/dev/null; then
|
||||
needs_install=true
|
||||
fi
|
||||
|
||||
@@ -71,6 +61,7 @@ install_yay() {
|
||||
|
||||
log_info "Building and installing yay..."
|
||||
makepkg -si
|
||||
add_rollback_cmd "sudo pacman -R --noconfirm yay"
|
||||
|
||||
cd "$orig_dir"
|
||||
log_info "Cleaning up installer directory..."
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
# Yazi Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -31,7 +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'
|
||||
@@ -47,16 +41,7 @@ y() {
|
||||
EOF
|
||||
)
|
||||
|
||||
for config_file in "${target_files[@]}"; do
|
||||
log_info "Adding yazi wrapper function 'y' to $config_file..."
|
||||
inject_block "$config_file" "yazi wrapper" "$wrapper_content"
|
||||
done
|
||||
|
||||
# Source ~/.bashrc to make the alias immediately available in the current shell context (if sourced)
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
log_info "Sourcing ~/.bashrc..."
|
||||
. "$HOME/.bashrc" 2>/dev/null || true
|
||||
fi
|
||||
write_alias_snippet "yazi" "$wrapper_content"
|
||||
}
|
||||
|
||||
install_yazi() {
|
||||
@@ -84,17 +69,18 @@ install_yazi() {
|
||||
|
||||
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/')
|
||||
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}..."
|
||||
curl -fsSL "$deb_url" -o "$TMP_DIR/yazi.deb"
|
||||
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 || \
|
||||
@@ -116,6 +102,7 @@ install_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
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
# Zoxide Installer Script
|
||||
#
|
||||
|
||||
# Run metascript to check if the shell is bash and load libraries
|
||||
PARENT_DIR="$(dirname "$0")/.."
|
||||
METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh"
|
||||
METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh"
|
||||
|
||||
if [ -f "$METASCRIPT_LOCAL" ]; then
|
||||
. "$METASCRIPT_LOCAL"
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
eval "$(curl -fsSL "$METASCRIPT_URL")"
|
||||
else
|
||||
echo "Error: curl is not installed to fetch bootstrap.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
@@ -50,25 +40,21 @@ install_zoxide() {
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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
|
||||
log_info "Adding zoxide initialization to $config_file..."
|
||||
local content="eval \"\$(zoxide init --cmd cd bash)\""
|
||||
|
||||
inject_block "$config_file" "zoxide init" "$content"
|
||||
|
||||
# Source if modified (only for bashrc)
|
||||
if [ "$config_file" = "$HOME/.bashrc" ]; then
|
||||
. "$config_file" 2>/dev/null || true
|
||||
fi
|
||||
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)"'
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -84,3 +84,89 @@ version_lt() {
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# Cached and resumable download helper
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local cache_dir="$HOME/.local/state/bootstrap/cache"
|
||||
|
||||
mkdir -p "$cache_dir"
|
||||
|
||||
local safe_name
|
||||
if has_command md5sum; then
|
||||
safe_name=$(echo -n "$url" | md5sum | cut -d' ' -f1)
|
||||
elif has_command shasum; then
|
||||
safe_name=$(echo -n "$url" | shasum | cut -d' ' -f1)
|
||||
else
|
||||
safe_name=$(echo -n "$url" | tr -c '[:alnum:]_.-' '_')
|
||||
fi
|
||||
|
||||
local base_name
|
||||
base_name=$(basename "$url")
|
||||
local cache_file="$cache_dir/${safe_name}_${base_name}"
|
||||
|
||||
log_info "Downloading $base_name (resumable)..."
|
||||
if ! curl -fL -C - "$url" -o "$cache_file"; then
|
||||
local exit_code=$?
|
||||
# Exit code 33: HTTP server doesn't support ranges/resuming
|
||||
# Exit code 36: Bad download resume offset
|
||||
if [ $exit_code -eq 33 ] || [ $exit_code -eq 36 ]; then
|
||||
log_warn "Server does not support resuming. Retrying from scratch..."
|
||||
rm -f "$cache_file"
|
||||
curl -fL "$url" -o "$cache_file" || return 1
|
||||
else
|
||||
return $exit_code
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
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 download_multiple_files_parallel
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
160
lib/platform.sh
160
lib/platform.sh
@@ -36,18 +36,9 @@ detect_arch() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Install packages depending on detected distro
|
||||
# Usage: pkg_install <package_name_arch> <package_name_debian> <package_name_fedora>
|
||||
# Or simpler: map common packages to their distro equivalents
|
||||
pkg_install() {
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
if [ "$distro" = "unknown" ]; then
|
||||
log_error "Unsupported distribution. Cannot install packages automatically."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_resolve_pkg_names() {
|
||||
local distro="$1"
|
||||
shift
|
||||
local pkgs=()
|
||||
for arg in "$@"; do
|
||||
# Format can be "pkg" or "arch:pkg_a|debian:pkg_d|fedora:pkg_f"
|
||||
@@ -69,22 +60,159 @@ pkg_install() {
|
||||
pkgs+=("$arg")
|
||||
fi
|
||||
done
|
||||
echo "${pkgs[@]}"
|
||||
}
|
||||
|
||||
# Install packages depending on detected distro
|
||||
# Usage: pkg_install <package_name_arch> <package_name_debian> <package_name_fedora>
|
||||
# Or simpler: map common packages to their distro equivalents
|
||||
pkg_install() {
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
if [ "$distro" = "unknown" ]; then
|
||||
log_error "Unsupported distribution. Cannot install packages automatically."
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS=' ' read -ra pkgs <<< "$(_resolve_pkg_names "$distro" "$@")"
|
||||
|
||||
if [ ${#pkgs[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Installing packages via $distro package manager: ${pkgs[*]}"
|
||||
local to_install=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Installing packages via $distro package manager: ${to_install[*]}"
|
||||
case "$distro" in
|
||||
arch)
|
||||
sudo pacman -Sy --needed --noconfirm "${pkgs[@]}"
|
||||
sudo pacman -Sy --needed --noconfirm "${to_install[@]}"
|
||||
;;
|
||||
debian)
|
||||
sudo apt update
|
||||
sudo apt install -y "${pkgs[@]}"
|
||||
sudo apt install -y "${to_install[@]}"
|
||||
;;
|
||||
fedora)
|
||||
sudo dnf install -y "${pkgs[@]}"
|
||||
sudo dnf install -y "${to_install[@]}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Check if packages are installed
|
||||
# Returns 0 if all are installed, 1 otherwise
|
||||
pkg_check() {
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
if [ "$distro" = "unknown" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS=' ' read -ra pkgs <<< "$(_resolve_pkg_names "$distro" "$@")"
|
||||
|
||||
if [ ${#pkgs[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$distro" in
|
||||
arch)
|
||||
pacman -Qq "${pkgs[@]}" >/dev/null 2>&1
|
||||
return $?
|
||||
;;
|
||||
debian)
|
||||
dpkg -s "${pkgs[@]}" >/dev/null 2>&1
|
||||
return $?
|
||||
;;
|
||||
fedora)
|
||||
rpm -q "${pkgs[@]}" >/dev/null 2>&1
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Remove packages depending on detected distro
|
||||
pkg_remove() {
|
||||
local distro
|
||||
distro=$(detect_distro)
|
||||
|
||||
if [ "$distro" = "unknown" ]; then
|
||||
log_error "Unsupported distribution. Cannot remove packages automatically."
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS=' ' read -ra pkgs <<< "$(_resolve_pkg_names "$distro" "$@")"
|
||||
|
||||
if [ ${#pkgs[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
to_remove+=("$pkg")
|
||||
done
|
||||
|
||||
if [ ${#to_remove[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Removing packages via $distro package manager: ${to_remove[*]}"
|
||||
case "$distro" in
|
||||
arch)
|
||||
local pac_remove=()
|
||||
for pkg in "${to_remove[@]}"; do
|
||||
if pacman -Qq "$pkg" >/dev/null 2>&1; then
|
||||
pac_remove+=("$pkg")
|
||||
fi
|
||||
done
|
||||
if [ ${#pac_remove[@]} -gt 0 ]; then
|
||||
sudo pacman -R --noconfirm "${pac_remove[@]}"
|
||||
fi
|
||||
;;
|
||||
debian)
|
||||
sudo apt remove -y "${to_remove[@]}"
|
||||
;;
|
||||
fedora)
|
||||
sudo dnf remove -y "${to_remove[@]}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Export functions and variables for subshells
|
||||
export _LIB_PLATFORM_SOURCED=1
|
||||
export -f detect_distro detect_arch _resolve_pkg_names pkg_install pkg_check pkg_remove
|
||||
|
||||
|
||||
217
lib/plugins.sh
Normal file
217
lib/plugins.sh
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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() {
|
||||
# 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
|
||||
ensure_sources_file() {
|
||||
local sources_file="$BOOTSTRAP_DIR/plugin_sources.txt"
|
||||
if [ ! -f "$sources_file" ]; then
|
||||
mkdir -p "$BOOTSTRAP_DIR"
|
||||
echo "# Add raw URLs to JSON plugin manifests here, one per line." > "$sources_file"
|
||||
echo "# Official Bootstrap plugin repository" >> "$sources_file"
|
||||
echo "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins.json" >> "$sources_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetches manifests from sources and generates the cache
|
||||
update_plugin_cache() {
|
||||
ensure_sources_file
|
||||
local cache_file="$BOOTSTRAP_DIR/lib/plugin_cache.sh"
|
||||
local sources_file="$BOOTSTRAP_DIR/plugin_sources.txt"
|
||||
|
||||
mkdir -p "$BOOTSTRAP_DIR/lib"
|
||||
|
||||
# Initialize cache file
|
||||
cat << 'EOF' > "$cache_file"
|
||||
# Auto-generated plugin cache. Do not edit manually.
|
||||
declare -g -A PLUGIN_URLS
|
||||
declare -g -A PLUGIN_VERSIONS
|
||||
declare -g -A PLUGIN_BOOTSTRAP_VERSIONS
|
||||
EOF
|
||||
|
||||
if [ -f "$sources_file" ]; then
|
||||
local dl_args=()
|
||||
local temp_manifests=()
|
||||
|
||||
while IFS= read -r url || [ -n "$url" ]; do
|
||||
# Skip empty lines and comments
|
||||
[[ -z "$url" || "$url" == \#* ]] && continue
|
||||
|
||||
local temp_file
|
||||
temp_file=$(mktemp --suffix=".json" 2>/dev/null || mktemp)
|
||||
dl_args+=("$url" "$temp_file")
|
||||
temp_manifests+=("$temp_file")
|
||||
done < "$sources_file"
|
||||
|
||||
if [ ${#dl_args[@]} -gt 0 ]; then
|
||||
log_info "Fetching ${#temp_manifests[@]} plugin manifests in parallel..."
|
||||
download_multiple_files_parallel "${dl_args[@]}"
|
||||
|
||||
for temp_file in "${temp_manifests[@]}"; do
|
||||
if [ -s "$temp_file" ]; then
|
||||
cat "$temp_file" | parse_plugin_manifest >> "$cache_file"
|
||||
fi
|
||||
rm -f "$temp_file"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clear downloaded scripts to force lazy re-download of the updated versions
|
||||
rm -rf "$BOOTSTRAP_DIR/plugins" 2>/dev/null || true
|
||||
|
||||
log_success "Plugin cache updated successfully."
|
||||
}
|
||||
|
||||
manage_plugin_sources() {
|
||||
ensure_sources_file
|
||||
local sources_file="$BOOTSTRAP_DIR/plugin_sources.txt"
|
||||
|
||||
local editor="${EDITOR:-}"
|
||||
if [ -z "$editor" ]; then
|
||||
if has_command nvim; then editor="nvim"
|
||||
elif has_command vim; then editor="vim"
|
||||
elif has_command nano; then editor="nano"
|
||||
else editor="vi"
|
||||
fi
|
||||
fi
|
||||
|
||||
$editor "$sources_file"
|
||||
|
||||
# Update cache after editing
|
||||
update_plugin_cache
|
||||
}
|
||||
|
||||
handle_plugin() {
|
||||
local subcmd="${1:-}"
|
||||
case "$subcmd" in
|
||||
sources)
|
||||
manage_plugin_sources
|
||||
;;
|
||||
update)
|
||||
update_plugin_cache
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown plugin command: $subcmd"
|
||||
log_info "Available commands: b plugin sources, b plugin update"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_plugin() {
|
||||
local plugin_name="$1"
|
||||
shift
|
||||
|
||||
local is_ephemeral=false
|
||||
local cmd_args=()
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "-e" ] || [ "$arg" = "--ephemeral" ]; then
|
||||
is_ephemeral=true
|
||||
else
|
||||
cmd_args+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
local url="${PLUGIN_URLS[$plugin_name]:-}"
|
||||
if [ -z "$url" ]; then
|
||||
log_error "Plugin '$plugin_name' not found in cache."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check compatibility version
|
||||
local compat_ver="${PLUGIN_BOOTSTRAP_VERSIONS[$plugin_name]:-}"
|
||||
if [ -n "$compat_ver" ]; then
|
||||
local current_ver="0.0.0"
|
||||
if [ -f "$BOOTSTRAP_DIR/VERSION" ]; then
|
||||
current_ver=$(cat "$BOOTSTRAP_DIR/VERSION" | tr -d '[:space:]')
|
||||
fi
|
||||
if version_lt "$compat_ver" "$current_ver"; then
|
||||
log_warn "Plugin '$plugin_name' is only tested up to bootstrap version $compat_ver (current: $current_ver). It may be incompatible."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$is_ephemeral" = "true" ]; then
|
||||
log_info "Downloading and running plugin '$plugin_name' (ephemeral)..."
|
||||
local script_content
|
||||
if ! script_content=$(curl -fsSL "$url"); then
|
||||
log_error "Failed to download plugin '$plugin_name' from $url"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Execute the plugin directly in memory in a subshell
|
||||
(
|
||||
export BOOTSTRAP_DIR
|
||||
# We use bash -c and pass the script content to keep stdin free for interactive plugins
|
||||
# The "$0" arg for bash -c is set to the plugin name
|
||||
bash -c "$script_content" "$plugin_name" "${cmd_args[@]}"
|
||||
)
|
||||
return $?
|
||||
else
|
||||
local plugin_dir="$BOOTSTRAP_DIR/plugins"
|
||||
local local_plugin="$plugin_dir/${plugin_name}.sh"
|
||||
|
||||
if [ ! -f "$local_plugin" ]; then
|
||||
log_info "Downloading plugin '$plugin_name'..."
|
||||
mkdir -p "$plugin_dir"
|
||||
if ! curl -fsSL "$url" -o "$local_plugin"; then
|
||||
log_error "Failed to download plugin '$plugin_name' from $url"
|
||||
rm -f "$local_plugin"
|
||||
return 1
|
||||
fi
|
||||
chmod +x "$local_plugin"
|
||||
fi
|
||||
|
||||
log_info "Running plugin '$plugin_name'..."
|
||||
# Execute the plugin in a subshell, passing any additional arguments
|
||||
(
|
||||
export BOOTSTRAP_DIR
|
||||
bash "$local_plugin" "${cmd_args[@]}"
|
||||
)
|
||||
return $?
|
||||
fi
|
||||
}
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
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.11.7 and configuration"
|
||||
[nvim]="Neovim 0.12.0 and configuration"
|
||||
[pnpm]="pnpm package manager"
|
||||
[rust]="Rustup and Rust compiler/toolchain"
|
||||
[starship]="Starship shell prompt"
|
||||
@@ -16,7 +20,11 @@ declare -A INSTALLERS=(
|
||||
|
||||
declare -A INSTALLER_DISPLAYS=(
|
||||
[agy]="Antigravity"
|
||||
[asciicinema]="asciicinema"
|
||||
[bat]="Bat"
|
||||
[docker]="Docker"
|
||||
[hyperfine]="Hyperfine"
|
||||
[lazygit]="lazygit"
|
||||
[node]="Node"
|
||||
[nvim]="Neovim"
|
||||
[pnpm]="Pnpm"
|
||||
@@ -28,4 +36,4 @@ declare -A INSTALLER_DISPLAYS=(
|
||||
[zoxide]="Zoxide"
|
||||
)
|
||||
|
||||
INSTALLER_KEYS=(agy bat 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)
|
||||
|
||||
138
lib/rollback.sh
Normal file
138
lib/rollback.sh
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -n "${_LIB_ROLLBACK_SOURCED:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
_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"
|
||||
}
|
||||
|
||||
setup_uninstaller_context() {
|
||||
local tool="$1"
|
||||
export BOOTSTRAP_CURRENT_TOOL="$tool"
|
||||
export BOOTSTRAP_UNINSTALLER_CMDS="$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds"
|
||||
|
||||
# If a manifest already exists and the tool is NOT marked as successfully installed
|
||||
# in history.log, we treat this as a resumed run. We preserve the manifest so
|
||||
# that new commands are prepended to the existing ones.
|
||||
if [ -f "$BOOTSTRAP_UNINSTALLER_CMDS" ] && ! grep -q "^INSTALL: $tool$" "$BOOTSTRAP_HISTORY_LOG" 2>/dev/null; then
|
||||
log_info "Resuming installation of '$tool'. Preserving existing rollback manifest."
|
||||
else
|
||||
# Fresh installation or reinstall, start with a clean slate
|
||||
rm -f "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
fi
|
||||
touch "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
}
|
||||
|
||||
add_rollback_cmd() {
|
||||
local cmd="$1"
|
||||
if [ -n "${BOOTSTRAP_UNINSTALLER_CMDS:-}" ] && [ -f "$BOOTSTRAP_UNINSTALLER_CMDS" ]; then
|
||||
# Prepend to the top of the file
|
||||
sed -i "1i $cmd" "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
fi
|
||||
}
|
||||
|
||||
track_file() {
|
||||
add_rollback_cmd "sudo rm -f '$1'"
|
||||
}
|
||||
|
||||
track_dir() {
|
||||
add_rollback_cmd "sudo rm -rf '$1'"
|
||||
}
|
||||
|
||||
create_savepoint() {
|
||||
local name="$1"
|
||||
echo "SAVEPOINT: $name" >> "$BOOTSTRAP_HISTORY_LOG"
|
||||
log_success "Savepoint '$name' created."
|
||||
}
|
||||
|
||||
mark_install_success() {
|
||||
local tool="$1"
|
||||
# Only record if we actually have an uninstaller
|
||||
if [ -f "$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds" ]; then
|
||||
echo "INSTALL: $tool" >> "$BOOTSTRAP_HISTORY_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
execute_rollback() {
|
||||
local tool="$1"
|
||||
local manifest="$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds"
|
||||
|
||||
if [ ! -f "$manifest" ]; then
|
||||
log_warn "No rollback manifest found for '$tool'."
|
||||
return 0
|
||||
fi
|
||||
|
||||
export BOOTSTRAP_CURRENT_TOOL="$tool"
|
||||
log_info "Rolling back '$tool'..."
|
||||
while IFS= read -r cmd; do
|
||||
[ -z "$cmd" ] && continue
|
||||
log_info "Executing: $cmd"
|
||||
eval "$cmd" || log_warn "Failed to execute: $cmd"
|
||||
done < "$manifest"
|
||||
|
||||
rm -f "$manifest"
|
||||
log_success "Rollback of '$tool' complete."
|
||||
}
|
||||
|
||||
rollback_bare() {
|
||||
if [ ! -s "$BOOTSTRAP_HISTORY_LOG" ]; then
|
||||
log_info "No history available to rollback."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local last_line
|
||||
last_line=$(tail -n 1 "$BOOTSTRAP_HISTORY_LOG")
|
||||
|
||||
if [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
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."
|
||||
fi
|
||||
}
|
||||
|
||||
rollback_to_savepoint() {
|
||||
local target_sp="$1"
|
||||
|
||||
if ! grep -q "SAVEPOINT: $target_sp" "$BOOTSTRAP_HISTORY_LOG"; then
|
||||
log_error "Savepoint '$target_sp' not found in history."
|
||||
return 1
|
||||
fi
|
||||
|
||||
while [ -s "$BOOTSTRAP_HISTORY_LOG" ]; do
|
||||
local last_line
|
||||
last_line=$(tail -n 1 "$BOOTSTRAP_HISTORY_LOG")
|
||||
|
||||
if [[ "$last_line" == SAVEPOINT:\ $target_sp ]]; then
|
||||
log_success "Reached savepoint '$target_sp'."
|
||||
# Optionally remove the savepoint itself or keep it? Let's keep it.
|
||||
break
|
||||
elif [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
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'..."
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
else
|
||||
# Unknown line format, just remove it
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
fi
|
||||
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
|
||||
148
lib/routes.sh
148
lib/routes.sh
@@ -2,42 +2,49 @@
|
||||
# Central routing script for bootstrap installers.
|
||||
# This file is updated automatically by the 'b' command.
|
||||
|
||||
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}"
|
||||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
|
||||
BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$(dirname "$_LIB_DIR")}"
|
||||
|
||||
# Source common library
|
||||
# Fallback to ~/.config/bootstrap if not found locally
|
||||
if [ ! -d "$BOOTSTRAP_DIR/lib" ]; then
|
||||
BOOTSTRAP_DIR="$HOME/.config/bootstrap"
|
||||
fi
|
||||
export BOOTSTRAP_DIR
|
||||
|
||||
# Source libraries
|
||||
if [ -f "$BOOTSTRAP_DIR/lib/common.sh" ]; then
|
||||
. "$BOOTSTRAP_DIR/lib/common.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/rollback.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/platform.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/shell_config.sh"
|
||||
init_rollback_system
|
||||
else
|
||||
# Fallback/Bootstrap case if lib is not installed yet
|
||||
echo "Error: Bootstrap libraries not found at $BOOTSTRAP_DIR/lib/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_bash
|
||||
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)"
|
||||
|
||||
# Source registry
|
||||
if [ -f "$_SCRIPT_DIR/registry.sh" ]; then
|
||||
. "$_SCRIPT_DIR/registry.sh"
|
||||
elif [ -f "$BOOTSTRAP_DIR/lib/registry.sh" ]; then
|
||||
if [ -f "$BOOTSTRAP_DIR/lib/registry.sh" ]; then
|
||||
. "$BOOTSTRAP_DIR/lib/registry.sh"
|
||||
else
|
||||
# Standalone/remote fallback: download registry
|
||||
_tmp_registry=$(mktemp)
|
||||
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}"
|
||||
curl -fsSL "${BOOTSTRAP_BASE_URL}/lib/registry.sh" -o "$_tmp_registry" 2>/dev/null || \
|
||||
curl -fsSL "${BOOTSTRAP_FALLBACK_URL}/lib/registry.sh" -o "$_tmp_registry" 2>/dev/null
|
||||
if [ -s "$_tmp_registry" ]; then
|
||||
. "$_tmp_registry"
|
||||
else
|
||||
# Critical fallback
|
||||
declare -A INSTALLERS
|
||||
declare -A INSTALLER_DISPLAYS
|
||||
INSTALLER_KEYS=()
|
||||
# Critical fallback
|
||||
declare -A INSTALLERS
|
||||
declare -A INSTALLER_DISPLAYS
|
||||
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
|
||||
rm -f "$_tmp_registry"
|
||||
fi
|
||||
|
||||
# Helper function to run/edit installer scripts
|
||||
@@ -64,6 +71,13 @@ run_ware() {
|
||||
|
||||
# Check for local installer first
|
||||
local local_installer="$BOOTSTRAP_DIR/installers/install_${tool}.sh"
|
||||
|
||||
if [ "$bypass_edit" = "true" ] && [ -f "$local_installer" ]; then
|
||||
log_info "Running ${display_name} installer..."
|
||||
bash "$local_installer" "${cmd_args[@]}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
local temp_script
|
||||
temp_script=$(mktemp --suffix=".sh" 2>/dev/null || mktemp)
|
||||
|
||||
@@ -111,9 +125,58 @@ run_ware() {
|
||||
|
||||
# Run the script (edited or unchanged)
|
||||
log_info "Running ${display_name} installer..."
|
||||
setup_uninstaller_context "$tool"
|
||||
|
||||
# Set trap for signals to intercept interruption and allow user to choose rollback/keep
|
||||
local interrupted=false
|
||||
trap 'interrupted=true' INT TERM
|
||||
|
||||
bash "$temp_script" "${cmd_args[@]}"
|
||||
local run_status=$?
|
||||
|
||||
# Restore default traps
|
||||
trap - INT TERM
|
||||
|
||||
if [ "$run_status" -eq 0 ] && [ "$interrupted" = "false" ]; then
|
||||
mark_install_success "$tool"
|
||||
source_bashrc
|
||||
else
|
||||
echo
|
||||
if [ "$interrupted" = "true" ]; then
|
||||
log_error "Installation of ${display_name} was interrupted."
|
||||
run_status=130
|
||||
else
|
||||
log_error "Installation of ${display_name} failed with exit code $run_status."
|
||||
fi
|
||||
|
||||
local choice=""
|
||||
if [ -t 0 ]; then
|
||||
while true; do
|
||||
read -r -p "Would you like to [r]ollback partial changes, or [k]eep them to resume/debug later? (r/k): " choice </dev/tty || choice="r"
|
||||
case "$choice" in
|
||||
[Rr]*)
|
||||
execute_rollback "$tool"
|
||||
run_status=1
|
||||
break
|
||||
;;
|
||||
[Kk]*)
|
||||
log_info "Keeping partial changes. Run 'b ${tool}' again to resume."
|
||||
run_status=1
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Please enter 'r' or 'k'."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
# Non-interactive environment, default to safe rollback
|
||||
log_warn "Non-interactive shell detected. Defaulting to automatic rollback to keep system clean."
|
||||
execute_rollback "$tool"
|
||||
run_status=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$temp_script"
|
||||
return "$run_status"
|
||||
@@ -140,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"
|
||||
@@ -148,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" "$@"
|
||||
@@ -193,10 +269,32 @@ for script in "${SCRIPTS[@]}"; do
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
fall)
|
||||
local savepoint_name="${1:-}"
|
||||
if [ -z "$savepoint_name" ]; then
|
||||
log_error "Usage: b fall <savepoint_name>"
|
||||
exit 1
|
||||
fi
|
||||
create_savepoint "$savepoint_name"
|
||||
exit 0
|
||||
;;
|
||||
rb)
|
||||
local target="${1:-}"
|
||||
if [ -z "$target" ]; then
|
||||
rollback_bare
|
||||
else
|
||||
rollback_to_savepoint "$target"
|
||||
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
|
||||
|
||||
@@ -102,6 +102,90 @@ add_env_if_missing() {
|
||||
return 1 # Not added
|
||||
}
|
||||
|
||||
# Write environment snippet to env.d/
|
||||
# Usage: write_env_snippet <name> <content>
|
||||
write_env_snippet() {
|
||||
local name="$1"
|
||||
local content="$2"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/env.d"
|
||||
|
||||
mkdir -p "$dir"
|
||||
log_info "Writing environment 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
|
||||
}
|
||||
|
||||
# Write alias snippet to aliases.d/
|
||||
# Usage: write_alias_snippet <name> <content>
|
||||
write_alias_snippet() {
|
||||
local name="$1"
|
||||
local content="$2"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/aliases.d"
|
||||
|
||||
mkdir -p "$dir"
|
||||
log_info "Writing alias 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 environment snippet from env.d/
|
||||
# Usage: remove_env_snippet <name>
|
||||
remove_env_snippet() {
|
||||
local name="$1"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/env.d"
|
||||
|
||||
if [ -f "$dir/${name}.sh" ]; then
|
||||
log_info "Removing environment snippet '$name'"
|
||||
rm -f "$dir/${name}.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove alias snippet from aliases.d/
|
||||
# Usage: remove_alias_snippet <name>
|
||||
remove_alias_snippet() {
|
||||
local name="$1"
|
||||
local dir="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/aliases.d"
|
||||
|
||||
if [ -f "$dir/${name}.sh" ]; then
|
||||
log_info "Removing alias snippet '$name'"
|
||||
rm -f "$dir/${name}.sh"
|
||||
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
|
||||
@@ -109,3 +193,15 @@ create_fd_symlink() {
|
||||
sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd
|
||||
fi
|
||||
}
|
||||
|
||||
# Source the bashrc file to reload configurations
|
||||
source_bashrc() {
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
log_info "Re-sourcing ~/.bashrc..."
|
||||
. "$HOME/.bashrc"
|
||||
fi
|
||||
}
|
||||
|
||||
# Export functions and variables for subshells
|
||||
export _LIB_SHELL_CONFIG_SOURCED=1
|
||||
export -f get_shell_configs remove_block inject_block add_alias_if_missing add_env_if_missing create_fd_symlink write_env_snippet write_alias_snippet remove_env_snippet remove_alias_snippet 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)
|
||||
> "$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 "$@"
|
||||
75
readme.md
75
readme.md
@@ -13,6 +13,7 @@ Here is a comparison of the size and complexity of using Bootstrap (`to b`) vers
|
||||
| application | to b | not to b |
|
||||
| :--- | :--- | :--- |
|
||||
| **Antigravity CLI (`agy`)** | 197 lines | 239 lines (Official Antigravity install script) |
|
||||
| **asciicinema (`asciicinema`)** | 99 lines | N/A (Official binary distribution) |
|
||||
| **Bat (`bat`)** | 155 lines | N/A (Standard package install) |
|
||||
| **Node.js & NVM (`node`)** | 156 lines | 507 lines (Official NVM install script) |
|
||||
| **Neovim (`nvim`)** | 178 lines | N/A (Official binary/config distribution) |
|
||||
@@ -76,6 +77,28 @@ b con i3
|
||||
|
||||
It automatically fuzzy-finds the folder in case there is no exact match. Also, in case there is only a singular config file in that folder, then it will directly open that file.
|
||||
|
||||
### 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.
|
||||
|
||||
To safely uninstall the very last tool you installed (including wiping its shell paths and aliases):
|
||||
|
||||
```bash
|
||||
b rb
|
||||
```
|
||||
|
||||
To create a named savepoint before experimenting with your setup:
|
||||
|
||||
```bash
|
||||
b fall pre_dev_setup
|
||||
```
|
||||
|
||||
To completely roll back all installations made after that savepoint, restoring your system back to that exact state:
|
||||
|
||||
```bash
|
||||
b rb pre_dev_setup
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
To check for updates and update the tool manually:
|
||||
@@ -86,6 +109,58 @@ b up
|
||||
b up --force
|
||||
```
|
||||
|
||||
## Plugins (`b <plugin_name>`)
|
||||
|
||||
Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike installers (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a subshell**.
|
||||
|
||||
Downloading and invoking a plugin makes no system modifications other than caching the `.sh` file itself. They are fetched only the very first time you invoke them.
|
||||
|
||||
### Official Plugins
|
||||
|
||||
Bootstrap comes pre-configured with a set of official plugins ready to use out-of-the-box:
|
||||
|
||||
* **`weather`**: Fetches and displays a neat weather forecast using `wttr.in`.
|
||||
```bash
|
||||
b weather
|
||||
b weather New York
|
||||
```
|
||||
* **`sysinfo`**: Displays a system resource dashboard showing CPU, memory, disk usage, and OS/kernel details.
|
||||
```bash
|
||||
b sysinfo
|
||||
```
|
||||
* **`todo`**: A command-line todo list manager. Tasks are persisted to a lightweight text file in your user directory.
|
||||
```bash
|
||||
b todo add "Write a new plugin"
|
||||
b todo list
|
||||
b todo done 1
|
||||
```
|
||||
|
||||
### Adding Third-Party Plugins
|
||||
|
||||
To manage plugin repositories, run:
|
||||
|
||||
```bash
|
||||
b plugin sources
|
||||
```
|
||||
|
||||
This opens a configuration file in your `$EDITOR`. You can add raw URLs pointing to JSON plugin manifests from any repository. Once you close the editor, `bootstrap` automatically parses those manifests using its native JSON parser and generates a fast, zero-latency lookup cache.
|
||||
|
||||
You can then execute any plugin simply by calling its name:
|
||||
|
||||
```bash
|
||||
b my_plugin
|
||||
```
|
||||
|
||||
Plugins are automatically checked for updates and lazily re-downloaded whenever you run `b up`.
|
||||
|
||||
If you prefer to run a plugin strictly in **ephemeral mode** (meaning it will bypass the cache and execute directly in memory to guarantee the absolute latest version without leaving any footprint), simply pass the `-e` or `--ephemeral` flag:
|
||||
|
||||
```bash
|
||||
b my_plugin -e
|
||||
```
|
||||
|
||||
For documentation on how to develop and publish your own plugins, please see the [Plugin Development Guide](docs/plugin_development.md).
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To uninstall the bootstrap helper tool but leave a lightweight `b back` function to easily reinstall it later:
|
||||
|
||||
@@ -9,41 +9,3 @@ if [ -f "./scripts/generate_registry.sh" ]; then
|
||||
./scripts/generate_registry.sh
|
||||
git add lib/registry.sh
|
||||
fi
|
||||
|
||||
VERSION_FILE="VERSION"
|
||||
|
||||
if [ ! -f "$VERSION_FILE" ]; then
|
||||
echo "1.0.0" > "$VERSION_FILE"
|
||||
git add "$VERSION_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if there are staged changes other than VERSION, documentation (*.md), or installers folder
|
||||
if ! git diff --cached --name-only | grep -Ev "^($VERSION_FILE$|.*\.md$|^installers/)" | grep -q .; then
|
||||
# No other files staged, skip version bump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Read current version
|
||||
current_version=$(cat "$VERSION_FILE" | tr -d '[:space:]')
|
||||
|
||||
# Basic regex validation for semantic versioning (X.Y.Z)
|
||||
if [[ ! "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Current version '$current_version' in $VERSION_FILE is not in X.Y.Z format." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse version components
|
||||
IFS='.' read -r major minor patch <<< "$current_version"
|
||||
|
||||
# Increment patch version
|
||||
patch=$((patch + 1))
|
||||
new_version="$major.$minor.$patch"
|
||||
|
||||
# Write to VERSION file
|
||||
echo "$new_version" > "$VERSION_FILE"
|
||||
|
||||
# Add VERSION file to the commit
|
||||
git add "$VERSION_FILE"
|
||||
|
||||
echo "[pre-commit] Automatically bumped version from $current_version to $new_version"
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user