├── .husky └── pre-commit ├── package.json ├── bun.lock ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── release.yml ├── Cargo.toml ├── LICENSE ├── src ├── notify.rs ├── detect.rs ├── config.rs ├── execute.rs ├── tui.rs └── main.rs ├── README.md ├── scripts └── release.sh ├── backbone.toml └── Cargo.lock /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #\!/bin/sh 2 | set -e 3 | 4 | echo "Running cargo fmt..." 5 | cargo fmt --all -- --check 6 | 7 | echo "Running cargo clippy..." 8 | cargo clippy --all-targets --all-features -- -D warnings 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spine", 3 | "version": "1.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "husky": "^9.1.7" 7 | }, 8 | "scripts": { 9 | "prepare": "husky", 10 | "release": "bash ./scripts/release.sh" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "spine", 6 | "devDependencies": { 7 | "husky": "^9.1.7", 8 | }, 9 | }, 10 | }, 11 | "packages": { 12 | "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target/ 3 | **/*.rs.bk 4 | 5 | # IDE 6 | .vscode/ 7 | .idea/ 8 | *.swp 9 | *.swo 10 | *~ 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Logs 17 | *.log 18 | 19 | # Runtime 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Config files (user-specific) 25 | backbone.toml.local 26 | 27 | # Node.js (for development tools) 28 | node_modules/ 29 | package-lock.json 30 | yarn.lock 31 | bun.lockb 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. OSX] 28 | - Version [e.g. Sequoia 15.5] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spine-pkgman" 3 | version = "0.3.0" 4 | edition = "2021" 5 | description = "The backbone of your package management ecosystem. Automatically detects and updates all installed package managers in parallel across *nix systems." 6 | license = "MIT" 7 | repository = "https://github.com/plyght/spine" 8 | readme = "README.md" 9 | keywords = ["package-manager", "automation", "system", "cli", "tui"] 10 | categories = ["command-line-utilities", "development-tools"] 11 | 12 | [[bin]] 13 | name = "spn" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | clap = { version = "4.0", features = ["derive"] } 18 | ratatui = "0.29" 19 | crossterm = "0.29" 20 | tokio = { version = "1.0", features = ["full"] } 21 | serde = { version = "1.0", features = ["derive"] } 22 | toml = "0.8" 23 | anyhow = "1.0" 24 | which = "7.0" 25 | dirs = "6.0" 26 | indicatif = "0.17" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 plyght 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/notify.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::process::Command; 3 | 4 | pub fn send_notification(title: &str, message: &str) -> Result<()> { 5 | #[cfg(target_os = "macos")] 6 | { 7 | send_macos_notification(title, message) 8 | } 9 | 10 | #[cfg(target_os = "linux")] 11 | { 12 | send_linux_notification(title, message) 13 | } 14 | 15 | #[cfg(not(any(target_os = "macos", target_os = "linux")))] 16 | { 17 | Ok(()) 18 | } 19 | } 20 | 21 | #[cfg(target_os = "macos")] 22 | fn send_macos_notification(title: &str, message: &str) -> Result<()> { 23 | let script = format!( 24 | r#"display notification "{}" with title "{}""#, 25 | message.replace('\"', "\\\""), 26 | title.replace('\"', "\\\"") 27 | ); 28 | 29 | Command::new("osascript").arg("-e").arg(&script).output()?; 30 | 31 | Ok(()) 32 | } 33 | 34 | #[cfg(target_os = "linux")] 35 | fn send_linux_notification(title: &str, message: &str) -> Result<()> { 36 | Command::new("notify-send") 37 | .arg(title) 38 | .arg(message) 39 | .arg("--icon=system-software-update") 40 | .output()?; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /src/detect.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, ManagerConfig}; 2 | use anyhow::Result; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct DetectedManager { 6 | pub name: String, 7 | pub config: ManagerConfig, 8 | pub status: ManagerStatus, 9 | pub logs: String, 10 | } 11 | 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub enum ManagerStatus { 14 | Pending, 15 | Running(String), 16 | Success, 17 | Failed(String), 18 | } 19 | 20 | pub async fn detect_package_managers(config: &Config) -> Result> { 21 | let mut detected = Vec::new(); 22 | 23 | for (name, manager_config) in &config.managers { 24 | if is_manager_available(&manager_config.check_command).await? { 25 | detected.push(DetectedManager { 26 | name: name.clone(), 27 | config: manager_config.clone(), 28 | status: ManagerStatus::Pending, 29 | logs: String::new(), 30 | }); 31 | } 32 | } 33 | 34 | detected.sort_by(|a, b| a.name.cmp(&b.name)); 35 | 36 | Ok(detected) 37 | } 38 | 39 | async fn is_manager_available(check_command: &str) -> Result { 40 | let parts: Vec<&str> = check_command.split_whitespace().collect(); 41 | if parts.is_empty() { 42 | return Ok(false); 43 | } 44 | 45 | let command = parts[0]; 46 | Ok(which::which(command).is_ok()) 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spine 2 | 3 |
4 | 19a97d4a6a32afcd37b173d3d1e3ad02_upscayl_4x_upscayl-standard-4x 5 |
6 | 7 | The backbone of your package management ecosystem, Spine, Automatically detects and updates all installed package managers in parallel across \*nix systems. 8 | 9 | ## Overview 10 | 11 | Spine serves as the central support structure that connects and coordinates all your package managers. Just as the spine supports the entire body, Spine supports all package managers by discovering them on your system and running their update workflows simultaneously with a clean TUI interface. 12 | 13 | ## Features 14 | 15 | - **Universal Detection**: Auto-discovers 15+ package managers (Homebrew, APT, DNF, Pacman, Nix, Snap, Flatpak, etc.) 16 | - **Parallel Execution**: Runs all workflows simultaneously for maximum efficiency 17 | - **Interactive TUI**: Real-time progress monitoring with vim-style navigation 18 | - **Cross-Platform**: Works across Linux, macOS, and BSD variants 19 | - **Smart Sudo Handling**: Automatically handles privilege requirements per manager 20 | - **Configurable**: Extensible via TOML configuration 21 | 22 | ## Installation 23 | 24 | ```bash 25 | # From source 26 | git clone https://github.com/plyght/spine.git 27 | cd spine 28 | cargo build --release 29 | sudo cp target/release/spn /usr/local/bin/ 30 | 31 | # Using Cargo 32 | cargo install spine-pkgman 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```bash 38 | # List detected package managers 39 | spn list 40 | 41 | # Upgrade all package managers 42 | spn upgrade 43 | ``` 44 | 45 | The TUI interface shows real-time status: Pending → Refreshing → Self-updating → Upgrading → Cleaning → Complete 46 | 47 | Navigate with ↑↓/j/k, press Enter for details, 'q' to quit. 48 | 49 | ## Configuration 50 | 51 | Spine uses `backbone.toml` to define package manager commands: 52 | 53 | ```toml 54 | [managers.brew] 55 | name = "Homebrew" 56 | check_command = "brew --version" 57 | refresh = "brew update" 58 | upgrade_all = "brew upgrade" 59 | cleanup = "brew cleanup" 60 | requires_sudo = false 61 | ``` 62 | 63 | Configuration is searched in: current directory → binary directory → `/etc/spine/` → `/usr/local/etc/spine/` 64 | 65 | ## Architecture 66 | 67 | - `config.rs`: Configuration loading and parsing 68 | - `detect.rs`: Package manager discovery 69 | - `execute.rs`: Command execution with timeout/sudo handling 70 | - `tui.rs`: Terminal interface using Ratatui 71 | - `main.rs`: CLI orchestration 72 | 73 | ## Development 74 | 75 | ```bash 76 | cargo build 77 | cargo test 78 | ``` 79 | 80 | Requires Rust 1.70+. Key dependencies: clap, ratatui, crossterm, tokio, serde/toml. 81 | 82 | ## License 83 | 84 | MIT License -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | name: Build ${{ matrix.platform }} (${{ matrix.arch }}) 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | # Linux x64 20 | - os: ubuntu-latest 21 | platform: linux 22 | arch: x64 23 | target: x86_64-unknown-linux-gnu 24 | binary_name: spn 25 | asset_name: spn-linux-x64 26 | 27 | # Linux ARM64 28 | - os: ubuntu-latest 29 | platform: linux 30 | arch: arm64 31 | target: aarch64-unknown-linux-gnu 32 | binary_name: spn 33 | asset_name: spn-linux-arm64 34 | 35 | # macOS x64 36 | - os: macos-latest 37 | platform: macos 38 | arch: x64 39 | target: x86_64-apple-darwin 40 | binary_name: spn 41 | asset_name: spn-macos-x64 42 | 43 | # macOS ARM64 44 | - os: macos-latest 45 | platform: macos 46 | arch: arm64 47 | target: aarch64-apple-darwin 48 | binary_name: spn 49 | asset_name: spn-macos-arm64 50 | 51 | steps: 52 | - name: Checkout code 53 | uses: actions/checkout@v4 54 | 55 | - name: Setup Rust toolchain 56 | uses: dtolnay/rust-toolchain@stable 57 | with: 58 | targets: ${{ matrix.target }} 59 | 60 | - name: Install cross-compilation tools (Linux ARM64) 61 | if: matrix.target == 'aarch64-unknown-linux-gnu' 62 | run: | 63 | sudo apt-get update 64 | sudo apt-get install -y gcc-aarch64-linux-gnu 65 | 66 | - name: Build release binary 67 | run: cargo build --release --target ${{ matrix.target }} 68 | 69 | - name: Prepare binary 70 | run: | 71 | cp target/${{ matrix.target }}/release/${{ matrix.binary_name }} ${{ matrix.asset_name }} 72 | chmod +x ${{ matrix.asset_name }} 73 | 74 | - name: Upload binary to release 75 | uses: softprops/action-gh-release@v2 76 | with: 77 | files: ${{ matrix.asset_name }} 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | publish-crates: 82 | name: Publish to crates.io 83 | runs-on: ubuntu-latest 84 | needs: build 85 | steps: 86 | - name: Checkout code 87 | uses: actions/checkout@v4 88 | 89 | - name: Setup Rust toolchain 90 | uses: dtolnay/rust-toolchain@stable 91 | 92 | - name: Publish to crates.io 93 | run: cargo publish --token ${{ secrets.CARGO_TOKEN }} 94 | continue-on-error: true 95 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Debug, Clone, Deserialize, Serialize)] 7 | pub struct Config { 8 | pub managers: HashMap, 9 | #[serde(default)] 10 | pub auto_update: AutoUpdateConfig, 11 | } 12 | 13 | #[derive(Debug, Clone, Deserialize, Serialize)] 14 | pub struct ManagerConfig { 15 | pub name: String, 16 | pub check_command: String, 17 | pub refresh: Option, 18 | pub self_update: Option, 19 | pub upgrade_all: String, 20 | pub cleanup: Option, 21 | pub requires_sudo: bool, 22 | } 23 | 24 | #[derive(Debug, Clone, Deserialize, Serialize)] 25 | pub struct AutoUpdateConfig { 26 | #[serde(default)] 27 | pub enabled: bool, 28 | #[serde(default = "default_schedule")] 29 | pub schedule: String, 30 | #[serde(default = "default_time")] 31 | pub time: String, 32 | #[serde(default = "default_day")] 33 | pub day: String, 34 | #[serde(default = "default_notify")] 35 | pub notify: bool, 36 | #[serde(default = "default_no_tui")] 37 | pub no_tui: bool, 38 | } 39 | 40 | impl Default for AutoUpdateConfig { 41 | fn default() -> Self { 42 | Self { 43 | enabled: false, 44 | schedule: default_schedule(), 45 | time: default_time(), 46 | day: default_day(), 47 | notify: default_notify(), 48 | no_tui: default_no_tui(), 49 | } 50 | } 51 | } 52 | 53 | fn default_schedule() -> String { 54 | "daily".to_string() 55 | } 56 | 57 | fn default_time() -> String { 58 | "18:00".to_string() 59 | } 60 | 61 | fn default_day() -> String { 62 | "monday".to_string() 63 | } 64 | 65 | fn default_notify() -> bool { 66 | true 67 | } 68 | 69 | fn default_no_tui() -> bool { 70 | true 71 | } 72 | 73 | fn get_config_paths() -> Vec { 74 | let mut paths = Vec::new(); 75 | 76 | // XDG config directory (~/.config/spine/backbone.toml) - FIRST priority 77 | if let Some(config_dir) = dirs::config_dir() { 78 | paths.push(config_dir.join("spine").join("backbone.toml")); 79 | } 80 | 81 | // Current directory 82 | if let Ok(current_dir) = std::env::current_dir() { 83 | paths.push(current_dir.join("backbone.toml")); 84 | } 85 | 86 | // Home directory (~/.spine/backbone.toml) 87 | if let Some(home_dir) = dirs::home_dir() { 88 | paths.push(home_dir.join(".spine").join("backbone.toml")); 89 | } 90 | 91 | // Binary directory 92 | if let Ok(exe_path) = std::env::current_exe() { 93 | if let Some(parent) = exe_path.parent() { 94 | paths.push(parent.join("backbone.toml")); 95 | } 96 | } 97 | 98 | // System directories 99 | paths.push(PathBuf::from("/etc/spine/backbone.toml")); 100 | paths.push(PathBuf::from("/usr/local/etc/spine/backbone.toml")); 101 | 102 | paths 103 | } 104 | 105 | async fn create_default_config() -> Result { 106 | let default_config = include_str!("../backbone.toml"); 107 | 108 | // Always try XDG config directory first (default on all systems) 109 | if let Some(config_dir) = dirs::config_dir() { 110 | let spine_config_dir = config_dir.join("spine"); 111 | let config_path = spine_config_dir.join("backbone.toml"); 112 | 113 | match tokio::fs::create_dir_all(&spine_config_dir).await { 114 | Ok(_) => { 115 | tokio::fs::write(&config_path, default_config).await?; 116 | return Ok(config_path); 117 | } 118 | Err(_) => { 119 | // Continue to fallback if XDG fails 120 | } 121 | } 122 | } 123 | 124 | // Fallback to home directory 125 | if let Some(home_dir) = dirs::home_dir() { 126 | let spine_home_dir = home_dir.join(".spine"); 127 | let config_path = spine_home_dir.join("backbone.toml"); 128 | tokio::fs::create_dir_all(&spine_home_dir).await?; 129 | tokio::fs::write(&config_path, default_config).await?; 130 | return Ok(config_path); 131 | } 132 | 133 | anyhow::bail!("Unable to create config directory in any standard location"); 134 | } 135 | 136 | pub async fn load_config() -> Result { 137 | let possible_paths = get_config_paths(); 138 | 139 | for path in &possible_paths { 140 | if path.exists() { 141 | let content = tokio::fs::read_to_string(&path).await?; 142 | let config: Config = toml::from_str(&content)?; 143 | return Ok(config); 144 | } 145 | } 146 | 147 | // No config found, create a default one 148 | let created_path = create_default_config().await?; 149 | let content = tokio::fs::read_to_string(&created_path).await?; 150 | let config: Config = toml::from_str(&content)?; 151 | 152 | eprintln!( 153 | "Created default configuration at: {}", 154 | created_path.display() 155 | ); 156 | Ok(config) 157 | } 158 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Colors for output 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | # Function to print colored output 11 | print_status() { 12 | printf "${GREEN}[INFO]${NC} %s\n" "$1" 13 | } 14 | 15 | print_warning() { 16 | printf "${YELLOW}[WARN]${NC} %s\n" "$1" 17 | } 18 | 19 | print_error() { 20 | printf "${RED}[ERROR]${NC} %s\n" "$1" 21 | } 22 | 23 | # Check if version argument is provided 24 | if [ "$#" -eq 0 ]; then 25 | print_error "Version argument required. Usage: ./scripts/release.sh v1.0.0" 26 | exit 1 27 | fi 28 | 29 | VERSION=$1 30 | 31 | # Validate version format (should start with 'v') 32 | if [[ ! $VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[0-9]+)?)?$ ]]; then 33 | print_error "Version should be in format 'v1.0.0' or 'v1.0.0-beta.1'" 34 | exit 1 35 | fi 36 | 37 | # Check for required dependencies 38 | command -v gh >/dev/null 2>&1 || { 39 | print_error "gh (GitHub CLI) is not installed. Please install it first: https://cli.github.com/" 40 | exit 1 41 | } 42 | 43 | command -v jq >/dev/null 2>&1 || { 44 | print_error "jq is not installed. Please install it first." 45 | exit 1 46 | } 47 | 48 | 49 | # Check if user is authenticated with GitHub CLI 50 | if ! gh auth status >/dev/null 2>&1; then 51 | print_error "GitHub CLI is not authenticated. Please run 'gh auth login' first." 52 | exit 1 53 | fi 54 | 55 | # Check if user has write access to this repository 56 | REPO_INFO=$(gh repo view --json owner,name,viewerPermission 2>/dev/null || echo "") 57 | if [[ -z "$REPO_INFO" ]]; then 58 | print_error "Unable to determine repository information. Make sure you're in a git repository with GitHub remote." 59 | exit 1 60 | fi 61 | 62 | VIEWER_PERMISSION=$(echo "$REPO_INFO" | jq -r '.viewerPermission // "NONE"') 63 | if [[ "$VIEWER_PERMISSION" != "ADMIN" && "$VIEWER_PERMISSION" != "WRITE" ]]; then 64 | print_error "You don't have write access to this repository. Only repository maintainers can create releases. Current permission: $VIEWER_PERMISSION" 65 | exit 1 66 | fi 67 | 68 | 69 | print_status "Starting release process for version $VERSION" 70 | 71 | # Check if we're on a clean git state 72 | if ! git diff-index --quiet HEAD --; then 73 | print_error "Working directory is not clean. Please commit or stash your changes." 74 | exit 1 75 | fi 76 | 77 | # Check if we're on main/master branch 78 | CURRENT_BRANCH=$(git branch --show-current) 79 | if [[ "$CURRENT_BRANCH" != "main" && "$CURRENT_BRANCH" != "master" ]]; then 80 | print_warning "You're not on main/master branch. Current branch: $CURRENT_BRANCH" 81 | read -p "Continue? (y/N) " -n 1 -r 82 | echo 83 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 84 | print_status "Release cancelled." 85 | exit 1 86 | fi 87 | fi 88 | 89 | # Build for current platform only to avoid cross-compilation issues 90 | CURRENT_TARGET=$(rustc -vV | grep host | cut -d' ' -f2) 91 | TARGETS=("$CURRENT_TARGET") 92 | 93 | # Array to store built binaries 94 | BUILT_BINARIES=() 95 | 96 | # Build for multiple targets 97 | print_status "Building release binaries for multiple platforms..." 98 | 99 | for target in "${TARGETS[@]}"; do 100 | print_status "Building for target: $target" 101 | 102 | # Install target if not already installed 103 | if ! rustup target list --installed | grep -q "$target"; then 104 | print_status "Installing target $target..." 105 | rustup target add "$target" 106 | fi 107 | 108 | # Build for target 109 | if cargo build --release --target "$target"; then 110 | binary_path="target/$target/release/spn" 111 | if [ -f "$binary_path" ]; then 112 | # Create target-specific binary name 113 | case "$target" in 114 | "x86_64-unknown-linux-gnu") 115 | binary_name="spn-linux-x64" 116 | ;; 117 | "aarch64-unknown-linux-gnu") 118 | binary_name="spn-linux-arm64" 119 | ;; 120 | "x86_64-apple-darwin") 121 | binary_name="spn-macos-x64" 122 | ;; 123 | "aarch64-apple-darwin") 124 | binary_name="spn-macos-arm64" 125 | ;; 126 | *) 127 | # Use current platform detection for unknown targets 128 | if [[ "$OSTYPE" == "darwin"* ]]; then 129 | if [[ "$(uname -m)" == "arm64" ]]; then 130 | binary_name="spn-macos-arm64" 131 | else 132 | binary_name="spn-macos-x64" 133 | fi 134 | else 135 | if [[ "$(uname -m)" == "aarch64" ]]; then 136 | binary_name="spn-linux-arm64" 137 | else 138 | binary_name="spn-linux-x64" 139 | fi 140 | fi 141 | ;; 142 | esac 143 | 144 | # Copy binary with target-specific name 145 | cp "$binary_path" "target/$binary_name" 146 | BUILT_BINARIES+=("target/$binary_name#$binary_name") 147 | print_status "Successfully built $binary_name" 148 | else 149 | print_warning "Binary not found for target $target, skipping..." 150 | fi 151 | else 152 | print_warning "Failed to build for target $target, skipping..." 153 | fi 154 | done 155 | 156 | # Check if at least one binary was built 157 | if [ ${#BUILT_BINARIES[@]} -eq 0 ]; then 158 | print_error "No binaries were successfully built" 159 | exit 1 160 | fi 161 | 162 | print_status "Successfully built ${#BUILT_BINARIES[@]} binaries" 163 | 164 | # Create git tag 165 | print_status "Creating git tag $VERSION..." 166 | if git rev-parse "$VERSION" >/dev/null 2>&1; then 167 | print_error "Tag $VERSION already exists." 168 | exit 1 169 | fi 170 | git tag -a "$VERSION" -m "Release $VERSION" 171 | 172 | # Push tag to origin 173 | print_status "Pushing tag to origin..." 174 | git push origin "$VERSION" 175 | 176 | # Create GitHub release with binary 177 | print_status "Creating GitHub release..." 178 | print_status "Using GitHub's auto-generated release notes" 179 | if ! gh release create "$VERSION" \ 180 | --title "Release $VERSION" \ 181 | --generate-notes \ 182 | "${BUILT_BINARIES[@]}"; then 183 | print_error "GitHub release creation failed." 184 | exit 1 185 | fi 186 | 187 | print_status "Release $VERSION created successfully!" 188 | print_status "You can view it at: https://github.com/$(gh repo view --json owner,name -q '.owner.login + "/" + .name')/releases/tag/$VERSION" -------------------------------------------------------------------------------- /backbone.toml: -------------------------------------------------------------------------------- 1 | # Package Manager Configuration File for Spine 2 | # Each section defines a package manager with its commands 3 | 4 | # Auto-update settings 5 | [auto_update] 6 | enabled = false # Set to true to enable automatic background updates 7 | schedule = "daily" # "daily" or "weekly" 8 | time = "18:00" # Time to run (24h format) 9 | day = "monday" # Day for weekly updates (monday, tuesday, etc.) 10 | notify = true # Send notification when complete 11 | no_tui = true # Run without interactive TUI 12 | 13 | [managers.brew] 14 | name = "Homebrew" 15 | check_command = "brew --version" 16 | refresh = "brew update" 17 | self_update = "brew update" 18 | upgrade_all = "brew upgrade" 19 | cleanup = "brew cleanup" 20 | requires_sudo = false 21 | 22 | [managers.apt] 23 | name = "APT" 24 | check_command = "apt --version" 25 | refresh = "apt update" 26 | upgrade_all = "apt upgrade -y" 27 | cleanup = "apt autoremove -y && apt autoclean" 28 | requires_sudo = true 29 | 30 | [managers.yum] 31 | name = "YUM" 32 | check_command = "yum --version" 33 | refresh = "yum check-update" 34 | upgrade_all = "yum update -y" 35 | cleanup = "yum autoremove -y && yum clean all" 36 | requires_sudo = true 37 | 38 | [managers.dnf] 39 | name = "DNF" 40 | check_command = "dnf --version" 41 | refresh = "dnf check-update" 42 | upgrade_all = "dnf upgrade -y" 43 | cleanup = "dnf autoremove -y && dnf clean all" 44 | requires_sudo = true 45 | 46 | [managers.pacman] 47 | name = "Pacman" 48 | check_command = "pacman --version" 49 | refresh = "pacman -Sy" 50 | upgrade_all = "pacman -Syu --noconfirm" 51 | cleanup = "pacman -Sc --noconfirm" 52 | requires_sudo = true 53 | 54 | [managers.zypper] 55 | name = "Zypper" 56 | check_command = "zypper --version" 57 | refresh = "zypper refresh" 58 | upgrade_all = "zypper update -y" 59 | cleanup = "zypper clean -a" 60 | requires_sudo = true 61 | 62 | [managers.emerge] 63 | name = "Portage" 64 | check_command = "emerge --version" 65 | refresh = "emerge --sync" 66 | upgrade_all = "emerge -uDN @world" 67 | cleanup = "emerge --depclean" 68 | requires_sudo = true 69 | 70 | [managers.nix] 71 | name = "Nix" 72 | check_command = "nix --version" 73 | refresh = "nix-channel --update" 74 | self_update = "nix upgrade-nix" 75 | upgrade_all = "nix-env -u" 76 | cleanup = "nix-collect-garbage -d" 77 | requires_sudo = false 78 | 79 | [managers.snap] 80 | name = "Snap" 81 | check_command = "snap version" 82 | refresh = "snap refresh" 83 | upgrade_all = "snap refresh" 84 | requires_sudo = true 85 | 86 | [managers.flatpak] 87 | name = "Flatpak" 88 | check_command = "flatpak --version" 89 | refresh = "flatpak update" 90 | upgrade_all = "flatpak update -y" 91 | cleanup = "flatpak uninstall --unused -y" 92 | requires_sudo = false 93 | 94 | [managers.port] 95 | name = "MacPorts" 96 | check_command = "port version" 97 | refresh = "port sync" 98 | self_update = "port selfupdate" 99 | upgrade_all = "port upgrade outdated" 100 | cleanup = "port uninstall inactive" 101 | requires_sudo = true 102 | 103 | [managers.pkg] 104 | name = "FreeBSD Packages" 105 | check_command = "pkg version" 106 | refresh = "pkg update" 107 | upgrade_all = "pkg upgrade -y" 108 | cleanup = "pkg autoremove -y && pkg clean" 109 | requires_sudo = true 110 | 111 | [managers.apk] 112 | name = "Alpine Package Keeper" 113 | check_command = "apk --version" 114 | refresh = "apk update" 115 | upgrade_all = "apk upgrade" 116 | cleanup = "apk cache clean" 117 | requires_sudo = true 118 | 119 | [managers.xbps] 120 | name = "XBPS" 121 | check_command = "xbps-query --version" 122 | refresh = "xbps-install -S" 123 | upgrade_all = "xbps-install -Su" 124 | cleanup = "xbps-remove -O" 125 | requires_sudo = true 126 | 127 | [managers.npm] 128 | name = "npm" 129 | check_command = "npm --version" 130 | refresh = "npm update -g" 131 | self_update = "npm install -g npm@latest" 132 | upgrade_all = "npm update -g" 133 | cleanup = "npm cache clean --force" 134 | requires_sudo = false 135 | 136 | [managers.yarn] 137 | name = "Yarn" 138 | check_command = "yarn --version" 139 | refresh = "yarn global upgrade" 140 | self_update = "yarn set version latest" 141 | upgrade_all = "yarn global upgrade" 142 | requires_sudo = false 143 | 144 | [managers.pnpm] 145 | name = "pnpm" 146 | check_command = "pnpm --version" 147 | refresh = "pnpm update -g" 148 | self_update = "pnpm add -g pnpm" 149 | upgrade_all = "pnpm update -g" 150 | requires_sudo = false 151 | 152 | [managers.pip] 153 | name = "pip" 154 | check_command = "pip --version" 155 | refresh = "pip index versions pip" 156 | self_update = "python -m pip install --upgrade pip" 157 | upgrade_all = "python -m pip install --upgrade pip setuptools wheel" 158 | requires_sudo = false 159 | 160 | [managers.pip3] 161 | name = "pip3" 162 | check_command = "pip3 --version" 163 | refresh = "pip3 index versions pip" 164 | self_update = "python3 -m pip install --upgrade pip" 165 | upgrade_all = "python3 -m pip install --upgrade pip setuptools wheel" 166 | requires_sudo = false 167 | 168 | [managers.rustup] 169 | name = "Rustup" 170 | check_command = "rustup --version" 171 | refresh = "rustup check" 172 | self_update = "rustup self update" 173 | upgrade_all = "rustup update" 174 | requires_sudo = false 175 | 176 | [managers.cargo] 177 | name = "Cargo" 178 | check_command = "cargo --version" 179 | refresh = "cargo search --limit 0" 180 | upgrade_all = "cargo update" 181 | requires_sudo = false 182 | 183 | [managers.composer] 184 | name = "Composer" 185 | check_command = "composer --version" 186 | refresh = "composer outdated" 187 | self_update = "composer self-update" 188 | upgrade_all = "composer global update" 189 | requires_sudo = false 190 | 191 | [managers.gem] 192 | name = "RubyGems" 193 | check_command = "gem --version" 194 | refresh = "gem outdated" 195 | self_update = "gem update --system" 196 | upgrade_all = "gem update" 197 | cleanup = "gem cleanup" 198 | requires_sudo = false 199 | 200 | [managers.go] 201 | name = "Go modules" 202 | check_command = "go version" 203 | refresh = "go list -u -m all" 204 | upgrade_all = "go get -u all" 205 | requires_sudo = false 206 | 207 | [managers.conda] 208 | name = "Conda" 209 | check_command = "conda --version" 210 | refresh = "conda list --outdated" 211 | self_update = "conda update conda" 212 | upgrade_all = "conda update --all" 213 | cleanup = "conda clean --all" 214 | requires_sudo = false 215 | 216 | [managers.scoop] 217 | name = "Scoop" 218 | check_command = "scoop --version" 219 | refresh = "scoop update" 220 | self_update = "scoop update scoop" 221 | upgrade_all = "scoop update *" 222 | cleanup = "scoop cleanup *" 223 | requires_sudo = false 224 | 225 | [managers.bun] 226 | name = "Bun" 227 | check_command = "bun --version" 228 | refresh = "bun update" 229 | self_update = "bun upgrade" 230 | upgrade_all = "bun update" 231 | cleanup = "bun pm cache rm" 232 | requires_sudo = false -------------------------------------------------------------------------------- /src/execute.rs: -------------------------------------------------------------------------------- 1 | use crate::detect::{DetectedManager, ManagerStatus}; 2 | use anyhow::Result; 3 | use std::process::Stdio; 4 | use std::sync::Arc; 5 | use std::time::Duration; 6 | use tokio::io::{AsyncBufReadExt, BufReader}; 7 | use tokio::process::Command; 8 | use tokio::sync::Mutex; 9 | 10 | pub async fn execute_manager_workflow(manager_ref: Arc>) -> Result<()> { 11 | let config = { 12 | let manager = manager_ref.lock().await; 13 | manager.config.clone() 14 | }; 15 | 16 | let mut accumulated_logs = String::new(); 17 | 18 | // Refresh repositories 19 | if let Some(refresh_cmd) = &config.refresh { 20 | accumulated_logs.push_str("=== REFRESHING REPOSITORIES ===\n"); 21 | { 22 | let mut manager = manager_ref.lock().await; 23 | manager.status = ManagerStatus::Running("Refreshing".to_string()); 24 | manager.logs = accumulated_logs.clone(); 25 | } 26 | 27 | match execute_command_with_logs( 28 | refresh_cmd, 29 | config.requires_sudo, 30 | Duration::from_secs(300), 31 | manager_ref.clone(), 32 | "Refreshing".to_string(), 33 | &mut accumulated_logs, 34 | ) 35 | .await 36 | { 37 | Ok(true) => { 38 | accumulated_logs.push_str("\n✓ Refresh completed\n\n"); 39 | } 40 | Ok(false) => { 41 | let mut manager = manager_ref.lock().await; 42 | manager.status = ManagerStatus::Failed(format!( 43 | "Refresh command failed\n\nLogs:\n{accumulated_logs}" 44 | )); 45 | return Ok(()); 46 | } 47 | Err(e) => { 48 | let mut manager = manager_ref.lock().await; 49 | manager.status = ManagerStatus::Failed(format!( 50 | "Refresh error: {e}\n\nLogs:\n{accumulated_logs}" 51 | )); 52 | return Ok(()); 53 | } 54 | } 55 | } 56 | 57 | // Self-update 58 | if let Some(self_update_cmd) = &config.self_update { 59 | accumulated_logs.push_str("=== SELF-UPDATE ===\n"); 60 | { 61 | let mut manager = manager_ref.lock().await; 62 | manager.status = ManagerStatus::Running("Self-updating".to_string()); 63 | manager.logs = accumulated_logs.clone(); 64 | } 65 | 66 | match execute_command_with_logs( 67 | self_update_cmd, 68 | config.requires_sudo, 69 | Duration::from_secs(600), 70 | manager_ref.clone(), 71 | "Self-updating".to_string(), 72 | &mut accumulated_logs, 73 | ) 74 | .await 75 | { 76 | Ok(true) => { 77 | accumulated_logs.push_str("\n✓ Self-update completed\n\n"); 78 | } 79 | Ok(false) => { 80 | let mut manager = manager_ref.lock().await; 81 | manager.status = ManagerStatus::Failed(format!( 82 | "Self-update command failed\n\nLogs:\n{accumulated_logs}" 83 | )); 84 | return Ok(()); 85 | } 86 | Err(e) => { 87 | let mut manager = manager_ref.lock().await; 88 | manager.status = ManagerStatus::Failed(format!( 89 | "Self-update error: {e}\n\nLogs:\n{accumulated_logs}" 90 | )); 91 | return Ok(()); 92 | } 93 | } 94 | } 95 | 96 | // Upgrade all packages 97 | accumulated_logs.push_str("=== UPGRADING PACKAGES ===\n"); 98 | { 99 | let mut manager = manager_ref.lock().await; 100 | manager.status = ManagerStatus::Running("Upgrading".to_string()); 101 | manager.logs = accumulated_logs.clone(); 102 | } 103 | 104 | match execute_command_with_logs( 105 | &config.upgrade_all, 106 | config.requires_sudo, 107 | Duration::from_secs(3600), 108 | manager_ref.clone(), 109 | "Upgrading".to_string(), 110 | &mut accumulated_logs, 111 | ) 112 | .await 113 | { 114 | Ok(true) => { 115 | accumulated_logs.push_str("\n✓ Upgrade completed\n\n"); 116 | } 117 | Ok(false) => { 118 | let mut manager = manager_ref.lock().await; 119 | manager.status = ManagerStatus::Failed(format!( 120 | "Upgrade command failed\n\nLogs:\n{accumulated_logs}" 121 | )); 122 | return Ok(()); 123 | } 124 | Err(e) => { 125 | let mut manager = manager_ref.lock().await; 126 | manager.status = 127 | ManagerStatus::Failed(format!("Upgrade error: {e}\n\nLogs:\n{accumulated_logs}")); 128 | return Ok(()); 129 | } 130 | } 131 | 132 | // Cleanup 133 | if let Some(cleanup_cmd) = &config.cleanup { 134 | accumulated_logs.push_str("=== CLEANUP ===\n"); 135 | { 136 | let mut manager = manager_ref.lock().await; 137 | manager.status = ManagerStatus::Running("Cleaning".to_string()); 138 | manager.logs = accumulated_logs.clone(); 139 | } 140 | 141 | match execute_command_with_logs( 142 | cleanup_cmd, 143 | config.requires_sudo, 144 | Duration::from_secs(300), 145 | manager_ref.clone(), 146 | "Cleaning".to_string(), 147 | &mut accumulated_logs, 148 | ) 149 | .await 150 | { 151 | Ok(true) => { 152 | accumulated_logs.push_str("\n✓ Cleanup completed\n\n"); 153 | } 154 | Ok(false) => { 155 | let mut manager = manager_ref.lock().await; 156 | manager.status = ManagerStatus::Failed(format!( 157 | "Cleanup command failed\n\nLogs:\n{accumulated_logs}" 158 | )); 159 | return Ok(()); 160 | } 161 | Err(e) => { 162 | let mut manager = manager_ref.lock().await; 163 | manager.status = ManagerStatus::Failed(format!( 164 | "Cleanup error: {e}\n\nLogs:\n{accumulated_logs}" 165 | )); 166 | return Ok(()); 167 | } 168 | } 169 | } 170 | 171 | // Set final success status with complete logs 172 | { 173 | let mut manager = manager_ref.lock().await; 174 | manager.status = ManagerStatus::Success; 175 | manager.logs = accumulated_logs; 176 | } 177 | Ok(()) 178 | } 179 | 180 | // Wrapper function for backwards compatibility with non-TUI usage 181 | pub async fn execute_manager_workflow_simple(manager: &mut DetectedManager) -> Result<()> { 182 | let manager_ref = Arc::new(Mutex::new(manager.clone())); 183 | execute_manager_workflow(manager_ref.clone()).await?; 184 | 185 | // Copy the updated state back 186 | let updated_manager = manager_ref.lock().await; 187 | *manager = updated_manager.clone(); 188 | 189 | Ok(()) 190 | } 191 | 192 | async fn execute_command_with_logs( 193 | command: &str, 194 | requires_sudo: bool, 195 | timeout: Duration, 196 | manager_ref: Arc>, 197 | operation: String, 198 | accumulated_logs: &mut String, 199 | ) -> Result { 200 | let mut cmd = build_command(command, requires_sudo)?; 201 | 202 | let mut child = cmd.spawn()?; 203 | 204 | let stdout = child 205 | .stdout 206 | .take() 207 | .ok_or_else(|| anyhow::anyhow!("Failed to get stdout"))?; 208 | let stderr = child 209 | .stderr 210 | .take() 211 | .ok_or_else(|| anyhow::anyhow!("Failed to get stderr"))?; 212 | 213 | let mut stdout_reader = BufReader::new(stdout).lines(); 214 | let mut stderr_reader = BufReader::new(stderr).lines(); 215 | 216 | let timeout_future = tokio::time::sleep(timeout); 217 | tokio::pin!(timeout_future); 218 | 219 | let mut stdout_closed = false; 220 | let mut stderr_closed = false; 221 | 222 | loop { 223 | tokio::select! { 224 | () = &mut timeout_future => { 225 | let _ = child.kill().await; 226 | accumulated_logs.push_str("\nERROR: Command timed out\n"); 227 | let mut manager = manager_ref.lock().await; 228 | manager.status = ManagerStatus::Failed(format!("Command timed out\n\nLogs:\n{accumulated_logs}")); 229 | return Err(anyhow::anyhow!("Command timed out")); 230 | } 231 | 232 | stdout_line = stdout_reader.next_line(), if !stdout_closed => { 233 | match stdout_line { 234 | Ok(Some(line)) => { 235 | accumulated_logs.push_str(&line); 236 | accumulated_logs.push('\n'); 237 | 238 | let mut manager = manager_ref.lock().await; 239 | manager.status = ManagerStatus::Running(operation.clone()); 240 | manager.logs = accumulated_logs.clone(); 241 | } 242 | Ok(None) => { 243 | stdout_closed = true; 244 | } 245 | Err(e) => { 246 | accumulated_logs.push_str(&format!("ERROR reading stdout: {e}\n")); 247 | return Err(anyhow::anyhow!("Error reading stdout: {e}")); 248 | } 249 | } 250 | } 251 | 252 | stderr_line = stderr_reader.next_line(), if !stderr_closed => { 253 | match stderr_line { 254 | Ok(Some(line)) => { 255 | accumulated_logs.push_str("STDERR: "); 256 | accumulated_logs.push_str(&line); 257 | accumulated_logs.push('\n'); 258 | 259 | let mut manager = manager_ref.lock().await; 260 | manager.status = ManagerStatus::Running(operation.clone()); 261 | manager.logs = accumulated_logs.clone(); 262 | } 263 | Ok(None) => { 264 | stderr_closed = true; 265 | } 266 | Err(e) => { 267 | accumulated_logs.push_str(&format!("ERROR reading stderr: {e}\n")); 268 | return Err(anyhow::anyhow!("Error reading stderr: {e}")); 269 | } 270 | } 271 | } 272 | 273 | status = child.wait() => { 274 | match status { 275 | Ok(exit_status) => { 276 | let success = exit_status.success(); 277 | if !success { 278 | accumulated_logs.push_str(&format!("\nCommand exited with code: {}\n", exit_status.code().unwrap_or(-1))); 279 | } 280 | return Ok(success); 281 | } 282 | Err(e) => { 283 | accumulated_logs.push_str(&format!("ERROR waiting for command: {e}\n")); 284 | return Err(anyhow::anyhow!("Error waiting for command: {e}")); 285 | } 286 | } 287 | } 288 | } 289 | } 290 | } 291 | 292 | fn build_command(command: &str, requires_sudo: bool) -> Result { 293 | if command.is_empty() { 294 | anyhow::bail!("Empty command"); 295 | } 296 | 297 | let mut cmd = if requires_sudo { 298 | if which::which("sudo").is_err() { 299 | anyhow::bail!("sudo is required but not available"); 300 | } 301 | let mut c = Command::new("sudo"); 302 | c.arg("-n"); 303 | c.arg("sh"); 304 | c.arg("-c"); 305 | c.arg(command); 306 | c 307 | } else { 308 | let mut c = Command::new("sh"); 309 | c.arg("-c"); 310 | c.arg(command); 311 | c 312 | }; 313 | 314 | cmd.stdout(Stdio::piped()) 315 | .stderr(Stdio::piped()) 316 | .stdin(Stdio::null()); 317 | 318 | Ok(cmd) 319 | } 320 | 321 | pub async fn check_sudo_availability() -> bool { 322 | if which::which("sudo").is_err() { 323 | return false; 324 | } 325 | 326 | // Test if we can run sudo without password prompt 327 | match Command::new("sudo") 328 | .args(["-n", "true"]) 329 | .stdout(Stdio::null()) 330 | .stderr(Stdio::null()) 331 | .status() 332 | .await 333 | { 334 | Ok(status) => status.success(), 335 | Err(_) => false, 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::detect::{DetectedManager, ManagerStatus}; 3 | use crate::execute::execute_manager_workflow; 4 | use anyhow::Result; 5 | use crossterm::{ 6 | event::{self, Event, KeyCode, KeyEventKind}, 7 | execute, 8 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 9 | }; 10 | use ratatui::{ 11 | backend::CrosstermBackend, 12 | layout::{Constraint, Direction, Layout, Margin}, 13 | style::{Color, Modifier, Style}, 14 | text::{Line, Span, Text}, 15 | widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, 16 | Frame, Terminal, 17 | }; 18 | use std::io; 19 | use std::sync::Arc; 20 | use tokio::sync::Mutex; 21 | use tokio::task::JoinSet; 22 | 23 | #[derive(Debug, Clone, PartialEq)] 24 | enum AppState { 25 | ManagerList, 26 | DetailView(usize), 27 | LogsView(usize), 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | struct LogsViewState { 32 | scroll_offset: u16, 33 | } 34 | 35 | pub async fn run_tui( 36 | managers: Vec, 37 | _config: Config, 38 | selective: bool, 39 | ) -> Result<()> { 40 | enable_raw_mode()?; 41 | let mut stdout = io::stdout(); 42 | execute!(stdout, EnterAlternateScreen)?; 43 | let backend = CrosstermBackend::new(stdout); 44 | let mut terminal = Terminal::new(backend)?; 45 | 46 | // Convert managers to shared Arc> for real-time updates 47 | let shared_managers: Vec>> = managers 48 | .into_iter() 49 | .map(|m| Arc::new(Mutex::new(m))) 50 | .collect(); 51 | 52 | let mut selected = 0; 53 | let mut list_state = ListState::default(); 54 | list_state.select(Some(0)); 55 | let mut app_state = AppState::ManagerList; 56 | 57 | // Track scroll state for each manager's logs view 58 | let mut logs_scroll_states: Vec = (0..shared_managers.len()) 59 | .map(|_| LogsViewState { scroll_offset: 0 }) 60 | .collect(); 61 | 62 | // Track which managers have started their workflows 63 | let mut started_workflows: Vec = vec![false; shared_managers.len()]; 64 | 65 | // Track whether user manually quit to avoid showing summary 66 | #[allow(unused_assignments)] 67 | let mut user_quit = false; 68 | 69 | // Track when all operations completed for timed message display 70 | let mut completion_time: Option = None; 71 | 72 | // Start all manager workflows in parallel (only if not in selective mode) 73 | let mut join_set = JoinSet::new(); 74 | if !selective { 75 | for (i, manager_ref) in shared_managers.iter().enumerate() { 76 | let manager_ref = manager_ref.clone(); 77 | started_workflows[i] = true; 78 | join_set.spawn(async move { 79 | let _ = execute_manager_workflow(manager_ref).await; 80 | i 81 | }); 82 | } 83 | } 84 | 85 | loop { 86 | // Check for completed tasks 87 | while let Some(result) = join_set.try_join_next() { 88 | match result { 89 | Ok(_index) => { 90 | // Task completed - manager state was updated via shared reference 91 | } 92 | Err(join_error) => { 93 | // Log join errors but continue - individual manager failures are handled in the workflow 94 | eprintln!("Task join error: {join_error}"); 95 | break; 96 | } 97 | } 98 | } 99 | 100 | // Check if all managers are done 101 | let all_done = if selective { 102 | // In selective mode, only check started workflows 103 | let mut all_complete = true; 104 | for (i, m) in shared_managers.iter().enumerate() { 105 | if started_workflows[i] { 106 | let manager = m.lock().await; 107 | if !matches!( 108 | manager.status, 109 | ManagerStatus::Success | ManagerStatus::Failed(_) 110 | ) { 111 | all_complete = false; 112 | break; 113 | } 114 | } 115 | } 116 | all_complete 117 | } else { 118 | // In non-selective mode, check all managers 119 | let mut all_complete = true; 120 | for m in shared_managers.iter() { 121 | let manager = m.lock().await; 122 | if !matches!( 123 | manager.status, 124 | ManagerStatus::Success | ManagerStatus::Failed(_) 125 | ) { 126 | all_complete = false; 127 | break; 128 | } 129 | } 130 | all_complete 131 | }; 132 | 133 | // Set completion time when all done for the first time 134 | if all_done && completion_time.is_none() { 135 | completion_time = Some(std::time::Instant::now()); 136 | } 137 | 138 | // Check if completion message should still be shown (5 seconds) 139 | let show_completion_message = if let Some(time) = completion_time { 140 | time.elapsed().as_secs() < 5 141 | } else { 142 | false 143 | }; 144 | 145 | // Clone manager data for rendering to avoid blocking in draw 146 | let managers_snapshot: Vec = { 147 | let mut snapshot = Vec::new(); 148 | for m in shared_managers.iter() { 149 | snapshot.push(m.lock().await.clone()); 150 | } 151 | snapshot 152 | }; 153 | 154 | terminal.draw(|f| { 155 | ui( 156 | f, 157 | &managers_snapshot, 158 | &mut list_state, 159 | &app_state, 160 | &logs_scroll_states, 161 | selective, 162 | all_done && show_completion_message, 163 | ) 164 | })?; 165 | 166 | // Handle input 167 | if event::poll(std::time::Duration::from_millis(100))? { 168 | if let Event::Key(key) = event::read()? { 169 | if key.kind == KeyEventKind::Press { 170 | match (&app_state, key.code) { 171 | // Global quit commands 172 | (_, KeyCode::Char('q')) => { 173 | user_quit = true; 174 | break; 175 | } 176 | (AppState::DetailView(_) | AppState::LogsView(_), KeyCode::Esc) => { 177 | app_state = AppState::ManagerList; 178 | } 179 | // Manager list navigation 180 | (AppState::ManagerList, KeyCode::Down | KeyCode::Char('j')) => { 181 | if selected < shared_managers.len() - 1 { 182 | selected += 1; 183 | list_state.select(Some(selected)); 184 | } 185 | } 186 | (AppState::ManagerList, KeyCode::Up | KeyCode::Char('k')) => { 187 | if selected > 0 { 188 | selected -= 1; 189 | list_state.select(Some(selected)); 190 | } 191 | } 192 | (AppState::ManagerList, KeyCode::Enter) => { 193 | app_state = AppState::DetailView(selected); 194 | } 195 | // Selective mode: start workflow for selected manager 196 | (AppState::ManagerList, KeyCode::Char(' ')) if selective => { 197 | if selected < shared_managers.len() && !started_workflows[selected] { 198 | let manager_ref = shared_managers[selected].clone(); 199 | let index = selected; 200 | started_workflows[selected] = true; 201 | join_set.spawn(async move { 202 | let _ = execute_manager_workflow(manager_ref).await; 203 | index 204 | }); 205 | } 206 | } 207 | // Detail view navigation 208 | (AppState::DetailView(manager_index), KeyCode::Char('l')) => { 209 | app_state = AppState::LogsView(*manager_index); 210 | } 211 | ( 212 | AppState::DetailView(_) | AppState::LogsView(_), 213 | KeyCode::Char('h') | KeyCode::Left, 214 | ) => { 215 | app_state = AppState::ManagerList; 216 | } 217 | // Logs view scrolling 218 | (AppState::LogsView(manager_index), KeyCode::Up | KeyCode::Char('k')) => { 219 | if let Some(scroll_state) = logs_scroll_states.get_mut(*manager_index) { 220 | scroll_state.scroll_offset = 221 | scroll_state.scroll_offset.saturating_sub(1); 222 | } 223 | } 224 | (AppState::LogsView(manager_index), KeyCode::Down | KeyCode::Char('j')) => { 225 | if let Some(scroll_state) = logs_scroll_states.get_mut(*manager_index) { 226 | scroll_state.scroll_offset = 227 | scroll_state.scroll_offset.saturating_add(1); 228 | } 229 | } 230 | (AppState::LogsView(manager_index), KeyCode::PageUp) => { 231 | if let Some(scroll_state) = logs_scroll_states.get_mut(*manager_index) { 232 | scroll_state.scroll_offset = 233 | scroll_state.scroll_offset.saturating_sub(10); 234 | } 235 | } 236 | (AppState::LogsView(manager_index), KeyCode::PageDown) => { 237 | if let Some(scroll_state) = logs_scroll_states.get_mut(*manager_index) { 238 | scroll_state.scroll_offset = 239 | scroll_state.scroll_offset.saturating_add(10); 240 | } 241 | } 242 | (AppState::LogsView(manager_index), KeyCode::Home) => { 243 | if let Some(scroll_state) = logs_scroll_states.get_mut(*manager_index) { 244 | scroll_state.scroll_offset = 0; 245 | } 246 | } 247 | (AppState::LogsView(manager_index), KeyCode::End) => { 248 | if let Some(scroll_state) = logs_scroll_states.get_mut(*manager_index) { 249 | // Set to a high value - the render function will clamp it appropriately 250 | scroll_state.scroll_offset = u16::MAX; 251 | } 252 | } 253 | _ => {} 254 | } 255 | } 256 | } 257 | } 258 | 259 | // No auto-exit - let user decide when to quit 260 | } 261 | 262 | disable_raw_mode()?; 263 | execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 264 | terminal.show_cursor()?; 265 | 266 | // Only show summary if user didn't manually quit 267 | if !user_quit { 268 | let mut final_managers = Vec::new(); 269 | for m in shared_managers.iter() { 270 | final_managers.push(m.lock().await.clone()); 271 | } 272 | 273 | print_summary(&final_managers); 274 | } 275 | 276 | Ok(()) 277 | } 278 | 279 | fn ui( 280 | f: &mut Frame, 281 | managers_snapshot: &[DetectedManager], 282 | list_state: &mut ListState, 283 | app_state: &AppState, 284 | logs_scroll_states: &[LogsViewState], 285 | selective: bool, 286 | show_completion_message: bool, 287 | ) { 288 | match app_state { 289 | AppState::ManagerList => { 290 | render_manager_list( 291 | f, 292 | managers_snapshot, 293 | list_state, 294 | selective, 295 | show_completion_message, 296 | ); 297 | } 298 | AppState::DetailView(manager_index) => { 299 | if let Some(manager) = managers_snapshot.get(*manager_index) { 300 | render_detail_view(f, manager); 301 | } 302 | } 303 | AppState::LogsView(manager_index) => { 304 | if let Some(manager) = managers_snapshot.get(*manager_index) { 305 | if let Some(scroll_state) = logs_scroll_states.get(*manager_index) { 306 | render_logs_view(f, manager, scroll_state); 307 | } 308 | } 309 | } 310 | } 311 | } 312 | 313 | fn render_manager_list( 314 | f: &mut Frame, 315 | managers_snapshot: &[DetectedManager], 316 | list_state: &mut ListState, 317 | selective: bool, 318 | show_completion_message: bool, 319 | ) { 320 | let area = f.area().inner(Margin { 321 | horizontal: 2, 322 | vertical: 1, 323 | }); 324 | 325 | let chunks = Layout::default() 326 | .direction(Direction::Vertical) 327 | .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) 328 | .split(area); 329 | 330 | let items: Vec = managers_snapshot 331 | .iter() 332 | .map(|manager| { 333 | let status_style = match manager.status { 334 | ManagerStatus::Success => Style::default().fg(Color::Green), 335 | ManagerStatus::Failed(_) => Style::default().fg(Color::Red), 336 | _ => Style::default().fg(Color::Yellow), 337 | }; 338 | 339 | let status_text = match &manager.status { 340 | ManagerStatus::Pending => "Pending".to_string(), 341 | ManagerStatus::Running(operation) => format!("{operation}..."), 342 | ManagerStatus::Success => "✓ Complete".to_string(), 343 | ManagerStatus::Failed(_err) => "✗ Failed".to_string(), 344 | }; 345 | 346 | ListItem::new(Line::from(vec![ 347 | Span::styled(format!("{:<20}", manager.name), Style::default()), 348 | Span::styled(status_text, status_style), 349 | ])) 350 | }) 351 | .collect(); 352 | 353 | let list = List::new(items) 354 | .block( 355 | Block::default() 356 | .borders(Borders::ALL) 357 | .title("Package Managers - Spine"), 358 | ) 359 | .highlight_style(Style::default().add_modifier(Modifier::REVERSED)); 360 | 361 | f.render_stateful_widget(list, chunks[0], list_state); 362 | 363 | // Help text or completion message 364 | let help_text = if show_completion_message { 365 | Paragraph::new("All operations completed! Press 'q' to quit or navigate to view details.") 366 | .block(Block::default().borders(Borders::ALL).title("Status")) 367 | .style(Style::default().fg(Color::Green)) 368 | } else if selective { 369 | Paragraph::new("Navigate: ↑↓/j k | Start: Space | Detail: Enter | Quit: q") 370 | .block(Block::default().borders(Borders::ALL).title("Help")) 371 | .style(Style::default().fg(Color::Cyan)) 372 | } else { 373 | Paragraph::new("Navigate: ↑↓/j k | Detail: Enter | Quit: q") 374 | .block(Block::default().borders(Borders::ALL).title("Help")) 375 | .style(Style::default().fg(Color::Cyan)) 376 | }; 377 | 378 | f.render_widget(help_text, chunks[1]); 379 | } 380 | 381 | fn render_detail_view(f: &mut Frame, manager: &DetectedManager) { 382 | let area = f.area().inner(Margin { 383 | horizontal: 2, 384 | vertical: 1, 385 | }); 386 | 387 | let chunks = Layout::default() 388 | .direction(Direction::Vertical) 389 | .constraints( 390 | [ 391 | Constraint::Length(7), 392 | Constraint::Min(0), 393 | Constraint::Length(3), 394 | ] 395 | .as_ref(), 396 | ) 397 | .split(area); 398 | 399 | // Manager info block 400 | let info_text = format!( 401 | "Name: {}\nCheck Command: {}\nRefresh: {}\nSelf-Update: {}\nUpgrade: {}\nCleanup: {}", 402 | manager.config.name, 403 | manager.config.check_command, 404 | manager.config.refresh.as_deref().unwrap_or("N/A"), 405 | manager.config.self_update.as_deref().unwrap_or("N/A"), 406 | manager.config.upgrade_all, 407 | manager.config.cleanup.as_deref().unwrap_or("N/A") 408 | ); 409 | 410 | let info_block = Paragraph::new(info_text) 411 | .block( 412 | Block::default() 413 | .borders(Borders::ALL) 414 | .title("Manager Configuration"), 415 | ) 416 | .wrap(Wrap { trim: true }); 417 | 418 | f.render_widget(info_block, chunks[0]); 419 | 420 | // Status and logs 421 | let status_color = match manager.status { 422 | ManagerStatus::Success => Color::Green, 423 | ManagerStatus::Failed(_) => Color::Red, 424 | _ => Color::Yellow, 425 | }; 426 | 427 | let status_text = match &manager.status { 428 | ManagerStatus::Pending => "Status: Pending".to_string(), 429 | ManagerStatus::Running(operation) => { 430 | format!("Status: {operation}...") 431 | } 432 | ManagerStatus::Success => "Status: ✓ All operations completed successfully".to_string(), 433 | ManagerStatus::Failed(err) => format!("Status: ✗ Failed - {err}"), 434 | }; 435 | 436 | let status_block = Paragraph::new(Text::from(status_text)) 437 | .block(Block::default().borders(Borders::ALL).title("Status")) 438 | .style(Style::default().fg(status_color)) 439 | .wrap(Wrap { trim: true }); 440 | 441 | f.render_widget(status_block, chunks[1]); 442 | 443 | // Help text for detail view 444 | let help_text = Paragraph::new("Back: Esc/h/← | Logs: l | Quit: q") 445 | .block(Block::default().borders(Borders::ALL).title("Help")) 446 | .style(Style::default().fg(Color::Cyan)); 447 | 448 | f.render_widget(help_text, chunks[2]); 449 | } 450 | 451 | fn render_logs_view(f: &mut Frame, manager: &DetectedManager, scroll_state: &LogsViewState) { 452 | let area = f.area().inner(Margin { 453 | horizontal: 2, 454 | vertical: 1, 455 | }); 456 | 457 | let chunks = Layout::default() 458 | .direction(Direction::Vertical) 459 | .constraints( 460 | [ 461 | Constraint::Length(3), 462 | Constraint::Min(0), 463 | Constraint::Length(3), 464 | ] 465 | .as_ref(), 466 | ) 467 | .split(area); 468 | 469 | // Title block 470 | let title_text = format!("{} - Live Logs", manager.name); 471 | let title_block = Paragraph::new(title_text) 472 | .block(Block::default().borders(Borders::ALL).title("Logs")) 473 | .style(Style::default().fg(Color::Cyan)); 474 | 475 | f.render_widget(title_block, chunks[0]); 476 | 477 | // Raw logs content - show actual package manager output 478 | let logs_text = if manager.logs.is_empty() { 479 | match &manager.status { 480 | ManagerStatus::Pending => "Process not started yet...".to_string(), 481 | ManagerStatus::Running(_) => "No output yet...".to_string(), 482 | ManagerStatus::Success => { 483 | "Command completed successfully - no output captured".to_string() 484 | } 485 | ManagerStatus::Failed(err) => err.clone(), 486 | } 487 | } else { 488 | manager.logs.clone() 489 | }; 490 | 491 | let status_color = match manager.status { 492 | ManagerStatus::Success => Color::Green, 493 | ManagerStatus::Failed(_) => Color::Red, 494 | _ => Color::Yellow, 495 | }; 496 | 497 | // Calculate scroll bounds 498 | let content_height = logs_text.lines().count() as u16; 499 | let display_height = chunks[1].height.saturating_sub(2); // Subtract borders 500 | let max_scroll = content_height.saturating_sub(display_height); 501 | let scroll_offset = scroll_state.scroll_offset.min(max_scroll); 502 | 503 | let logs_block = Paragraph::new(Text::from(logs_text)) 504 | .block(Block::default().borders(Borders::ALL)) 505 | .style(Style::default().fg(status_color)) 506 | .wrap(Wrap { trim: true }) 507 | .scroll((scroll_offset, 0)); 508 | 509 | f.render_widget(logs_block, chunks[1]); 510 | 511 | // Help text for logs view with scroll indicator 512 | let scroll_indicator = if content_height > display_height { 513 | format!( 514 | " | Scroll: ↑↓/jk PgUp/PgDn Home/End ({}/{})", 515 | scroll_offset + 1, 516 | max_scroll + 1 517 | ) 518 | } else { 519 | String::new() 520 | }; 521 | 522 | let help_text = Paragraph::new(format!("Back: Esc/h/← | Quit: q{scroll_indicator}")) 523 | .block(Block::default().borders(Borders::ALL).title("Help")) 524 | .style(Style::default().fg(Color::Cyan)); 525 | 526 | f.render_widget(help_text, chunks[2]); 527 | } 528 | 529 | fn print_summary(managers: &[DetectedManager]) { 530 | let total = managers.len(); 531 | let successful = managers 532 | .iter() 533 | .filter(|m| matches!(m.status, ManagerStatus::Success)) 534 | .count(); 535 | let failed = managers 536 | .iter() 537 | .filter(|m| matches!(m.status, ManagerStatus::Failed(_))) 538 | .count(); 539 | let incomplete = total - successful - failed; 540 | 541 | println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 542 | println!(" SPINE UPGRADE SUMMARY"); 543 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 544 | 545 | println!("\nOverall Results:"); 546 | println!(" Total Managers: {total}"); 547 | println!( 548 | " ✓ Successful: {} ({:.1}%)", 549 | successful, 550 | (successful as f32 / total as f32) * 100.0 551 | ); 552 | println!( 553 | " ✗ Failed: {} ({:.1}%)", 554 | failed, 555 | (failed as f32 / total as f32) * 100.0 556 | ); 557 | 558 | if incomplete > 0 { 559 | println!( 560 | " ? Incomplete: {} ({:.1}%)", 561 | incomplete, 562 | (incomplete as f32 / total as f32) * 100.0 563 | ); 564 | } 565 | 566 | println!("\nDetailed Results:"); 567 | for manager in managers { 568 | match &manager.status { 569 | ManagerStatus::Success => { 570 | println!(" ✓ {:<20} Success", manager.name); 571 | } 572 | ManagerStatus::Failed(err) => { 573 | println!(" ✗ {:<20} Failed", manager.name); 574 | println!(" └─ Error: {err}"); 575 | } 576 | _ => { 577 | println!(" ? {:<20} Incomplete", manager.name); 578 | } 579 | } 580 | } 581 | 582 | if failed > 0 { 583 | println!("\n⚠️ Some package managers failed to upgrade completely."); 584 | println!(" Check the error details above and consider running 'spn upgrade' again."); 585 | println!(" You may also need to run the failed managers manually with sudo privileges."); 586 | } else if successful > 0 { 587 | println!("\n🎉 All package managers upgraded successfully!"); 588 | println!(" Your system is now up to date."); 589 | } 590 | 591 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 592 | } 593 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{Parser, Subcommand}; 3 | use indicatif::{ProgressBar, ProgressStyle}; 4 | use std::io; 5 | 6 | use crate::detect::{DetectedManager, ManagerStatus}; 7 | use crate::execute::execute_manager_workflow_simple; 8 | 9 | mod config; 10 | mod detect; 11 | mod execute; 12 | mod notify; 13 | mod tui; 14 | 15 | #[derive(Parser)] 16 | #[command(name = "spn")] 17 | #[command(about = "A meta package manager for Unix-like systems")] 18 | struct Cli { 19 | #[command(subcommand)] 20 | command: Commands, 21 | } 22 | 23 | #[derive(Subcommand)] 24 | enum Commands { 25 | #[command(about = "Upgrade all package managers")] 26 | Upgrade { 27 | #[arg( 28 | short, 29 | long, 30 | help = "Selective mode - wait for user to select which managers to update" 31 | )] 32 | selective: bool, 33 | #[arg( 34 | short = 'n', 35 | long = "no-tui", 36 | help = "Non-TUI mode - use spinners instead of interactive interface" 37 | )] 38 | no_tui: bool, 39 | #[arg(long, help = "Send notification when upgrade completes")] 40 | notify: bool, 41 | }, 42 | #[command(about = "List detected package managers")] 43 | List, 44 | #[command(about = "Enable or disable automatic background updates")] 45 | Auto { 46 | #[arg(long, help = "Enable automatic updates")] 47 | enable: bool, 48 | #[arg(long, help = "Disable automatic updates")] 49 | disable: bool, 50 | #[arg(long, help = "Show current auto-update status")] 51 | status: bool, 52 | }, 53 | } 54 | 55 | #[tokio::main] 56 | async fn main() -> Result<()> { 57 | let cli = Cli::parse(); 58 | 59 | match cli.command { 60 | Commands::Upgrade { 61 | selective, 62 | no_tui, 63 | notify, 64 | } => { 65 | upgrade(selective, no_tui, notify).await?; 66 | } 67 | Commands::List => { 68 | list_managers().await?; 69 | } 70 | Commands::Auto { 71 | enable, 72 | disable, 73 | status, 74 | } => { 75 | manage_auto_update(enable, disable, status).await?; 76 | } 77 | } 78 | 79 | Ok(()) 80 | } 81 | 82 | async fn list_managers() -> Result<()> { 83 | let config = match config::load_config().await { 84 | Ok(config) => config, 85 | Err(e) => { 86 | eprintln!("Error loading configuration: {e}"); 87 | eprintln!("Please ensure backbone.toml is available in the current directory or installed with the binary."); 88 | std::process::exit(1); 89 | } 90 | }; 91 | 92 | let managers = match detect::detect_package_managers(&config).await { 93 | Ok(managers) => managers, 94 | Err(e) => { 95 | eprintln!("Error detecting package managers: {e}"); 96 | std::process::exit(1); 97 | } 98 | }; 99 | 100 | if managers.is_empty() { 101 | println!("No package managers detected on this system."); 102 | println!( 103 | "Spine checked for: {}", 104 | config 105 | .managers 106 | .keys() 107 | .cloned() 108 | .collect::>() 109 | .join(", ") 110 | ); 111 | return Ok(()); 112 | } 113 | 114 | println!("Detected {} package manager(s):", managers.len()); 115 | for manager in &managers { 116 | println!(" ✓ {} ({})", manager.name, manager.config.name); 117 | println!(" Check command: {}", manager.config.check_command); 118 | println!(" Requires sudo: {}", manager.config.requires_sudo); 119 | println!(); 120 | } 121 | 122 | Ok(()) 123 | } 124 | 125 | async fn upgrade(selective: bool, no_tui: bool, notify_on_complete: bool) -> Result<()> { 126 | // Load configuration with error handling 127 | let config = match config::load_config().await { 128 | Ok(config) => config, 129 | Err(e) => { 130 | eprintln!("Error loading configuration: {e}"); 131 | eprintln!("Please ensure backbone.toml is available in the current directory or installed with the binary."); 132 | std::process::exit(1); 133 | } 134 | }; 135 | 136 | // Check for sudo availability if any managers require it 137 | let requires_sudo = config.managers.values().any(|m| m.requires_sudo); 138 | if requires_sudo { 139 | match execute::check_sudo_availability().await { 140 | true => {} 141 | false => { 142 | eprintln!("Warning: Some package managers require sudo access."); 143 | eprintln!("Please ensure you have the necessary privileges or run with sudo."); 144 | eprintln!("Continuing anyway - some operations may fail...\n"); 145 | } 146 | } 147 | } 148 | 149 | // Detect available package managers 150 | let managers = match detect::detect_package_managers(&config).await { 151 | Ok(managers) => managers, 152 | Err(e) => { 153 | eprintln!("Error detecting package managers: {e}"); 154 | std::process::exit(1); 155 | } 156 | }; 157 | 158 | if managers.is_empty() { 159 | println!("No package managers detected on this system."); 160 | println!( 161 | "Spine checked for: {}", 162 | config 163 | .managers 164 | .keys() 165 | .cloned() 166 | .collect::>() 167 | .join(", ") 168 | ); 169 | return Ok(()); 170 | } 171 | 172 | println!( 173 | "Detected {} package manager(s): {}", 174 | managers.len(), 175 | managers 176 | .iter() 177 | .map(|m| &m.name) 178 | .cloned() 179 | .collect::>() 180 | .join(", ") 181 | ); 182 | println!("Starting upgrade process...\n"); 183 | 184 | // Choose between TUI and non-TUI workflow 185 | let result = if no_tui { 186 | run_spinner_upgrade(managers, selective).await 187 | } else { 188 | tui::run_tui(managers, config, selective).await 189 | }; 190 | 191 | match result { 192 | Ok(()) => { 193 | println!("Upgrade process completed."); 194 | if notify_on_complete { 195 | let _ = notify::send_notification( 196 | "Spine Update Complete", 197 | "All package managers have been updated successfully.", 198 | ); 199 | } 200 | } 201 | Err(e) => { 202 | eprintln!("Error during upgrade process: {e}"); 203 | if notify_on_complete { 204 | let _ = notify::send_notification( 205 | "Spine Update Failed", 206 | "Package manager updates encountered errors.", 207 | ); 208 | } 209 | std::process::exit(1); 210 | } 211 | } 212 | 213 | Ok(()) 214 | } 215 | 216 | async fn run_spinner_upgrade(mut managers: Vec, selective: bool) -> Result<()> { 217 | println!("Running package manager upgrades...\n"); 218 | 219 | if selective { 220 | // In selective mode, prompt for each manager 221 | let mut i = 0; 222 | while i < managers.len() { 223 | println!("Run upgrade for {} (y/N)?", managers[i].name); 224 | let mut input = String::new(); 225 | io::stdin().read_line(&mut input)?; 226 | 227 | if input.trim().to_lowercase() == "y" || input.trim().to_lowercase() == "yes" { 228 | run_manager_with_spinner(&mut managers[i]).await?; 229 | } else { 230 | println!("Skipping {}\n", managers[i].name); 231 | } 232 | i += 1; 233 | } 234 | } else { 235 | // Run all managers sequentially 236 | for manager in managers.iter_mut() { 237 | run_manager_with_spinner(manager).await?; 238 | } 239 | } 240 | 241 | // Print summary using the same function as TUI 242 | print_spinner_summary(&managers); 243 | 244 | Ok(()) 245 | } 246 | 247 | async fn run_manager_with_spinner(manager: &mut DetectedManager) -> Result<()> { 248 | let pb = ProgressBar::new_spinner(); 249 | pb.set_style( 250 | ProgressStyle::default_spinner() 251 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") 252 | .template("{spinner:.green} {msg}")?, 253 | ); 254 | 255 | pb.set_message(format!("Starting {}", manager.name)); 256 | pb.enable_steady_tick(std::time::Duration::from_millis(100)); 257 | 258 | // Execute the manager workflow 259 | let result = execute_manager_workflow_simple(manager).await; 260 | 261 | pb.finish_with_message(match &manager.status { 262 | ManagerStatus::Success => format!("✓ {} completed successfully", manager.name), 263 | ManagerStatus::Failed(err) => format!("✗ {} failed: {}", manager.name, err), 264 | _ => format!("? {} finished with unknown status", manager.name), 265 | }); 266 | 267 | println!(); 268 | 269 | result 270 | } 271 | 272 | fn print_spinner_summary(managers: &[DetectedManager]) { 273 | let total = managers.len(); 274 | let successful = managers 275 | .iter() 276 | .filter(|m| matches!(m.status, ManagerStatus::Success)) 277 | .count(); 278 | let failed = managers 279 | .iter() 280 | .filter(|m| matches!(m.status, ManagerStatus::Failed(_))) 281 | .count(); 282 | 283 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 284 | println!(" SPINE UPGRADE SUMMARY"); 285 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 286 | 287 | println!("\nOverall Results:"); 288 | println!(" Total Managers: {total}"); 289 | println!( 290 | " ✓ Successful: {} ({:.1}%)", 291 | successful, 292 | (successful as f32 / total as f32) * 100.0 293 | ); 294 | println!( 295 | " ✗ Failed: {} ({:.1}%)", 296 | failed, 297 | (failed as f32 / total as f32) * 100.0 298 | ); 299 | 300 | println!("\nDetailed Results:"); 301 | for manager in managers { 302 | match &manager.status { 303 | ManagerStatus::Success => { 304 | println!(" ✓ {:<20} Success", manager.name); 305 | } 306 | ManagerStatus::Failed(err) => { 307 | println!(" ✗ {:<20} Failed", manager.name); 308 | println!(" └─ Error: {err}"); 309 | } 310 | _ => { 311 | println!(" ? {:<20} Incomplete", manager.name); 312 | } 313 | } 314 | } 315 | 316 | if failed > 0 { 317 | println!("\n⚠️ Some package managers failed to upgrade completely."); 318 | println!(" Check the error details above and consider running 'spn upgrade' again."); 319 | println!(" You may also need to run the failed managers manually with sudo privileges."); 320 | } else if successful > 0 { 321 | println!("\n🎉 All package managers upgraded successfully!"); 322 | println!(" Your system is now up to date."); 323 | } 324 | 325 | println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 326 | } 327 | 328 | async fn manage_auto_update(enable: bool, disable: bool, status_only: bool) -> Result<()> { 329 | let config = config::load_config().await?; 330 | 331 | if status_only { 332 | print_auto_update_status(&config); 333 | return Ok(()); 334 | } 335 | 336 | if !enable && !disable { 337 | print_auto_update_status(&config); 338 | eprintln!("\nUse --enable or --disable to change settings"); 339 | eprintln!("Edit ~/.config/spine/backbone.toml to configure schedule"); 340 | return Ok(()); 341 | } 342 | 343 | if enable { 344 | enable_auto_update(&config).await?; 345 | } else if disable { 346 | disable_auto_update().await?; 347 | } 348 | 349 | Ok(()) 350 | } 351 | 352 | fn print_auto_update_status(config: &config::Config) { 353 | println!("Auto-Update Status:"); 354 | println!( 355 | " Enabled: {}", 356 | if config.auto_update.enabled { 357 | "✓ Yes" 358 | } else { 359 | "✗ No" 360 | } 361 | ); 362 | println!(" Schedule: {}", config.auto_update.schedule); 363 | 364 | if config.auto_update.schedule == "daily" { 365 | println!(" Time: {}", config.auto_update.time); 366 | } else { 367 | println!(" Day: {}", config.auto_update.day); 368 | println!(" Time: 18:00"); 369 | } 370 | 371 | println!( 372 | " Notifications: {}", 373 | if config.auto_update.notify { 374 | "✓ Enabled" 375 | } else { 376 | "✗ Disabled" 377 | } 378 | ); 379 | println!( 380 | " Mode: {}", 381 | if config.auto_update.no_tui { 382 | "Background" 383 | } else { 384 | "Interactive" 385 | } 386 | ); 387 | } 388 | 389 | async fn enable_auto_update(config: &config::Config) -> Result<()> { 390 | let binary_path = std::env::current_exe()?; 391 | 392 | if config.auto_update.schedule == "daily" { 393 | setup_daily_auto_update( 394 | &config.auto_update.time, 395 | &binary_path, 396 | config.auto_update.notify, 397 | )?; 398 | println!( 399 | "✓ Enabled automatic daily updates at {}", 400 | config.auto_update.time 401 | ); 402 | } else { 403 | setup_weekly_auto_update( 404 | &config.auto_update.day, 405 | &binary_path, 406 | config.auto_update.notify, 407 | )?; 408 | println!( 409 | "✓ Enabled automatic weekly updates on {}", 410 | config.auto_update.day 411 | ); 412 | } 413 | 414 | println!("\nUpdates will run in the background."); 415 | if config.auto_update.notify { 416 | println!("You'll receive a notification when complete."); 417 | } 418 | 419 | Ok(()) 420 | } 421 | 422 | async fn disable_auto_update() -> Result<()> { 423 | remove_auto_update_schedule()?; 424 | println!("✓ Disabled automatic updates"); 425 | Ok(()) 426 | } 427 | 428 | #[cfg(target_os = "macos")] 429 | fn setup_daily_auto_update(time: &str, binary_path: &std::path::Path, notify: bool) -> Result<()> { 430 | use std::env; 431 | use std::fs; 432 | 433 | let parts: Vec<&str> = time.split(':').collect(); 434 | if parts.len() != 2 { 435 | anyhow::bail!("Invalid time format. Use HH:MM (e.g., 18:00)"); 436 | } 437 | 438 | let hour = parts[0]; 439 | let minute = parts[1]; 440 | 441 | let notify_flag = if notify { " --notify" } else { "" }; 442 | let binary_path_str = binary_path.to_string_lossy(); 443 | 444 | let plist_content = format!( 445 | r#" 446 | 447 | 448 | 449 | Label 450 | com.spine.auto-update 451 | ProgramArguments 452 | 453 | {binary_path_str} 454 | upgrade 455 | --no-tui{notify_flag} 456 | 457 | StartCalendarInterval 458 | 459 | Hour 460 | {hour} 461 | Minute 462 | {minute} 463 | 464 | StandardOutPath 465 | /tmp/spine-auto-update.log 466 | StandardErrorPath 467 | /tmp/spine-auto-update-error.log 468 | 469 | "# 470 | ); 471 | 472 | let home = env::var("HOME")?; 473 | let plist_path = format!("{home}/Library/LaunchAgents/com.spine.auto-update.plist"); 474 | fs::write(&plist_path, plist_content)?; 475 | 476 | std::process::Command::new("launchctl") 477 | .args(["load", "-w", &plist_path]) 478 | .output()?; 479 | 480 | Ok(()) 481 | } 482 | 483 | #[cfg(target_os = "linux")] 484 | fn setup_daily_auto_update(time: &str, binary_path: &std::path::Path, notify: bool) -> Result<()> { 485 | let parts: Vec<&str> = time.split(':').collect(); 486 | if parts.len() != 2 { 487 | anyhow::bail!("Invalid time format. Use HH:MM (e.g., 18:00)"); 488 | } 489 | 490 | let hour = parts[0]; 491 | let minute = parts[1]; 492 | 493 | let notify_flag = if notify { " --notify" } else { "" }; 494 | let binary_path_str = binary_path.to_string_lossy(); 495 | 496 | let cron_entry = format!( 497 | "{minute} {hour} * * * {binary_path_str} upgrade --no-tui{notify_flag} >> /tmp/spine-auto-update.log 2>&1\n" 498 | ); 499 | 500 | let output = std::process::Command::new("crontab").arg("-l").output(); 501 | 502 | let mut current_crontab = if output.is_ok() { 503 | String::from_utf8_lossy(&output.unwrap().stdout).to_string() 504 | } else { 505 | String::new() 506 | }; 507 | 508 | current_crontab = current_crontab 509 | .lines() 510 | .filter(|line| !line.contains("spine") && !line.contains("spn")) 511 | .collect::>() 512 | .join("\n"); 513 | 514 | if !current_crontab.is_empty() && !current_crontab.ends_with('\n') { 515 | current_crontab.push('\n'); 516 | } 517 | current_crontab.push_str(&cron_entry); 518 | 519 | let mut child = std::process::Command::new("crontab") 520 | .arg("-") 521 | .stdin(std::process::Stdio::piped()) 522 | .spawn()?; 523 | 524 | use std::io::Write; 525 | child 526 | .stdin 527 | .as_mut() 528 | .unwrap() 529 | .write_all(current_crontab.as_bytes())?; 530 | child.wait()?; 531 | 532 | Ok(()) 533 | } 534 | 535 | #[cfg(not(any(target_os = "macos", target_os = "linux")))] 536 | fn setup_daily_auto_update( 537 | _time: &str, 538 | _binary_path: &std::path::Path, 539 | _notify: bool, 540 | ) -> Result<()> { 541 | anyhow::bail!("Auto-update is only supported on macOS and Linux") 542 | } 543 | 544 | #[cfg(target_os = "macos")] 545 | fn setup_weekly_auto_update(day: &str, binary_path: &std::path::Path, notify: bool) -> Result<()> { 546 | let weekday = match day.to_lowercase().as_str() { 547 | "monday" => 1, 548 | "tuesday" => 2, 549 | "wednesday" => 3, 550 | "thursday" => 4, 551 | "friday" => 5, 552 | "saturday" => 6, 553 | "sunday" => 7, 554 | _ => anyhow::bail!( 555 | "Invalid day. Use: monday, tuesday, wednesday, thursday, friday, saturday, sunday" 556 | ), 557 | }; 558 | 559 | let notify_flag = if notify { " --notify" } else { "" }; 560 | let binary_path_str = binary_path.to_string_lossy(); 561 | 562 | let plist_content = format!( 563 | r#" 564 | 565 | 566 | 567 | Label 568 | com.spine.auto-update 569 | ProgramArguments 570 | 571 | {binary_path_str} 572 | upgrade 573 | --no-tui{notify_flag} 574 | 575 | StartCalendarInterval 576 | 577 | Weekday 578 | {weekday} 579 | Hour 580 | 18 581 | Minute 582 | 0 583 | 584 | StandardOutPath 585 | /tmp/spine-auto-update.log 586 | StandardErrorPath 587 | /tmp/spine-auto-update-error.log 588 | 589 | "# 590 | ); 591 | 592 | use std::env; 593 | use std::fs; 594 | let home = env::var("HOME")?; 595 | let plist_path = format!("{home}/Library/LaunchAgents/com.spine.auto-update.plist"); 596 | fs::write(&plist_path, plist_content)?; 597 | 598 | std::process::Command::new("launchctl") 599 | .args(["load", "-w", &plist_path]) 600 | .output()?; 601 | 602 | Ok(()) 603 | } 604 | 605 | #[cfg(target_os = "linux")] 606 | fn setup_weekly_auto_update(day: &str, binary_path: &std::path::Path, notify: bool) -> Result<()> { 607 | let weekday = match day.to_lowercase().as_str() { 608 | "monday" => "1", 609 | "tuesday" => "2", 610 | "wednesday" => "3", 611 | "thursday" => "4", 612 | "friday" => "5", 613 | "saturday" => "6", 614 | "sunday" => "0", 615 | _ => anyhow::bail!( 616 | "Invalid day. Use: monday, tuesday, wednesday, thursday, friday, saturday, sunday" 617 | ), 618 | }; 619 | 620 | let notify_flag = if notify { " --notify" } else { "" }; 621 | let binary_path_str = binary_path.to_string_lossy(); 622 | 623 | let cron_entry = format!( 624 | "0 18 * * {weekday} {binary_path_str} upgrade --no-tui{notify_flag} >> /tmp/spine-auto-update.log 2>&1\n" 625 | ); 626 | 627 | let output = std::process::Command::new("crontab").arg("-l").output(); 628 | 629 | let mut current_crontab = if output.is_ok() { 630 | String::from_utf8_lossy(&output.unwrap().stdout).to_string() 631 | } else { 632 | String::new() 633 | }; 634 | 635 | current_crontab = current_crontab 636 | .lines() 637 | .filter(|line| !line.contains("spine") && !line.contains("spn")) 638 | .collect::>() 639 | .join("\n"); 640 | 641 | if !current_crontab.is_empty() && !current_crontab.ends_with('\n') { 642 | current_crontab.push('\n'); 643 | } 644 | current_crontab.push_str(&cron_entry); 645 | 646 | let mut child = std::process::Command::new("crontab") 647 | .arg("-") 648 | .stdin(std::process::Stdio::piped()) 649 | .spawn()?; 650 | 651 | use std::io::Write; 652 | child 653 | .stdin 654 | .as_mut() 655 | .unwrap() 656 | .write_all(current_crontab.as_bytes())?; 657 | child.wait()?; 658 | 659 | Ok(()) 660 | } 661 | 662 | #[cfg(not(any(target_os = "macos", target_os = "linux")))] 663 | fn setup_weekly_auto_update( 664 | _day: &str, 665 | _binary_path: &std::path::Path, 666 | _notify: bool, 667 | ) -> Result<()> { 668 | anyhow::bail!("Auto-update is only supported on macOS and Linux") 669 | } 670 | 671 | #[cfg(target_os = "macos")] 672 | fn remove_auto_update_schedule() -> Result<()> { 673 | use std::env; 674 | let home = env::var("HOME")?; 675 | 676 | let plist_path = format!("{home}/Library/LaunchAgents/com.spine.auto-update.plist"); 677 | 678 | if std::path::Path::new(&plist_path).exists() { 679 | let _ = std::process::Command::new("launchctl") 680 | .args(["unload", &plist_path]) 681 | .output(); 682 | let _ = std::fs::remove_file(&plist_path); 683 | } 684 | 685 | Ok(()) 686 | } 687 | 688 | #[cfg(target_os = "linux")] 689 | fn remove_auto_update_schedule() -> Result<()> { 690 | let output = std::process::Command::new("crontab").arg("-l").output(); 691 | 692 | if output.is_ok() { 693 | let current_crontab = String::from_utf8_lossy(&output.unwrap().stdout); 694 | let filtered: String = current_crontab 695 | .lines() 696 | .filter(|line| !line.contains("spine") && !line.contains("spn")) 697 | .collect::>() 698 | .join("\n"); 699 | 700 | let mut child = std::process::Command::new("crontab") 701 | .arg("-") 702 | .stdin(std::process::Stdio::piped()) 703 | .spawn()?; 704 | 705 | use std::io::Write; 706 | child 707 | .stdin 708 | .as_mut() 709 | .unwrap() 710 | .write_all(filtered.as_bytes())?; 711 | child.wait()?; 712 | } 713 | 714 | Ok(()) 715 | } 716 | 717 | #[cfg(not(any(target_os = "macos", target_os = "linux")))] 718 | fn remove_auto_update_schedule() -> Result<()> { 719 | anyhow::bail!("Auto-update is only supported on macOS and Linux") 720 | } 721 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "allocator-api2" 22 | version = "0.2.21" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 25 | 26 | [[package]] 27 | name = "anstream" 28 | version = "0.6.18" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 31 | dependencies = [ 32 | "anstyle", 33 | "anstyle-parse", 34 | "anstyle-query", 35 | "anstyle-wincon", 36 | "colorchoice", 37 | "is_terminal_polyfill", 38 | "utf8parse", 39 | ] 40 | 41 | [[package]] 42 | name = "anstyle" 43 | version = "1.0.10" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 46 | 47 | [[package]] 48 | name = "anstyle-parse" 49 | version = "0.2.6" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 52 | dependencies = [ 53 | "utf8parse", 54 | ] 55 | 56 | [[package]] 57 | name = "anstyle-query" 58 | version = "1.1.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 61 | dependencies = [ 62 | "windows-sys 0.59.0", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-wincon" 67 | version = "3.0.8" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 70 | dependencies = [ 71 | "anstyle", 72 | "once_cell_polyfill", 73 | "windows-sys 0.59.0", 74 | ] 75 | 76 | [[package]] 77 | name = "anyhow" 78 | version = "1.0.98" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 81 | 82 | [[package]] 83 | name = "autocfg" 84 | version = "1.4.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 87 | 88 | [[package]] 89 | name = "backtrace" 90 | version = "0.3.75" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 93 | dependencies = [ 94 | "addr2line", 95 | "cfg-if", 96 | "libc", 97 | "miniz_oxide", 98 | "object", 99 | "rustc-demangle", 100 | "windows-targets", 101 | ] 102 | 103 | [[package]] 104 | name = "bitflags" 105 | version = "2.9.1" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 108 | 109 | [[package]] 110 | name = "bumpalo" 111 | version = "3.17.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 114 | 115 | [[package]] 116 | name = "bytes" 117 | version = "1.10.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 120 | 121 | [[package]] 122 | name = "cassowary" 123 | version = "0.3.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 126 | 127 | [[package]] 128 | name = "castaway" 129 | version = "0.2.3" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 132 | dependencies = [ 133 | "rustversion", 134 | ] 135 | 136 | [[package]] 137 | name = "cfg-if" 138 | version = "1.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 141 | 142 | [[package]] 143 | name = "clap" 144 | version = "4.5.39" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 147 | dependencies = [ 148 | "clap_builder", 149 | "clap_derive", 150 | ] 151 | 152 | [[package]] 153 | name = "clap_builder" 154 | version = "4.5.39" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 157 | dependencies = [ 158 | "anstream", 159 | "anstyle", 160 | "clap_lex", 161 | "strsim", 162 | ] 163 | 164 | [[package]] 165 | name = "clap_derive" 166 | version = "4.5.32" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 169 | dependencies = [ 170 | "heck", 171 | "proc-macro2", 172 | "quote", 173 | "syn", 174 | ] 175 | 176 | [[package]] 177 | name = "clap_lex" 178 | version = "0.7.4" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 181 | 182 | [[package]] 183 | name = "colorchoice" 184 | version = "1.0.3" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 187 | 188 | [[package]] 189 | name = "compact_str" 190 | version = "0.8.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 193 | dependencies = [ 194 | "castaway", 195 | "cfg-if", 196 | "itoa", 197 | "rustversion", 198 | "ryu", 199 | "static_assertions", 200 | ] 201 | 202 | [[package]] 203 | name = "console" 204 | version = "0.15.11" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 207 | dependencies = [ 208 | "encode_unicode", 209 | "libc", 210 | "once_cell", 211 | "unicode-width 0.2.0", 212 | "windows-sys 0.59.0", 213 | ] 214 | 215 | [[package]] 216 | name = "convert_case" 217 | version = "0.7.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 220 | dependencies = [ 221 | "unicode-segmentation", 222 | ] 223 | 224 | [[package]] 225 | name = "crossterm" 226 | version = "0.28.1" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 229 | dependencies = [ 230 | "bitflags", 231 | "crossterm_winapi", 232 | "mio", 233 | "parking_lot", 234 | "rustix 0.38.44", 235 | "signal-hook", 236 | "signal-hook-mio", 237 | "winapi", 238 | ] 239 | 240 | [[package]] 241 | name = "crossterm" 242 | version = "0.29.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 245 | dependencies = [ 246 | "bitflags", 247 | "crossterm_winapi", 248 | "derive_more", 249 | "document-features", 250 | "mio", 251 | "parking_lot", 252 | "rustix 1.0.7", 253 | "signal-hook", 254 | "signal-hook-mio", 255 | "winapi", 256 | ] 257 | 258 | [[package]] 259 | name = "crossterm_winapi" 260 | version = "0.9.1" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 263 | dependencies = [ 264 | "winapi", 265 | ] 266 | 267 | [[package]] 268 | name = "darling" 269 | version = "0.20.11" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 272 | dependencies = [ 273 | "darling_core", 274 | "darling_macro", 275 | ] 276 | 277 | [[package]] 278 | name = "darling_core" 279 | version = "0.20.11" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 282 | dependencies = [ 283 | "fnv", 284 | "ident_case", 285 | "proc-macro2", 286 | "quote", 287 | "strsim", 288 | "syn", 289 | ] 290 | 291 | [[package]] 292 | name = "darling_macro" 293 | version = "0.20.11" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 296 | dependencies = [ 297 | "darling_core", 298 | "quote", 299 | "syn", 300 | ] 301 | 302 | [[package]] 303 | name = "derive_more" 304 | version = "2.0.1" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 307 | dependencies = [ 308 | "derive_more-impl", 309 | ] 310 | 311 | [[package]] 312 | name = "derive_more-impl" 313 | version = "2.0.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 316 | dependencies = [ 317 | "convert_case", 318 | "proc-macro2", 319 | "quote", 320 | "syn", 321 | ] 322 | 323 | [[package]] 324 | name = "dirs" 325 | version = "6.0.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 328 | dependencies = [ 329 | "dirs-sys", 330 | ] 331 | 332 | [[package]] 333 | name = "dirs-sys" 334 | version = "0.5.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 337 | dependencies = [ 338 | "libc", 339 | "option-ext", 340 | "redox_users", 341 | "windows-sys 0.59.0", 342 | ] 343 | 344 | [[package]] 345 | name = "document-features" 346 | version = "0.2.11" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 349 | dependencies = [ 350 | "litrs", 351 | ] 352 | 353 | [[package]] 354 | name = "either" 355 | version = "1.15.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 358 | 359 | [[package]] 360 | name = "encode_unicode" 361 | version = "1.0.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 364 | 365 | [[package]] 366 | name = "env_home" 367 | version = "0.1.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" 370 | 371 | [[package]] 372 | name = "equivalent" 373 | version = "1.0.2" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 376 | 377 | [[package]] 378 | name = "errno" 379 | version = "0.3.12" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 382 | dependencies = [ 383 | "libc", 384 | "windows-sys 0.59.0", 385 | ] 386 | 387 | [[package]] 388 | name = "fnv" 389 | version = "1.0.7" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 392 | 393 | [[package]] 394 | name = "foldhash" 395 | version = "0.1.5" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 398 | 399 | [[package]] 400 | name = "getrandom" 401 | version = "0.2.16" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 404 | dependencies = [ 405 | "cfg-if", 406 | "libc", 407 | "wasi", 408 | ] 409 | 410 | [[package]] 411 | name = "gimli" 412 | version = "0.31.1" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 415 | 416 | [[package]] 417 | name = "hashbrown" 418 | version = "0.15.3" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 421 | dependencies = [ 422 | "allocator-api2", 423 | "equivalent", 424 | "foldhash", 425 | ] 426 | 427 | [[package]] 428 | name = "heck" 429 | version = "0.5.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 432 | 433 | [[package]] 434 | name = "ident_case" 435 | version = "1.0.1" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 438 | 439 | [[package]] 440 | name = "indexmap" 441 | version = "2.9.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 444 | dependencies = [ 445 | "equivalent", 446 | "hashbrown", 447 | ] 448 | 449 | [[package]] 450 | name = "indicatif" 451 | version = "0.17.11" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 454 | dependencies = [ 455 | "console", 456 | "number_prefix", 457 | "portable-atomic", 458 | "unicode-width 0.2.0", 459 | "web-time", 460 | ] 461 | 462 | [[package]] 463 | name = "indoc" 464 | version = "2.0.6" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 467 | 468 | [[package]] 469 | name = "instability" 470 | version = "0.3.7" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" 473 | dependencies = [ 474 | "darling", 475 | "indoc", 476 | "proc-macro2", 477 | "quote", 478 | "syn", 479 | ] 480 | 481 | [[package]] 482 | name = "is_terminal_polyfill" 483 | version = "1.70.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 486 | 487 | [[package]] 488 | name = "itertools" 489 | version = "0.13.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 492 | dependencies = [ 493 | "either", 494 | ] 495 | 496 | [[package]] 497 | name = "itoa" 498 | version = "1.0.15" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 501 | 502 | [[package]] 503 | name = "js-sys" 504 | version = "0.3.77" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 507 | dependencies = [ 508 | "once_cell", 509 | "wasm-bindgen", 510 | ] 511 | 512 | [[package]] 513 | name = "libc" 514 | version = "0.2.172" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 517 | 518 | [[package]] 519 | name = "libredox" 520 | version = "0.1.3" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 523 | dependencies = [ 524 | "bitflags", 525 | "libc", 526 | ] 527 | 528 | [[package]] 529 | name = "linux-raw-sys" 530 | version = "0.4.15" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 533 | 534 | [[package]] 535 | name = "linux-raw-sys" 536 | version = "0.9.4" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 539 | 540 | [[package]] 541 | name = "litrs" 542 | version = "0.4.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 545 | 546 | [[package]] 547 | name = "lock_api" 548 | version = "0.4.12" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 551 | dependencies = [ 552 | "autocfg", 553 | "scopeguard", 554 | ] 555 | 556 | [[package]] 557 | name = "log" 558 | version = "0.4.27" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 561 | 562 | [[package]] 563 | name = "lru" 564 | version = "0.12.5" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 567 | dependencies = [ 568 | "hashbrown", 569 | ] 570 | 571 | [[package]] 572 | name = "memchr" 573 | version = "2.7.4" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 576 | 577 | [[package]] 578 | name = "miniz_oxide" 579 | version = "0.8.8" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 582 | dependencies = [ 583 | "adler2", 584 | ] 585 | 586 | [[package]] 587 | name = "mio" 588 | version = "1.0.4" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 591 | dependencies = [ 592 | "libc", 593 | "log", 594 | "wasi", 595 | "windows-sys 0.59.0", 596 | ] 597 | 598 | [[package]] 599 | name = "number_prefix" 600 | version = "0.4.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 603 | 604 | [[package]] 605 | name = "object" 606 | version = "0.36.7" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 609 | dependencies = [ 610 | "memchr", 611 | ] 612 | 613 | [[package]] 614 | name = "once_cell" 615 | version = "1.21.3" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 618 | 619 | [[package]] 620 | name = "once_cell_polyfill" 621 | version = "1.70.1" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 624 | 625 | [[package]] 626 | name = "option-ext" 627 | version = "0.2.0" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 630 | 631 | [[package]] 632 | name = "parking_lot" 633 | version = "0.12.3" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 636 | dependencies = [ 637 | "lock_api", 638 | "parking_lot_core", 639 | ] 640 | 641 | [[package]] 642 | name = "parking_lot_core" 643 | version = "0.9.10" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 646 | dependencies = [ 647 | "cfg-if", 648 | "libc", 649 | "redox_syscall", 650 | "smallvec", 651 | "windows-targets", 652 | ] 653 | 654 | [[package]] 655 | name = "paste" 656 | version = "1.0.15" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 659 | 660 | [[package]] 661 | name = "pin-project-lite" 662 | version = "0.2.16" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 665 | 666 | [[package]] 667 | name = "portable-atomic" 668 | version = "1.11.0" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 671 | 672 | [[package]] 673 | name = "proc-macro2" 674 | version = "1.0.95" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 677 | dependencies = [ 678 | "unicode-ident", 679 | ] 680 | 681 | [[package]] 682 | name = "quote" 683 | version = "1.0.40" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 686 | dependencies = [ 687 | "proc-macro2", 688 | ] 689 | 690 | [[package]] 691 | name = "ratatui" 692 | version = "0.29.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 695 | dependencies = [ 696 | "bitflags", 697 | "cassowary", 698 | "compact_str", 699 | "crossterm 0.28.1", 700 | "indoc", 701 | "instability", 702 | "itertools", 703 | "lru", 704 | "paste", 705 | "strum", 706 | "unicode-segmentation", 707 | "unicode-truncate", 708 | "unicode-width 0.2.0", 709 | ] 710 | 711 | [[package]] 712 | name = "redox_syscall" 713 | version = "0.5.12" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 716 | dependencies = [ 717 | "bitflags", 718 | ] 719 | 720 | [[package]] 721 | name = "redox_users" 722 | version = "0.5.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 725 | dependencies = [ 726 | "getrandom", 727 | "libredox", 728 | "thiserror", 729 | ] 730 | 731 | [[package]] 732 | name = "rustc-demangle" 733 | version = "0.1.24" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 736 | 737 | [[package]] 738 | name = "rustix" 739 | version = "0.38.44" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 742 | dependencies = [ 743 | "bitflags", 744 | "errno", 745 | "libc", 746 | "linux-raw-sys 0.4.15", 747 | "windows-sys 0.59.0", 748 | ] 749 | 750 | [[package]] 751 | name = "rustix" 752 | version = "1.0.7" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 755 | dependencies = [ 756 | "bitflags", 757 | "errno", 758 | "libc", 759 | "linux-raw-sys 0.9.4", 760 | "windows-sys 0.59.0", 761 | ] 762 | 763 | [[package]] 764 | name = "rustversion" 765 | version = "1.0.21" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 768 | 769 | [[package]] 770 | name = "ryu" 771 | version = "1.0.20" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 774 | 775 | [[package]] 776 | name = "scopeguard" 777 | version = "1.2.0" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 780 | 781 | [[package]] 782 | name = "serde" 783 | version = "1.0.219" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 786 | dependencies = [ 787 | "serde_derive", 788 | ] 789 | 790 | [[package]] 791 | name = "serde_derive" 792 | version = "1.0.219" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 795 | dependencies = [ 796 | "proc-macro2", 797 | "quote", 798 | "syn", 799 | ] 800 | 801 | [[package]] 802 | name = "serde_spanned" 803 | version = "0.6.8" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 806 | dependencies = [ 807 | "serde", 808 | ] 809 | 810 | [[package]] 811 | name = "signal-hook" 812 | version = "0.3.18" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 815 | dependencies = [ 816 | "libc", 817 | "signal-hook-registry", 818 | ] 819 | 820 | [[package]] 821 | name = "signal-hook-mio" 822 | version = "0.2.4" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 825 | dependencies = [ 826 | "libc", 827 | "mio", 828 | "signal-hook", 829 | ] 830 | 831 | [[package]] 832 | name = "signal-hook-registry" 833 | version = "1.4.5" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 836 | dependencies = [ 837 | "libc", 838 | ] 839 | 840 | [[package]] 841 | name = "smallvec" 842 | version = "1.15.0" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 845 | 846 | [[package]] 847 | name = "socket2" 848 | version = "0.5.10" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 851 | dependencies = [ 852 | "libc", 853 | "windows-sys 0.52.0", 854 | ] 855 | 856 | [[package]] 857 | name = "spine-pkgman" 858 | version = "0.3.0" 859 | dependencies = [ 860 | "anyhow", 861 | "clap", 862 | "crossterm 0.29.0", 863 | "dirs", 864 | "indicatif", 865 | "ratatui", 866 | "serde", 867 | "tokio", 868 | "toml", 869 | "which", 870 | ] 871 | 872 | [[package]] 873 | name = "static_assertions" 874 | version = "1.1.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 877 | 878 | [[package]] 879 | name = "strsim" 880 | version = "0.11.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 883 | 884 | [[package]] 885 | name = "strum" 886 | version = "0.26.3" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 889 | dependencies = [ 890 | "strum_macros", 891 | ] 892 | 893 | [[package]] 894 | name = "strum_macros" 895 | version = "0.26.4" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 898 | dependencies = [ 899 | "heck", 900 | "proc-macro2", 901 | "quote", 902 | "rustversion", 903 | "syn", 904 | ] 905 | 906 | [[package]] 907 | name = "syn" 908 | version = "2.0.101" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 911 | dependencies = [ 912 | "proc-macro2", 913 | "quote", 914 | "unicode-ident", 915 | ] 916 | 917 | [[package]] 918 | name = "thiserror" 919 | version = "2.0.12" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 922 | dependencies = [ 923 | "thiserror-impl", 924 | ] 925 | 926 | [[package]] 927 | name = "thiserror-impl" 928 | version = "2.0.12" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 931 | dependencies = [ 932 | "proc-macro2", 933 | "quote", 934 | "syn", 935 | ] 936 | 937 | [[package]] 938 | name = "tokio" 939 | version = "1.45.1" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 942 | dependencies = [ 943 | "backtrace", 944 | "bytes", 945 | "libc", 946 | "mio", 947 | "parking_lot", 948 | "pin-project-lite", 949 | "signal-hook-registry", 950 | "socket2", 951 | "tokio-macros", 952 | "windows-sys 0.52.0", 953 | ] 954 | 955 | [[package]] 956 | name = "tokio-macros" 957 | version = "2.5.0" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 960 | dependencies = [ 961 | "proc-macro2", 962 | "quote", 963 | "syn", 964 | ] 965 | 966 | [[package]] 967 | name = "toml" 968 | version = "0.8.22" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 971 | dependencies = [ 972 | "serde", 973 | "serde_spanned", 974 | "toml_datetime", 975 | "toml_edit", 976 | ] 977 | 978 | [[package]] 979 | name = "toml_datetime" 980 | version = "0.6.9" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 983 | dependencies = [ 984 | "serde", 985 | ] 986 | 987 | [[package]] 988 | name = "toml_edit" 989 | version = "0.22.26" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 992 | dependencies = [ 993 | "indexmap", 994 | "serde", 995 | "serde_spanned", 996 | "toml_datetime", 997 | "toml_write", 998 | "winnow", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "toml_write" 1003 | version = "0.1.1" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 1006 | 1007 | [[package]] 1008 | name = "unicode-ident" 1009 | version = "1.0.18" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1012 | 1013 | [[package]] 1014 | name = "unicode-segmentation" 1015 | version = "1.12.0" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1018 | 1019 | [[package]] 1020 | name = "unicode-truncate" 1021 | version = "1.1.0" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1024 | dependencies = [ 1025 | "itertools", 1026 | "unicode-segmentation", 1027 | "unicode-width 0.1.14", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "unicode-width" 1032 | version = "0.1.14" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1035 | 1036 | [[package]] 1037 | name = "unicode-width" 1038 | version = "0.2.0" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1041 | 1042 | [[package]] 1043 | name = "utf8parse" 1044 | version = "0.2.2" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1047 | 1048 | [[package]] 1049 | name = "wasi" 1050 | version = "0.11.0+wasi-snapshot-preview1" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1053 | 1054 | [[package]] 1055 | name = "wasm-bindgen" 1056 | version = "0.2.100" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1059 | dependencies = [ 1060 | "cfg-if", 1061 | "once_cell", 1062 | "wasm-bindgen-macro", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "wasm-bindgen-backend" 1067 | version = "0.2.100" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1070 | dependencies = [ 1071 | "bumpalo", 1072 | "log", 1073 | "proc-macro2", 1074 | "quote", 1075 | "syn", 1076 | "wasm-bindgen-shared", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "wasm-bindgen-macro" 1081 | version = "0.2.100" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1084 | dependencies = [ 1085 | "quote", 1086 | "wasm-bindgen-macro-support", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "wasm-bindgen-macro-support" 1091 | version = "0.2.100" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1094 | dependencies = [ 1095 | "proc-macro2", 1096 | "quote", 1097 | "syn", 1098 | "wasm-bindgen-backend", 1099 | "wasm-bindgen-shared", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "wasm-bindgen-shared" 1104 | version = "0.2.100" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1107 | dependencies = [ 1108 | "unicode-ident", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "web-time" 1113 | version = "1.1.0" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1116 | dependencies = [ 1117 | "js-sys", 1118 | "wasm-bindgen", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "which" 1123 | version = "7.0.3" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" 1126 | dependencies = [ 1127 | "either", 1128 | "env_home", 1129 | "rustix 1.0.7", 1130 | "winsafe", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "winapi" 1135 | version = "0.3.9" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1138 | dependencies = [ 1139 | "winapi-i686-pc-windows-gnu", 1140 | "winapi-x86_64-pc-windows-gnu", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "winapi-i686-pc-windows-gnu" 1145 | version = "0.4.0" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1148 | 1149 | [[package]] 1150 | name = "winapi-x86_64-pc-windows-gnu" 1151 | version = "0.4.0" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1154 | 1155 | [[package]] 1156 | name = "windows-sys" 1157 | version = "0.52.0" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1160 | dependencies = [ 1161 | "windows-targets", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "windows-sys" 1166 | version = "0.59.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1169 | dependencies = [ 1170 | "windows-targets", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "windows-targets" 1175 | version = "0.52.6" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1178 | dependencies = [ 1179 | "windows_aarch64_gnullvm", 1180 | "windows_aarch64_msvc", 1181 | "windows_i686_gnu", 1182 | "windows_i686_gnullvm", 1183 | "windows_i686_msvc", 1184 | "windows_x86_64_gnu", 1185 | "windows_x86_64_gnullvm", 1186 | "windows_x86_64_msvc", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "windows_aarch64_gnullvm" 1191 | version = "0.52.6" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1194 | 1195 | [[package]] 1196 | name = "windows_aarch64_msvc" 1197 | version = "0.52.6" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1200 | 1201 | [[package]] 1202 | name = "windows_i686_gnu" 1203 | version = "0.52.6" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1206 | 1207 | [[package]] 1208 | name = "windows_i686_gnullvm" 1209 | version = "0.52.6" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1212 | 1213 | [[package]] 1214 | name = "windows_i686_msvc" 1215 | version = "0.52.6" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1218 | 1219 | [[package]] 1220 | name = "windows_x86_64_gnu" 1221 | version = "0.52.6" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1224 | 1225 | [[package]] 1226 | name = "windows_x86_64_gnullvm" 1227 | version = "0.52.6" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1230 | 1231 | [[package]] 1232 | name = "windows_x86_64_msvc" 1233 | version = "0.52.6" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1236 | 1237 | [[package]] 1238 | name = "winnow" 1239 | version = "0.7.10" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 1242 | dependencies = [ 1243 | "memchr", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "winsafe" 1248 | version = "0.0.19" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 1251 | --------------------------------------------------------------------------------