use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use proc_cli::commands::{
ByCommand, ForCommand, FreeCommand, FreezeCommand, InCommand, InfoCommand, KillCommand,
ListCommand, OnCommand, OrphansCommand, PortsCommand, StopCommand, StuckCommand, ThawCommand,
TreeCommand, UnstickCommand, WaitCommand, WatchCommand, WhyCommand,
};
use proc_cli::error::ExitCode;
use std::io;
use std::process;
const VERSION_INFO: &str = concat!(
env!("CARGO_PKG_VERSION"),
"\nhttps://github.com/yazeed/proc",
"\nLicense: MIT"
);
#[derive(Parser)]
#[command(name = "proc")]
#[command(author, version = VERSION_INFO, about, long_about = None)]
#[command(propagate_version = true)]
#[command(
after_help = "Targets: :port, PID, or process name. Comma-separate for multiple.
Run 'proc --help' for examples or visit https://github.com/yazeed/proc"
)]
#[command(after_long_help = "EXAMPLES:
Port/Process Lookup:
proc on :3000 What's on port 3000?
proc on :3000,:8080 What's on multiple ports?
proc on node What ports are node processes using?
Find by File:
proc for ./script.py What's running this file?
proc for /usr/bin/node Processes running this executable
proc for app.log What has this file open?
Filter by Name:
proc by node Processes named 'node'
proc by node --in . Node processes in current directory
proc by node --min-cpu 5 Node processes using >5% CPU
Filter by Directory:
proc in . Processes in current directory
proc in . --by node Node processes in cwd
List All:
proc list All processes
proc list --min-cpu 10 Processes using >10% CPU
Info/Kill/Stop (multi-target):
proc info :3000,:8080 Info for multiple targets
proc kill :3000,node Kill port 3000 and node processes
proc stop :3000,:8080 Stop multiple targets gracefully
Watch (Real-Time):
proc watch Watch all processes (alias: proc top)
proc watch node Watch node processes
proc watch :3000 Watch process on port 3000
proc watch -n 1 --sort mem 1s refresh, sorted by memory
proc watch --in . --by node Node processes in current directory
Freeze/Thaw:
proc freeze node Pause node processes (SIGSTOP)
proc freeze :3000 --yes Freeze by port, skip confirm
proc thaw node Resume frozen processes (SIGCONT)
Free Ports:
proc free :3000 Kill process, verify port freed
proc free :3000,:8080 --yes Free multiple ports
Diagnostics:
proc why :3000 Why is port 3000 busy? (ancestry)
proc orphans Find orphaned processes
proc orphans --kill Find and kill orphans
Wait:
proc wait node Wait until node processes exit
proc wait :3000 --timeout 60 Wait up to 60s for port to free
proc wait node -n 10 -q Check every 10s, quiet mode
Other:
proc ports List all listening ports
proc tree --min-cpu 5 Process tree filtered by CPU
proc stuck Find hung processes
proc unstick --force Recover or terminate stuck processes
proc stop nginx --signal HUP Send custom signal (config reload)
Targets: :port, PID, or process name. Comma-separate for multiple.
For more information, visit: https://github.com/yazeed/proc")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[command(visible_alias = ":")]
On(OnCommand),
#[command(visible_alias = "f")]
For(ForCommand),
#[command(visible_alias = "b")]
By(ByCommand),
In(InCommand),
#[command(visible_aliases = ["l", "ps"])]
List(ListCommand),
#[command(visible_alias = "i")]
Info(InfoCommand),
#[command(visible_alias = "p")]
Ports(PortsCommand),
#[command(visible_alias = "k")]
Kill(KillCommand),
#[command(visible_alias = "s")]
Stop(StopCommand),
#[command(visible_alias = "t")]
Tree(TreeCommand),
#[command(visible_aliases = ["w", "top"])]
Watch(WatchCommand),
#[command(visible_alias = "x")]
Stuck(StuckCommand),
#[command(visible_alias = "u")]
Unstick(UnstickCommand),
Freeze(FreezeCommand),
Thaw(ThawCommand),
#[command(visible_alias = "o")]
Orphans(OrphansCommand),
Why(WhyCommand),
Free(FreeCommand),
Wait(WaitCommand),
#[command(hide = true)]
Completions {
#[arg(value_enum)]
shell: Shell,
},
#[command(hide = true)]
Manpage,
}
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() {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
use clap::error::ErrorKind;
if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
e.exit();
}
let msg = e.to_string();
let cleaned: String = msg
.lines()
.filter(|line| !line.trim_start().starts_with("tip:"))
.collect::<Vec<_>>()
.join("\n");
eprint!("{}", cleaned);
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) {
if let Some(suggestion) = suggest_flag(&unknown, subcmd) {
eprintln!("\n tip: did you mean '{}'?", suggestion);
}
}
}
process::exit(2);
}
};
let result = match cli.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 wants_json() {
let action = std::env::args().nth(1).unwrap_or_default();
let error_json = serde_json::json!({
"action": action,
"success": false,
"error": format!("{}", e),
"exit_code": exit_code as i32
});
println!("{}", serde_json::to_string_pretty(&error_json).unwrap());
} else {
eprintln!("{}", e);
}
process::exit(exit_code as i32);
}
}
fn wants_json() -> bool {
std::env::args().any(|a| a == "--json" || a == "-j")
}