Skip to main content

proc_cli/commands/
kill.rs

1//! `proc kill` - Kill processes
2//!
3//! Examples:
4//!   proc kill node              # Kill all Node.js processes
5//!   proc kill :3000             # Kill what's on port 3000
6//!   proc kill 1234              # Kill specific PID
7//!   proc kill :3000,:8080       # Kill multiple targets
8//!   proc kill :3000,1234,node   # Mixed targets (port + PID + name)
9//!   proc kill node --yes        # Skip confirmation
10
11use crate::core::{parse_targets, resolve_in_dir, resolve_targets_excluding_self};
12use crate::error::{ProcError, Result};
13use crate::ui::{OutputFormat, Printer};
14use clap::Args;
15use dialoguer::Confirm;
16use std::path::PathBuf;
17
18/// Kill process(es)
19#[derive(Args, Debug)]
20pub struct KillCommand {
21    /// Target(s): process name, PID, or :port (comma-separated for multiple)
22    pub target: String,
23
24    /// Skip confirmation prompt
25    #[arg(long, short = 'y')]
26    pub yes: bool,
27
28    /// Show what would be killed without actually killing
29    #[arg(long)]
30    pub dry_run: bool,
31
32    /// Output as JSON
33    #[arg(long, short = 'j')]
34    pub json: bool,
35
36    /// Show verbose output
37    #[arg(long, short = 'v')]
38    pub verbose: bool,
39
40    /// Send SIGTERM instead of SIGKILL (graceful)
41    #[arg(long, short = 'g')]
42    pub graceful: bool,
43
44    /// Filter by directory (defaults to current directory if no path given)
45    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
46    pub in_dir: Option<String>,
47
48    /// Filter by process name
49    #[arg(long = "by", short = 'b')]
50    pub by_name: Option<String>,
51}
52
53impl KillCommand {
54    /// Executes the kill command, forcefully terminating matched processes.
55    pub fn execute(&self) -> Result<()> {
56        let format = if self.json {
57            OutputFormat::Json
58        } else {
59            OutputFormat::Human
60        };
61        let printer = Printer::new(format, self.verbose);
62
63        // Parse comma-separated targets and resolve to processes
64        // Use resolve_targets_excluding_self to avoid killing ourselves
65        let targets = parse_targets(&self.target);
66        let (mut processes, not_found) = resolve_targets_excluding_self(&targets);
67
68        // Warn about targets that weren't found
69        for target in &not_found {
70            printer.warning(&format!("Target not found: {}", target));
71        }
72
73        // Apply --in and --by filters
74        let in_dir_filter = resolve_in_dir(&self.in_dir);
75        processes.retain(|p| {
76            if let Some(ref dir_path) = in_dir_filter {
77                if let Some(ref cwd) = p.cwd {
78                    if !PathBuf::from(cwd).starts_with(dir_path) {
79                        return false;
80                    }
81                } else {
82                    return false;
83                }
84            }
85            if let Some(ref name) = self.by_name {
86                if !p.name.to_lowercase().contains(&name.to_lowercase()) {
87                    return false;
88                }
89            }
90            true
91        });
92
93        if processes.is_empty() {
94            return Err(ProcError::ProcessNotFound(self.target.clone()));
95        }
96
97        // Dry run: just show what would be killed
98        if self.dry_run {
99            printer.print_processes(&processes);
100            printer.warning(&format!(
101                "Dry run: would kill {} process{}",
102                processes.len(),
103                if processes.len() == 1 { "" } else { "es" }
104            ));
105            return Ok(());
106        }
107
108        // Confirm before killing (unless --yes)
109        if !self.yes && !self.json {
110            printer.print_confirmation("kill", &processes);
111
112            let confirmed = Confirm::new()
113                .with_prompt(format!(
114                    "Kill {} process{}?",
115                    processes.len(),
116                    if processes.len() == 1 { "" } else { "es" }
117                ))
118                .default(false)
119                .interact()
120                .unwrap_or(false);
121
122            if !confirmed {
123                printer.warning("Cancelled");
124                return Ok(());
125            }
126        }
127
128        // Kill the processes
129        let mut killed = Vec::new();
130        let mut failed = Vec::new();
131
132        for proc in processes {
133            let result = if self.graceful {
134                proc.terminate()
135            } else {
136                proc.kill()
137            };
138
139            match result {
140                Ok(()) => killed.push(proc),
141                Err(e) => failed.push((proc, e.to_string())),
142            }
143        }
144
145        printer.print_kill_result(&killed, &failed);
146
147        if failed.is_empty() {
148            Ok(())
149        } else {
150            Err(ProcError::SignalError(format!(
151                "Failed to kill {} process(es)",
152                failed.len()
153            )))
154        }
155    }
156}