use anyhow::Result;
use clap::{CommandFactory, Parser};
use clap_complete::Shell;
use clap_complete_nushell::Nushell;
use std::sync::{Arc, Mutex};
use tempfile::tempdir;
use ui::progress::{SimpleProgressManager, SimpleProgressState};
use which::which;
use runner::{disable_output_suppression, enable_output_suppression};
mod cli;
mod commands;
mod i18n;
mod parallel;
mod runner;
mod ui;
mod utils;
use cli::{Args, Commands, FeedbackType, ShellType};
use commands::{brew_cleanup, brew_update, brew_upgrade, mise_up, rustup_update};
use i18n::LocalizedStrings;
use parallel::{ParallelScheduler, TaskResult, Tool};
use runner::ShellRunner;
use std::path::Path;
use ui::colors::{print_banner, print_error, print_info, print_success, print_warning};
use ui::icons::IconManager;
use utils::ensure_cache_dir;
fn get_tool_description(tool: &Tool) -> String {
match tool {
Tool::Homebrew => "Homebrew update & upgrade & cleanup".to_string(),
Tool::Rustup => "Rustup all toolchains update".to_string(),
Tool::Mise => "Mise tools update".to_string(),
}
}
fn get_icon_manager() -> IconManager {
IconManager::new()
}
fn read_upgrade_details(tmpdir: &std::path::Path, tool: &Tool) -> Vec<String> {
let details_file = match tool {
Tool::Homebrew => tmpdir.join("brew_upgrade_details.txt"),
Tool::Rustup => {
let enhanced_file = tmpdir.join("rustup_upgrade_details_enhanced.txt");
if enhanced_file.exists() {
enhanced_file
} else {
tmpdir.join("rustup_upgrade_details.txt")
}
}
Tool::Mise => tmpdir.join("mise_upgrade_details.txt"),
};
if let Ok(content) = std::fs::read_to_string(&details_file) {
content.lines().map(|s| s.to_string()).collect()
} else {
Vec::new()
}
}
async fn execute_parallel_updates(
tools: Vec<Tool>,
jobs: usize,
dry_run: bool,
verbose: bool,
keep_logs: bool,
tmpdir: std::path::PathBuf,
_localized: &LocalizedStrings,
) -> Result<Vec<TaskResult>> {
let scheduler = ParallelScheduler::new(jobs);
let mut progress_manager = SimpleProgressManager::new();
progress_manager.create_progress_bars(&tools);
for tool in &tools {
progress_manager.update_state(tool, SimpleProgressState::Executing);
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let progress_manager = Arc::new(Mutex::new(progress_manager));
let progress_manager_for_finalize = progress_manager.clone();
let update_fn = move |tool: Tool| {
let tool_clone = tool.clone();
let tmpdir_path = tmpdir.clone();
let progress_manager = progress_manager.clone();
tokio::spawn(async move {
let result = execute_tool_update(
tool_clone.clone(),
dry_run,
verbose,
keep_logs,
&tmpdir_path,
)
.await;
if let Ok(mut manager) = progress_manager.lock() {
if manager.has_progress_bar(&tool_clone) {
match &result {
Ok(task_result) => {
if task_result.success {
manager.update_state(&tool_clone, SimpleProgressState::Completed);
} else {
manager.update_state(&tool_clone, SimpleProgressState::Failed);
}
}
Err(_) => {
manager.update_state(&tool_clone, SimpleProgressState::Failed);
}
}
}
}
result
})
};
let results = scheduler.execute_parallel(tools.clone(), update_fn).await?;
tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await;
if let Ok(mut manager) = progress_manager_for_finalize.lock() {
manager.finalize_all();
}
Ok(results)
}
fn save_debug_logs(tmpdir: &Path, tool: &Tool) -> Result<()> {
use std::fs;
let cache_dir = ensure_cache_dir()?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| anyhow::anyhow!("Failed to get system time"))?
.as_secs();
let tool_name = match tool {
Tool::Homebrew => "homebrew",
Tool::Rustup => "rustup",
Tool::Mise => "mise",
};
let tool_dir = cache_dir.join(tool_name);
fs::create_dir_all(&tool_dir)?;
let timestamp_dir = tool_dir.join(format!("{}", timestamp));
fs::create_dir_all(×tamp_dir)?;
let log_files = get_log_files_for_tool(tool);
for log_file in &log_files {
let src_path = tmpdir.join(log_file);
if src_path.exists() {
let dst_path = timestamp_dir.join(log_file);
if let Err(e) = fs::copy(&src_path, &dst_path) {
eprintln!("Warning: Failed to copy {}: {}", log_file, e);
}
}
}
let latest_link = tool_dir.join("latest");
if latest_link.exists() {
fs::remove_file(&latest_link).ok();
}
std::os::unix::fs::symlink(×tamp_dir, &latest_link).ok();
Ok(())
}
fn get_log_files_for_tool(tool: &Tool) -> Vec<&'static str> {
match tool {
Tool::Homebrew => vec![
"brew_detailed_debug.log",
"brew_outdated.log",
"brew_update.log",
"brew_upgrade.log",
"brew_cleanup.log",
"brew_errors.log",
"outdated_packages.json",
],
Tool::Rustup => vec!["rustup_update.log"],
Tool::Mise => vec!["mise_up.log"],
}
}
async fn execute_tool_update(
tool: Tool,
dry_run: bool,
verbose: bool,
keep_logs: bool,
tmpdir: &std::path::Path,
) -> Result<TaskResult> {
let runner = ShellRunner;
enable_output_suppression();
let result = if dry_run {
TaskResult {
tool: tool.clone(),
success: true,
output: format!("{} (dry run)", tool.display_name()),
}
} else {
match tool {
Tool::Homebrew => {
let update_result = brew_update(&runner, tmpdir, verbose)?;
let upgrade_result = brew_upgrade(&runner, tmpdir, verbose)?;
let cleanup_result = brew_cleanup(&runner, tmpdir, verbose)?;
let has_changes = update_result.0 == "changed"
|| upgrade_result.0 == "changed"
|| cleanup_result.0 == "changed";
let success = (update_result.0 == "changed" || update_result.0 == "unchanged")
&& (upgrade_result.0 == "changed" || upgrade_result.0 == "unchanged")
&& (cleanup_result.0 == "changed" || cleanup_result.0 == "unchanged");
let output = if has_changes {
"Homebrew updated".to_string()
} else {
"Homebrew already latest".to_string()
};
TaskResult {
tool,
success,
output,
}
}
Tool::Rustup => {
let result = rustup_update(&runner, tmpdir, verbose)?;
let has_changes = result.0 == "changed";
let output = if has_changes {
"Rustup updated".to_string()
} else {
"Rustup already latest".to_string()
};
TaskResult {
tool,
success: result.0 == "changed" || result.0 == "unchanged",
output,
}
}
Tool::Mise => {
let result = mise_up(&runner, tmpdir, verbose)?;
let has_changes = result.0 == "changed";
let output = if has_changes {
"Mise updated".to_string()
} else {
"Mise already latest".to_string()
};
TaskResult {
tool,
success: result.0 == "changed" || result.0 == "unchanged",
output,
}
}
}
};
disable_output_suppression();
if keep_logs {
if let Err(e) = save_debug_logs(tmpdir, &result.tool) {
eprintln!("Warning: Failed to save debug logs: {}", e);
}
}
Ok(result)
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if let Some(Commands::Completion { shell }) = &args.command {
let mut cmd = Args::command();
match shell {
ShellType::Bash => {
clap_complete::generate(Shell::Bash, &mut cmd, "devtool", &mut std::io::stdout())
}
ShellType::Zsh => {
clap_complete::generate(Shell::Zsh, &mut cmd, "devtool", &mut std::io::stdout())
}
ShellType::Fish => {
clap_complete::generate(Shell::Fish, &mut cmd, "devtool", &mut std::io::stdout())
}
ShellType::Powershell => clap_complete::generate(
Shell::PowerShell,
&mut cmd,
"devtool",
&mut std::io::stdout(),
),
ShellType::Elvish => {
clap_complete::generate(Shell::Elvish, &mut cmd, "devtool", &mut std::io::stdout())
}
ShellType::Nushell => {
clap_complete::generate(Nushell, &mut cmd, "devtool", &mut std::io::stdout())
}
}
return Ok(());
}
if let Some(Commands::Feedback {
feedback_type,
message,
verbose,
}) = &args.command
{
return handle_feedback_command(feedback_type, message, *verbose);
}
let (dry_run, verbose, no_color, keep_logs, parallel, sequential, jobs, no_banner, _compact) =
match &args.command {
Some(Commands::Update {
dry_run,
verbose,
no_color,
keep_logs,
parallel,
sequential,
jobs,
no_banner,
compact,
}) => (
*dry_run,
*verbose,
*no_color,
*keep_logs,
*parallel,
*sequential,
*jobs,
*no_banner,
*compact,
),
None => (false, false, false, false, true, false, 3, false, false), _ => return Ok(()),
};
let system_lang = i18n::detect_system_language();
if verbose {
println!("Debug: Detected language: {}", system_lang);
}
let localized = LocalizedStrings::new(&system_lang);
if no_color {
colored::control::set_override(false);
} else if ui::colors::supports_color() {
colored::control::set_override(true);
}
let start_time = chrono::Local::now();
if !no_banner {
if ui::colors::supports_color() && !no_color {
print_banner(&format!(
"{}{}",
localized.banner,
start_time.format("%Y-%m-%d %H:%M:%S")
));
} else {
println!(
"{}{}",
localized.banner,
start_time.format("%Y-%m-%d %H:%M:%S")
);
}
}
let mut available_tools: Vec<Tool> = Vec::new();
let mut skipped: Vec<&str> = Vec::new();
if which("brew").is_ok() {
available_tools.push(Tool::Homebrew);
} else {
skipped.push("Homebrew");
}
if which("rustup").is_ok() {
available_tools.push(Tool::Rustup);
} else {
skipped.push("Rust (rustup)");
}
if which("mise").is_ok() {
available_tools.push(Tool::Mise);
} else {
skipped.push("Mise");
}
let total = available_tools.len();
if total == 0 {
let icons = get_icon_manager();
let warning_msg = if system_lang == "zh" {
format!(
"{} 未检测到可执行步骤。跳过: {}",
icons.warning(),
skipped.join(", ")
)
} else {
format!(
"{} No executable steps detected. Skipped: {}",
icons.warning(),
skipped.join(", ")
)
};
if ui::colors::supports_color() && !no_color {
print_warning(&warning_msg);
} else {
println!("{}", warning_msg);
}
return Ok(());
}
let tmp = tempdir()?;
let _run_tmp = tmp.path().to_path_buf();
let icons = get_icon_manager();
let tools_msg = format!(
"{} {}",
icons.clipboard(),
localized.steps_count.replace("{}", &total.to_string())
);
if ui::colors::supports_color() && !no_color {
print_info(&tools_msg);
} else {
println!("{}", tools_msg);
}
for (i, tool) in available_tools.iter().enumerate() {
let tool_description = get_tool_description(tool);
println!(" {}) {}", i + 1, tool_description);
}
let mut results: Vec<TaskResult> = Vec::new();
let mut short_updates: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let use_parallel = parallel && !sequential;
if use_parallel {
if verbose {
println!("{} 并行执行模式 (最大并发数: {})", icons.rocket(), jobs);
}
results = execute_parallel_updates(
available_tools,
jobs,
dry_run,
verbose,
keep_logs,
_run_tmp.clone(),
&localized,
)
.await?;
for result in &results {
if result.success {
let details = read_upgrade_details(&_run_tmp, &result.tool);
if !details.is_empty() {
let key = match result.tool {
Tool::Homebrew => "Homebrew:升级软件包".to_string(),
Tool::Rustup => "Rust:更新工具链".to_string(),
Tool::Mise => "Mise:更新托管工具".to_string(),
};
short_updates.insert(key, details);
}
}
}
} else {
if verbose {
println!("🔄 顺序执行模式");
}
let mut progress_manager = SimpleProgressManager::new();
progress_manager.create_progress_bars(&available_tools);
for tool in &available_tools {
progress_manager.update_state(tool, SimpleProgressState::Executing);
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
for tool in available_tools.iter() {
let result = if dry_run {
TaskResult {
tool: tool.clone(),
success: true,
output: format!("{} (dry run)", tool.display_name()),
}
} else {
match execute_tool_update(tool.clone(), dry_run, verbose, keep_logs, &_run_tmp)
.await
{
Ok(result) => result,
Err(e) => {
if verbose {
eprintln!("Error executing {}: {}", tool.display_name(), e);
}
TaskResult {
tool: tool.clone(),
success: false,
output: format!("{} failed: {}", tool.display_name(), e),
}
}
}
};
if result.success {
progress_manager.update_state(tool, SimpleProgressState::Completed);
} else {
progress_manager.update_state(tool, SimpleProgressState::Failed);
}
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
if result.success {
let details = read_upgrade_details(&_run_tmp, tool);
if !details.is_empty() {
let key = match tool {
Tool::Homebrew => "Homebrew:升级软件包".to_string(),
Tool::Rustup => "Rust:更新工具链".to_string(),
Tool::Mise => "Mise:更新托管工具".to_string(),
};
short_updates.insert(key, details);
}
}
results.push(result);
}
progress_manager.finalize_all();
}
let mut succ: Vec<String> = Vec::new();
let mut fail: Vec<String> = Vec::new();
let mut updated: Vec<String> = Vec::new();
let mut unchanged: Vec<String> = Vec::new();
let actions: Vec<String> = Vec::new();
for result in &results {
if result.success {
succ.push(result.tool.display_name().to_string());
let has_upgrade_details = !read_upgrade_details(&_run_tmp, &result.tool).is_empty();
if result.output.contains("updated") && has_upgrade_details {
updated.push(result.tool.display_name().to_string());
} else if result.output.contains("updated") && !has_upgrade_details {
unchanged.push(result.tool.display_name().to_string());
} else if result.output.contains("already latest") {
unchanged.push(result.tool.display_name().to_string());
} else {
unchanged.push(result.tool.display_name().to_string());
}
} else {
fail.push(result.tool.display_name().to_string());
}
}
println!();
let end_time = chrono::Local::now();
let duration = end_time.signed_duration_since(start_time);
let duration_str = match (
duration.num_hours(),
duration.num_minutes(),
duration.num_seconds(),
) {
(h, _, _) if h > 0 => format!(
"{}小时{}分{}秒",
h,
duration.num_minutes() % 60,
duration.num_seconds() % 60
),
(_, m, _) if m > 0 => format!("{}分{}秒", m, duration.num_seconds() % 60),
(_, _, s) => format!("{}秒", s),
};
let update_complete_msg = format!(
"\n{} {} ({}: {})",
localized.update_complete,
end_time.format("%Y-%m-%d %H:%M:%S"),
localized.time_taken,
duration_str
);
if ui::colors::supports_color() && !no_color {
print_success(&update_complete_msg);
if !updated.is_empty() {
let updated_msg = if system_lang == "zh" {
format!("{} 已更新:{}", icons.success(), updated.join(", "))
} else {
format!("{} Updated: {}", icons.success(), updated.join(", "))
};
print_success(&updated_msg);
} else {
print_info(&format!("{} {}", icons.info(), localized.no_updates));
}
if !actions.is_empty() {
let actions_msg = format!(
"{}{}{}",
icons.tools(),
localized.actions_executed,
actions.join(", ")
);
print_info(&actions_msg);
}
if !unchanged.is_empty() {
let unchanged_msg = format!(
"{}{}{}",
icons.warning(),
localized.already_latest,
unchanged.join(", ")
);
print_warning(&unchanged_msg);
}
} else {
println!("{}", update_complete_msg);
if !updated.is_empty() {
let updated_msg = if system_lang == "zh" {
format!("{} 已更新:{}", icons.success(), updated.join(", "))
} else {
format!("{} Updated: {}", icons.success(), updated.join(", "))
};
println!("{}", updated_msg);
} else {
println!("{} {}", icons.info(), localized.no_updates);
}
if !actions.is_empty() {
let actions_msg = format!(
"{}{}{}",
icons.tools(),
localized.actions_executed,
actions.join(", ")
);
println!("{}", actions_msg);
}
if !unchanged.is_empty() {
let unchanged_msg = format!(
"{}{}{}",
icons.warning(),
localized.already_latest,
unchanged.join(", ")
);
println!("{}", unchanged_msg);
}
}
if let Some(vals) = short_updates.get("Homebrew:升级软件包") {
if !vals.is_empty() {
if ui::colors::supports_color() && !no_color {
print_info(&format!("{} Homebrew 升级详情:", icons.package()));
} else {
println!("{} Homebrew 升级详情:", icons.package());
}
for detail in vals {
println!(" {}", detail);
}
}
}
if let Some(vals) = short_updates.get("Rust:更新工具链") {
if !vals.is_empty() {
if ui::colors::supports_color() && !no_color {
print_info(&format!("{} Rust 升级详情:", icons.rust()));
} else {
println!("{} Rust 升级详情:", icons.rust());
}
for detail in vals {
println!(" {}", detail);
}
}
}
if let Some(vals) = short_updates.get("Mise:更新托管工具") {
if !vals.is_empty() {
if ui::colors::supports_color() && !no_color {
print_info(&format!("{} Mise 升级详情:", icons.wrench()));
} else {
println!("{} Mise 升级详情:", icons.wrench());
}
for detail in vals {
println!(" {}", detail);
}
}
}
if !fail.is_empty() {
if ui::colors::supports_color() && !no_color {
print_error(&format!("{} 失败:{}", icons.failure(), fail.join(", ")));
} else {
println!("{} 失败:{}", icons.failure(), fail.join(", "));
}
std::process::exit(1);
}
Ok(())
}
fn handle_feedback_command(
feedback_type: &Option<FeedbackType>,
message: &Option<String>,
verbose: bool,
) -> Result<()> {
use std::io::{self, Write};
use std::time::{SystemTime, UNIX_EPOCH};
if ui::colors::supports_color() {
print_info("📝 devtool User Feedback Collection");
} else {
println!("📝 devtool User Feedback Collection");
}
let system_info = collect_system_info();
let feedback_type = match feedback_type {
Some(ft) => ft.clone(),
None => {
println!("\nPlease select feedback type:");
println!("1. Bug Report");
println!("2. Feature Request");
println!("3. User Experience Issue");
println!("4. Performance Issue");
println!("5. Documentation Issue");
println!("6. Other");
print!("Please enter your choice (1-6): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
match input.trim() {
"1" => FeedbackType::Bug,
"2" => FeedbackType::Feature,
"3" => FeedbackType::Ux,
"4" => FeedbackType::Performance,
"5" => FeedbackType::Documentation,
"6" => FeedbackType::Other,
_ => FeedbackType::Other,
}
}
};
let feedback_message = match message {
Some(msg) => msg.clone(),
None => {
println!("\nPlease describe your feedback:");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
input.trim().to_string()
}
};
if feedback_message.is_empty() {
println!("Feedback content cannot be empty!");
return Ok(());
}
let feedback_report =
generate_feedback_report(&feedback_type, &feedback_message, &system_info, verbose);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| anyhow::anyhow!("Failed to get system time"))?
.as_secs();
let filename = format!("devtool_feedback_{}.md", timestamp);
let feedback_dir = ensure_cache_dir()?;
let feedback_file = feedback_dir.join(&filename);
std::fs::write(&feedback_file, &feedback_report)?;
let icons = get_icon_manager();
if ui::colors::supports_color() {
print_success(&format!(
"{} Feedback saved to: {}",
icons.success(),
feedback_file.display()
));
} else {
println!(
"{} Feedback saved to: {}",
icons.success(),
feedback_file.display()
);
}
println!("\n{} Feedback Summary:", icons.clipboard());
println!("Type: {:?}", feedback_type);
println!("Content: {}", feedback_message);
if verbose {
println!("\n{} System Information:", icons.tools());
println!("{}", system_info);
}
println!("\n💡 You can also submit feedback through:");
println!("- GitHub Issues: https://github.com/jenkinpan/devtool-rs/issues");
println!("- GitHub Discussions: https://github.com/jenkinpan/devtool-rs/discussions");
Ok(())
}
fn collect_system_info() -> String {
let mut info = String::new();
if let Ok(os) = std::env::var("OS") {
info.push_str(&format!("操作系统: {}\n", os));
} else if cfg!(target_os = "macos") {
info.push_str("操作系统: macOS\n");
} else if cfg!(target_os = "linux") {
info.push_str("操作系统: Linux\n");
} else if cfg!(target_os = "windows") {
info.push_str("操作系统: Windows\n");
}
info.push_str(&format!("devtool 版本: {}\n", env!("CARGO_PKG_VERSION")));
if let Ok(rustc_version) = std::process::Command::new("rustc")
.arg("--version")
.output()
{
if let Ok(version) = String::from_utf8(rustc_version.stdout) {
info.push_str(&format!("Rust 版本: {}", version.trim()));
}
}
info
}
fn generate_feedback_report(
feedback_type: &FeedbackType,
message: &str,
system_info: &str,
_verbose: bool,
) -> String {
let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
format!(
"# devtool User Feedback Report
## Basic Information
- **Submission Time**: {}
- **Feedback Type**: {:?}
- **devtool Version**: {}
## Feedback Content
{}
## System Information
```
{}
```
## Feedback Processing
- [ ] Received
- [ ] Analyzed
- [ ] Processed
- [ ] Replied
## Notes
_This feedback was automatically generated by devtool's built-in feedback system_
",
timestamp,
feedback_type,
env!("CARGO_PKG_VERSION"),
message,
system_info
)
}