Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
780e793
fix #9: Add validation check for pkg_remove
sortedcord Jun 25, 2026
7f3ff45
refactor: Use jq instead of custom posix complient json.sh
sortedcord Jun 26, 2026
c42687a
feat: Added registry helpers for installers
sortedcord Jun 26, 2026
f5a266f
feat: Implement the github release helper with `github.sh`
sortedcord Jun 26, 2026
4eec275
refactor: Installers use github_get_latest_release helpers
sortedcord Jun 26, 2026
0eaea2c
refactor: Unify fragmented install strategies within installers
sortedcord Jun 26, 2026
36c7be0
refactor: Migrate all installers to use installation strategies
sortedcord Jun 26, 2026
a4e5bc1
refactor: Remove legacy backwards compat code
sortedcord Jun 26, 2026
29de051
refactor: Remove standalone exec prevention code blocks from installers
sortedcord Jun 26, 2026
4c1c7de
refactor: Remove redundant curl availability checks
sortedcord Jun 26, 2026
d5c90d6
refactor: Use XDG compliant isolated directory structure
sortedcord Jun 26, 2026
0c16640
refactor: Simplified `pkg_install` and `pkg_remove`
sortedcord Jun 26, 2026
cee345e
feat: Unified Uninstallation and Rollback
sortedcord Jun 26, 2026
83c5244
fix #19: Prevent Checkpoint from having same bug as
sortedcord Jun 26, 2026
2ddd28d
feat: Added Multi-tool uninstallation via comma-separated arguments
sortedcord Jun 28, 2026
03e9c20
refactor: Linting fixes
sortedcord Jun 28, 2026
f9a25ff
refactor(Installers): Update all installers to use library helpers an…
sortedcord Jun 28, 2026
6e0f951
merge: Master into fix/reference-counting
sortedcord Jun 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 62 additions & 52 deletions .agents/skills/add_installer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ bootstrap/
│ ├── common.sh # Logging, confirm(), has_command(), make_temp_dir()
│ ├── platform.sh # detect_distro(), detect_arch(), pkg_install(), pkg_check(), pkg_remove()
│ ├── rollback.sh # Rollback tracking (track_file, track_dir, add_rollback_cmd)
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet
│ ├── shell_config.sh # write_env_snippet, write_alias_snippet, write_completion_snippet
│ ├── registry.sh # Dynamically generated installer registry
│ └── routes.sh # Central router script
├── commands/ # Non-installer commands (help, con, uninstall)
Expand All @@ -35,36 +35,48 @@ 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 an official install or curl script in the prompt, or a link to one:
- Execute the `curl` command (or use `read_url_content` or `read_browser_page`) to fetch the script and analyze what it actually does under the hood.
- Do NOT simply execute the official script blindly in your installer.
- Re-write its functionality according to the conventions of the bootstrap installer.
- Strip away redundant code, OS checks for macOS/Windows (we only target Linux), unnecessary shell configuration logic, and self-update logic.
- Implement only the core, essential Linux installation logic inside the `install_<name>` function.
- Do NOT add checks for `curl` or attempt to install it. Assume `curl` is installed and available.

### 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).

At the top of your new installer script, right below `#!/usr/bin/env bash`, add the following three metadata headers:
### 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 metadata headers:
```bash
# Tool: <name>
# DisplayName: <displayName>
# Description: <description>
# Strategy: <strategy> (binary | managed | system)
```

The central router `lib/routes.sh` and autocomplete function in `b.sh` will dynamically parse this metadata from all `install_*.sh` scripts to register the installer and keys automatically! No manual edits to `lib/routes.sh` or `b.sh` are required.

### Step 3: Implement Rollback Tracking (Crucial)
### Step 4: Implement Rollback Tracking (Crucial)

To ensure the user can seamlessly use `b rb <name>`, all manual modifications must be tracked:
- When extracting binaries to `~/.local/bin/`, use `track_file "$HOME/.local/bin/binary"`.
- When creating directories like `~/.config/tool/`, use `track_dir "$HOME/.config/tool"`.
- When running manual apt/dnf/npm commands, log their inverses: `add_rollback_cmd "sudo npm uninstall -g package"`.
Note: `pkg_install`, `write_env_snippet`, and `write_alias_snippet` will automatically track themselves.
To ensure the user can seamlessly use `b rb <name>` to uninstall or rollback a tool, all manual modifications must be tracked:
- When extracting binaries, copy them to `$BOOTSTRAP_BIN` and use `track_file "$BOOTSTRAP_BIN/binary"`. Never use `sudo` or write to system folders like `/usr/local/bin`.
- When creating directories (e.g., in `$BOOTSTRAP_OPT` or `$BOOTSTRAP_RUNTIMES`), use `track_dir` to register them.
- When running manual commands (like compiling or registry changes), log their inverses: `add_rollback_cmd "rm -rf ..."` or equivalent cleanup.
- When installing packages via `pkg_install`, always register them with `registry_add_sys_deps` for reference-counted rollback tracking.
- Note: `write_env_snippet`, `write_alias_snippet`, and `write_completion_snippet` will automatically track themselves.

### Step 4: Verify (optional)
### Step 5: Verify (optional)

Verify that the installer works and appears in the help output:
- Run `b all` to confirm it appears in the help list.
Expand All @@ -75,22 +87,15 @@ Verify that the installer works and appears in the help output:

## Installer Script Template

Every installer follows this exact boilerplate structure. Copy this and fill in the tool-specific logic:
Every installer follows this exact structure. Copy this and fill in the tool-specific logic (notice there are no standalone execution guards or curl checks):

```bash
#!/usr/bin/env bash
# Tool: <name>
# DisplayName: <ToolName>
# Description: Short description of what it installs
# Strategy: <strategy> (binary | managed | system)
#
# <ToolName> Installer Script
#

# Prevent standalone execution
if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then
echo "Error: This script must be run through the 'b' CLI." >&2
exit 1
fi

set -euo pipefail

Expand All @@ -110,14 +115,14 @@ install_<name>() {
fi

# --- Tool-specific installation logic goes here ---
# Use pkg_install for distro packages (it automatically handles rollback hooks!):
# Use pkg_install for distro packages:
# pkg_install "arch:<pkg_a>|debian:<pkg_d>|fedora:<pkg_f>"

# Or manual downloads (always use download_file for resumability!):
# local url="https://..."
# download_file "$url" "$TMP_DIR/binary"
# cp "$TMP_DIR/binary" "$HOME/.local/bin/binary"
# track_file "$HOME/.local/bin/binary" # Important for rollback!
# cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary"
# track_file "$BOOTSTRAP_BIN/binary" # Important for rollback!
}

# ─── Shell Configuration (if needed) ─────────────────────────────────
Expand All @@ -126,6 +131,7 @@ configure_shell() {
# Use drop-in snippets for shell configuration (they auto-rollback)
# write_env_snippet "<name>" "export VAR_NAME=value\neval \"\$(<name> init bash)\""
# write_alias_snippet "<name>" "alias <name>='<command>'"
# write_completion_snippet "<name>" "source <(<command> completion bash)"
:
}

Expand Down Expand Up @@ -159,15 +165,15 @@ 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/`. |
| `download_file <url> <dest>` | Resumable and cached download of `<url>` to `<dest>`. Uses `$BOOTSTRAP_CACHE_DIR/downloads/`. |

### From `lib/platform.sh`

| Function | Description |
|---|---|
| `detect_distro` | Echoes `arch`, `debian`, `fedora`, or `unknown` |
| `detect_arch` | Echoes `x86_64` or `arm64` |
| `pkg_install <pkg>...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. Automatically integrates with rollback context and handles package reference counting. |
| `pkg_install <pkg>...` | Install packages. Supports distro mapping: `"arch:pkg_a\|debian:pkg_d\|fedora:pkg_f"`. |
| `pkg_check <pkg>...` | Returns 0 if packages are installed. Supports identical mapping syntax. |

### From `lib/rollback.sh`
Expand All @@ -176,14 +182,23 @@ These are pre-loaded by `bootstrap.sh` — no need to source them manually in in
|---|---|
| `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>"`). |
| `add_rollback_cmd <cmd>` | Adds a raw bash command to the uninstall manifest (e.g., `add_rollback_cmd "rm -rf <path>"`). |

### From `lib/shell_config.sh`

| Function | Description |
|---|---|
| `write_env_snippet <name> <content>` | Creates an isolated `env.d/` shell drop-in snippet and registers it for rollback. |
| `write_alias_snippet <name> <content>` | Creates an isolated `aliases.d/` shell drop-in snippet and registers it for rollback. |
| `write_completion_snippet <name> <content>` | Creates an isolated `completions.d/` bash completion snippet and registers it for rollback. |

### From `lib/github.sh`

| Function | Description |
|---|---|
| `github_get_latest_release <repo>` | Fetches the latest release tag (e.g., `v1.0.0`) from the GitHub API. |
| `github_get_download_url <repo> <tag> <regex>` | Finds an asset matching a regex in the specified release and prints its URL. |
| `github_download_asset <repo> <tag> <regex> <dest>` | Resolves the URL for the matching asset and downloads it directly to `<dest>`. |

---

Expand All @@ -200,38 +215,33 @@ trap cleanup EXIT
### Distro-specific mapping

```bash
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "curl" "git"
pkg_install "arch:neovim|debian:nvim|fedora:neovim" "git"
```

### Fetching latest GitHub release tag

```bash
local latest_tag=""
if has_command curl; then
latest_tag=$(curl -sL https://api.github.com/repos/<owner>/<repo>/releases/latest \
| grep '"tag_name":' | head -n1 \
| sed -E 's/.*"tag_name": "([^"]+)".*/\1/' || true)
fi
latest_tag=$(github_get_latest_release "owner/repo")

if [ -z "$latest_tag" ]; then
latest_tag="v1.0.0" # fallback
log_warn "Failed to fetch latest version. Falling back to: $latest_tag"
fi
```

### Resumable Download and Extraction
### GitHub Download and Extraction

```bash
local url="https://github.com/owner/repo/releases/download/${version}/archive.tar.gz"
local dest="$TMP_DIR/archive.tar.gz"
local archive="$TMP_DIR/archive.tar.gz"

# Resumable, cached download
download_file "$url" "$dest"
# Download the asset matching a regex pattern for the specific release tag
github_download_asset "owner/repo" "$latest_tag" "app-.*-linux-x86_64\.tar\.gz" "$archive"

# Extract and install
tar -xzf "$dest" -C "$TMP_DIR"
sudo cp "$TMP_DIR/binary" /usr/local/bin/binary
track_file "/usr/local/bin/binary"
tar -xzf "$archive" -C "$TMP_DIR"
cp "$TMP_DIR/binary" "$BOOTSTRAP_BIN/binary"
track_file "$BOOTSTRAP_BIN/binary"
```

---
Expand All @@ -240,10 +250,10 @@ track_file "/usr/local/bin/binary"

1. **File naming**: Always `install_<name>.sh` in the `installers/` directory.
2. **Confirmation prompts**: Always ask before installing. Check if already installed first.
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `~/.local/bin/`, you MUST call `track_file`. If you run `makepkg`, you MUST call `add_rollback_cmd` for `pacman -R`.
4. **Shell Drop-ins**: Always use `write_env_snippet` or `write_alias_snippet` instead of manually injecting code directly into `~/.bashrc`.
5. **No hardcoded paths**: Use `$HOME`, library functions, and `detect_*` helpers.
6. **Error handling**: Use `set -euo pipefail` after the guard block.
7. **CLI Enforcement Guard**: Always copy the standalone execution guard block verbatim to the top of your installer script to prevent direct execution.
8. **Clean Official Scripts**: When implementing official curl/install scripts provided in the prompt, strip them of bloat, macOS/Windows support, and redundant shell setups before writing the script.
3. **Rollback Tracking**: NEVER omit rollback hooks. If you move a file to `$BOOTSTRAP_BIN`, you MUST call `track_file`. If you build/install manually (e.g. via `makepkg -si`), you MUST call `add_rollback_cmd "sudo pacman -R --noconfirm <pkg>"` or equivalent.
4. **Shell Drop-ins**: Always use `write_env_snippet`, `write_alias_snippet`, or `write_completion_snippet` instead of manually injecting code directly into `~/.bashrc`.
5. **No hardcoded paths**: Use `$HOME`, `$BOOTSTRAP_BIN`, `$BOOTSTRAP_OPT`, `$BOOTSTRAP_RUNTIMES`, library functions, and `detect_*` helpers.
6. **Error handling**: Use `set -euo pipefail`.
7. **No Standalone Execution Guards**: Do NOT add guards to prevent running the installer directly; simplify code paths to focus on clean runs.
8. **Clean Official Scripts**: When implementing official curl/install scripts, strip them of bloat, macOS/Windows support, and redundant shell setups. Do not check or install `curl`.
9. **No manual shell re-sourcing**: Do NOT manually run `source ~/.bashrc` or print instructions asking the user to run it. Sourcing of the shell configuration is handled automatically by the central router and CLI at the end of the installation.
8 changes: 7 additions & 1 deletion b.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# shellcheck shell=bash
# >>> bootstrap-cli b function >>>
# Shortcut for downloading and running bootstrap/install scripts
b() {
Expand All @@ -19,6 +20,7 @@ b() {
_version_lt() {
[ "$1" = "$2" ] && return 1
local IFS=.
# shellcheck disable=SC2206
local i ver1=($1) ver2=($2)
for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done
for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done
Expand All @@ -41,7 +43,7 @@ b() {

local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master"
local local_ver="0.0.0"
[ -f "$routes_dir/VERSION" ] && local_ver=$(cat "$routes_dir/VERSION" 2>/dev/null | tr -d '[:space:]')
[ -f "$routes_dir/VERSION" ] && local_ver=$(tr -d '[:space:]' < "$routes_dir/VERSION" 2>/dev/null)

local remote_ver
if remote_ver=$(curl -fsSL "$base_url/VERSION" 2>/dev/null); then
Expand Down Expand Up @@ -71,6 +73,7 @@ b() {
# Sourced again in the parent shell after successfully running the command
if [ $ret -eq 0 ]; then
if [ -f "$HOME/.bashrc" ]; then
# shellcheck source=/dev/null
. "$HOME/.bashrc"
fi
fi
Expand Down Expand Up @@ -123,6 +126,7 @@ _b_completion() {
return 0
fi

# shellcheck disable=SC2207
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
return 0
fi
Expand Down Expand Up @@ -161,6 +165,7 @@ _b_completion() {
return 0
fi

# shellcheck disable=SC2207
COMPREPLY=( $(compgen -W "$installer_keys" -- "$cur") )
return 0
fi
Expand All @@ -170,6 +175,7 @@ _b_completion() {
# List of directories in ~/.config/ to choose from
local config_dirs
config_dirs=$(find "$HOME/.config" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null)
# shellcheck disable=SC2207
COMPREPLY=( $(compgen -W "$config_dirs" -- "$cur") )
return 0
fi
Expand Down
Loading