diff --git a/docs/plugin_development.md b/docs/plugin_development.md index 2499d6f..9e62d38 100644 --- a/docs/plugin_development.md +++ b/docs/plugin_development.md @@ -41,6 +41,7 @@ Example `plugins.json`: "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" } } @@ -49,6 +50,7 @@ Example `plugins.json`: * **`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 diff --git a/lib/json.sh b/lib/json.sh index 4370582..fa85bb8 100644 --- a/lib/json.sh +++ b/lib/json.sh @@ -11,7 +11,7 @@ # pardon my french parse_json() { # Tokenize the JSON using grep - grep -oE '"([^"\\]|\\.)*"|true|false|null|[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?|[{}[\]:,]' | \ + grep -oE '"([^"\\]|\\.)*"|true|false|null|[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?|[][}{:,]' | \ awk ' BEGIN { depth=0; diff --git a/lib/plugin_cache.sh b/lib/plugin_cache.sh new file mode 100644 index 0000000..0864550 --- /dev/null +++ b/lib/plugin_cache.sh @@ -0,0 +1,13 @@ +# Auto-generated plugin cache. Do not edit manually. +declare -g -A PLUGIN_URLS +declare -g -A PLUGIN_VERSIONS +declare -g -A PLUGIN_BOOTSTRAP_VERSIONS +PLUGIN_VERSIONS["weather"]="1.0.0" +PLUGIN_URLS["weather"]="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/official_plugins/weather.sh" +PLUGIN_BOOTSTRAP_VERSIONS["weather"]="2.1.0" +PLUGIN_VERSIONS["sysinfo"]="1.0.0" +PLUGIN_URLS["sysinfo"]="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/official_plugins/sysinfo.sh" +PLUGIN_BOOTSTRAP_VERSIONS["sysinfo"]="2.1.0" +PLUGIN_VERSIONS["todo"]="1.0.0" +PLUGIN_URLS["todo"]="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/official_plugins/todo.sh" +PLUGIN_BOOTSTRAP_VERSIONS["todo"]="2.1.0" diff --git a/lib/plugins.sh b/lib/plugins.sh index e3c9a3a..c19400e 100644 --- a/lib/plugins.sh +++ b/lib/plugins.sh @@ -39,14 +39,28 @@ parse_plugin_manifest() { 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" @@ -57,6 +71,7 @@ update_plugin_cache() { # 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 @@ -93,11 +108,8 @@ EOF } manage_plugin_sources() { + ensure_sources_file local sources_file="$BOOTSTRAP_DIR/plugin_sources.txt" - if [ ! -f "$sources_file" ]; then - touch "$sources_file" - echo "# Add raw URLs to JSON plugin manifests here, one per line." > "$sources_file" - fi local editor="${EDITOR:-}" if [ -z "$editor" ]; then @@ -151,6 +163,18 @@ run_plugin() { 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 diff --git a/lib/routes.sh b/lib/routes.sh index 26a8b98..5e97ff1 100755 --- a/lib/routes.sh +++ b/lib/routes.sh @@ -200,7 +200,6 @@ for script in "${SCRIPTS[@]}"; do # Handle non-installer commands case "$script" in plugin) - shift # consume 'plugin' arg handle_plugin "$@" # Once handle_plugin completes, we should exit so it doesn't process more SCRIPTS exit $? diff --git a/plugins.json b/plugins.json new file mode 100644 index 0000000..843f9b8 --- /dev/null +++ b/plugins.json @@ -0,0 +1,22 @@ +{ + "plugins": { + "weather": { + "version": "1.0.0", + "url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/weather.sh", + "bootstrap": "2.1.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.1.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.1.0", + "description": "A simple command-line todo list manager" + } + } +} diff --git a/plugins/sysinfo.sh b/plugins/sysinfo.sh new file mode 100644 index 0000000..2454d06 --- /dev/null +++ b/plugins/sysinfo.sh @@ -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 "$@" diff --git a/plugins/todo.sh b/plugins/todo.sh new file mode 100644 index 0000000..f9dc0e1 --- /dev/null +++ b/plugins/todo.sh @@ -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 " + 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 " + 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 " + 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 " + 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 Add a new task" + echo " done Mark a task as completed" + echo " rm 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 "$@" diff --git a/plugins/weather.sh b/plugins/weather.sh new file mode 100644 index 0000000..dce1034 --- /dev/null +++ b/plugins/weather.sh @@ -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 "$@" diff --git a/readme.md b/readme.md index 313c756..dc41a48 100644 --- a/readme.md +++ b/readme.md @@ -115,6 +115,28 @@ Plugins are first-party or third-party applications written to work directly wit 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 @@ -123,7 +145,7 @@ 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 a plugin simply by calling its name: +You can then execute any plugin simply by calling its name: ```bash b my_plugin