use clap::{CommandFactory, Parser};
use clap_complete::generate;
use colored::Colorize;
use proc_cli::cli::{Cli, Commands};
use proc_cli::core::{comma_notice, Process};
use proc_cli::error::{ExitCode, ProcError, REASON_INVALID_INPUT};
use proc_cli::ui::print_note;
use std::io;
use std::process;
fn print_orientation() {
println!(
"{} {}",
"proc".bold(),
"— semantic process management".bright_black()
);
println!();
println!(
" {}",
"A target is the same everywhere: :port, PID, or name.".bright_black()
);
println!();
for (cmd, what) in [
("proc on :3000", "what's on port 3000"),
("proc in .", "what's running in this directory"),
("proc by node", "processes named node"),
("proc kill :3000", "stop whatever holds the port"),
] {
println!(
" {} {}",
format!("{:<20}", cmd).cyan(),
what.bright_black()
);
}
println!();
println!(
" {} {}",
format!("{:<8}", "Common:").bright_black(),
"on · in · by · ports · info · kill · stop · free · why".white()
);
println!();
println!(
" {} {} {}",
format!("{:<20}", "proc --help").cyan(),
"all 19 commands".bright_black(),
"· https://proc-cli.com".bright_black()
);
}
fn extract_unknown_arg(msg: &str) -> Option<String> {
let start = msg.find("unexpected argument '")? + "unexpected argument '".len();
let end = msg[start..].find('\'')?;
Some(msg[start..start + end].to_string())
}
fn edit_distance(a: &str, b: &str) -> usize {
let (a, b) = (a.as_bytes(), b.as_bytes());
let (m, n) = (a.len(), b.len());
let mut dp = vec![vec![0usize; n + 1]; m + 1];
for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
row[0] = i;
}
for (j, val) in dp[0].iter_mut().enumerate().take(n + 1) {
*val = j;
}
for i in 1..=m {
for j in 1..=n {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
dp[i][j] = (dp[i - 1][j] + 1)
.min(dp[i][j - 1] + 1)
.min(dp[i - 1][j - 1] + cost);
}
}
dp[m][n]
}
fn suggest_flag(unknown: &str, subcmd_name: &str) -> Option<String> {
let cmd = Cli::command();
let subcmd = cmd.find_subcommand(subcmd_name)?;
let unknown_clean = unknown.trim_start_matches('-');
let mut best: Option<String> = None;
let mut best_dist = usize::MAX;
for arg in subcmd.get_arguments() {
if let Some(long) = arg.get_long() {
let len_diff = (unknown_clean.len() as isize - long.len() as isize).unsigned_abs();
if len_diff <= 2 && long.len() >= 3 {
let dist = edit_distance(unknown_clean, long);
if dist < best_dist {
best_dist = dist;
best = Some(format!("--{}", long));
}
}
if long.contains(unknown_clean) && unknown_clean.len() >= 3 {
return Some(format!("--{}", long));
}
}
}
let threshold = if unknown_clean.len() <= 4 { 2 } else { 3 };
if best_dist <= threshold {
best
} else {
None
}
}
fn main() -> process::ExitCode {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
use clap::error::ErrorKind;
if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
let _ = e.print();
return process::ExitCode::SUCCESS;
}
let msg = e.to_string();
let cleaned: String = msg
.lines()
.filter(|line| !line.trim_start().starts_with("tip:"))
.collect::<Vec<_>>()
.join("\n");
let mut suggestion = None;
if matches!(
e.kind(),
ErrorKind::UnknownArgument | ErrorKind::InvalidValue
) {
let args: Vec<String> = std::env::args().collect();
let subcmd = args.iter().skip(1).find(|a| !a.starts_with('-'));
let unknown = extract_unknown_arg(&msg);
if let (Some(subcmd), Some(unknown)) = (subcmd, unknown) {
suggestion = suggest_flag(&unknown, subcmd);
}
}
let exit_code = ExitCode::InvalidInput as i32;
if wants_json() {
let mut error = cleaned
.trim()
.trim_start_matches("error: ")
.lines()
.next()
.unwrap_or_default()
.to_string();
if let Some(ref suggestion) = suggestion {
error.push_str(&format!("\n Try: did you mean '{}'?", suggestion));
}
print_json_error(&error, exit_code, Some(REASON_INVALID_INPUT), None);
} else {
eprint!("{}", cleaned);
if let Some(suggestion) = suggestion {
eprintln!("\n tip: did you mean '{}'?", suggestion);
}
}
return process::ExitCode::from(exit_code as u8);
}
};
if let Some(notice) = Cli::command()
.try_get_matches()
.ok()
.and_then(|matches| comma_notice(&matches))
{
print_note(¬ice);
}
let Some(command) = cli.command else {
print_orientation();
return process::ExitCode::SUCCESS;
};
let result = match command {
Commands::On(cmd) => cmd.execute(),
Commands::For(cmd) => cmd.execute(),
Commands::By(cmd) => cmd.execute(),
Commands::In(cmd) => cmd.execute(),
Commands::List(cmd) => cmd.execute(),
Commands::Info(cmd) => cmd.execute(),
Commands::Ports(cmd) => cmd.execute(),
Commands::Kill(cmd) => cmd.execute(),
Commands::Stop(cmd) => cmd.execute(),
Commands::Tree(cmd) => cmd.execute(),
Commands::Watch(cmd) => cmd.execute(),
Commands::Stuck(cmd) => cmd.execute(),
Commands::Unstick(cmd) => cmd.execute(),
Commands::Freeze(cmd) => cmd.execute(),
Commands::Thaw(cmd) => cmd.execute(),
Commands::Orphans(cmd) => cmd.execute(),
Commands::Why(cmd) => cmd.execute(),
Commands::Free(cmd) => cmd.execute(),
Commands::Wait(cmd) => cmd.execute(),
Commands::Completions { shell } => {
generate(shell, &mut Cli::command(), "proc", &mut io::stdout());
Ok(())
}
Commands::Manpage => {
let cmd = Cli::command();
let man = clap_mangen::Man::new(cmd);
man.render(&mut io::stdout())
.expect("Failed to generate man page");
Ok(())
}
};
if let Err(e) = result {
let exit_code = ExitCode::from(&e);
if matches!(e, ProcError::Reported { .. }) {
return process::ExitCode::from(exit_code as u8);
}
if wants_json() {
let confirmation = match &e {
ProcError::ConfirmationRequired {
processes,
not_found,
..
} => Some((processes.as_slice(), not_found.as_deref())),
_ => None,
};
print_json_error(&e.to_string(), exit_code as i32, e.reason(), confirmation);
} else {
eprintln!("{}", e);
}
return process::ExitCode::from(exit_code as u8);
}
process::ExitCode::SUCCESS
}
fn print_json_error(
error: &str,
exit_code: i32,
reason: Option<&str>,
confirmation: Option<(&[Process], Option<&[String]>)>,
) {
let mut error_json = serde_json::json!({
"action": action_name(),
"success": false,
"error": error,
"exit_code": exit_code
});
if let Some(reason) = reason {
error_json["reason"] = serde_json::json!(reason);
}
if let Some((processes, not_found)) = confirmation {
error_json["needs_confirmation"] = serde_json::json!(true);
error_json["processes"] = serde_json::json!(processes);
if let Some(not_found) = not_found {
error_json["not_found"] = serde_json::json!(not_found);
}
}
println!("{}", serde_json::to_string_pretty(&error_json).unwrap());
}
fn action_name() -> String {
let first = std::env::args().skip(1).find(|a| !a.starts_with('-'));
match first {
Some(arg) => Cli::command()
.find_subcommand(&arg)
.map(|cmd| cmd.get_name().to_string())
.unwrap_or(arg),
None => "proc".to_string(),
}
}
fn wants_json() -> bool {
std::env::args()
.skip(1)
.take_while(|a| a != "--")
.any(|a| is_json_flag(&a))
}
fn is_json_flag(arg: &str) -> bool {
if arg == "--json" {
return true;
}
let Some(cluster) = arg.strip_prefix('-') else {
return false;
};
if cluster.starts_with('-') {
return false;
}
for c in cluster.chars() {
match c {
'j' => return true,
'b' | 'd' | 'i' | 'l' | 'n' | 'p' | 'r' | 's' | 'S' | 't' => return false,
_ => {}
}
}
false
}