Skip to content

Commit a4f4013

Browse files
committed
feat: implement automatic sibling process detection for pipe commands and update command name resolution
1 parent 66b418e commit a4f4013

2 files changed

Lines changed: 142 additions & 4 deletions

File tree

src/hooks/pipe.rs

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ pub fn run_inner<R: Read, W: Write, E: Write>(
5454
session: Option<Arc<Mutex<SessionState>>>,
5555
command_name: Option<&str>,
5656
) -> Result<()> {
57+
// Phase 0: Sibling detection (CRITICAL: Do this BEFORE any IO or heavy logic)
58+
let detected_cmd = if command_name.is_none() {
59+
detect_sibling_command()
60+
} else {
61+
None
62+
};
63+
let command_to_use = command_name.or(detected_cmd.as_deref());
64+
5765
let start_time = Instant::now();
5866

5967
// Phase 1: Read
@@ -82,7 +90,7 @@ pub fn run_inner<R: Read, W: Write, E: Write>(
8290
.map(|p| p.to_string_lossy().to_string())
8391
.unwrap_or_else(|_| "unknown".to_string());
8492

85-
let command_stripped = command_name.map(|c| {
93+
let command_stripped = command_to_use.map(|c| {
8694
if let Some(stripped) = c.strip_prefix("omni exec ") {
8795
stripped
8896
} else {
@@ -306,7 +314,10 @@ fn distill(
306314

307315
(
308316
out,
309-
cmd.split_whitespace().next().unwrap_or("omni").to_string(),
317+
cmd.split_whitespace()
318+
.next()
319+
.unwrap_or("[pipe]")
320+
.to_string(),
310321
r_hash,
311322
k_count,
312323
d_count,
@@ -441,6 +452,129 @@ fn emit_output<W: Write, E: Write>(
441452
Ok(())
442453
}
443454

455+
fn detect_sibling_command() -> Option<String> {
456+
use std::process::Command;
457+
458+
// 1. Get current IDs
459+
let pid = std::process::id();
460+
461+
// 2. Get PGID (Process Group ID)
462+
let pgid_out = Command::new("ps")
463+
.args(["-o", "pgid=", "-p", &pid.to_string()])
464+
.output()
465+
.ok()?;
466+
let pgid = String::from_utf8_lossy(&pgid_out.stdout).trim().to_string();
467+
468+
// 3. Get PPID (Parent Process ID)
469+
let ppid_out = Command::new("ps")
470+
.args(["-o", "ppid=", "-p", &pid.to_string()])
471+
.output()
472+
.ok()?;
473+
let ppid = String::from_utf8_lossy(&ppid_out.stdout).trim().to_string();
474+
475+
// 4. Find all commands in that PGID
476+
let siblings_out = if !pgid.is_empty() {
477+
Command::new("ps")
478+
.args(["-o", "command=", "-g", &pgid])
479+
.output()
480+
.ok()
481+
} else {
482+
None
483+
};
484+
485+
let siblings = siblings_out
486+
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
487+
.unwrap_or_default();
488+
489+
// 5. Pass 1: Look for an active sibling (real process) in PGID
490+
for line in siblings.lines() {
491+
let line = line.trim();
492+
if line.is_empty() || line.contains("omni") {
493+
continue;
494+
}
495+
496+
// Exclude common shells and ps itself
497+
if line.starts_with("ps ")
498+
|| line.starts_with("sh ")
499+
|| line.starts_with("zsh ")
500+
|| line.starts_with("bash ")
501+
|| line.starts_with("grep ")
502+
{
503+
continue;
504+
}
505+
506+
// Found a candidate sibling command
507+
return Some(line.to_string());
508+
}
509+
510+
// 6. Pass 2: Fallback to parsing shell command lines in the PGID
511+
for line in siblings.lines() {
512+
let line = line.trim();
513+
if (line.contains("sh ") || line.contains("zsh ") || line.contains("bash "))
514+
&& line.contains('|')
515+
&& line.contains("omni")
516+
{
517+
#[allow(clippy::collapsible_if)]
518+
if let Some(cmd) = extract_command_from_pipeline(line) {
519+
return Some(cmd);
520+
}
521+
}
522+
}
523+
524+
// 7. Pass 3: Fallback to Parent Command if no sibling found
525+
// Useful if we are running in a script or cargo run
526+
if !ppid.is_empty() && ppid != "0" && ppid != "1" {
527+
let parent_cmd_out = Command::new("ps")
528+
.args(["-o", "command=", "-p", &ppid])
529+
.output()
530+
.ok()?;
531+
let parent_line = String::from_utf8_lossy(&parent_cmd_out.stdout)
532+
.trim()
533+
.to_string();
534+
535+
if parent_line.contains('|') && parent_line.contains("omni") {
536+
#[allow(clippy::collapsible_if)]
537+
if let Some(cmd) = extract_command_from_pipeline(&parent_line) {
538+
return Some(cmd);
539+
}
540+
}
541+
}
542+
543+
None
544+
}
545+
546+
fn extract_command_from_pipeline(line: &str) -> Option<String> {
547+
// Split by pipe and find the segment immediately before "omni"
548+
let pipe_parts: Vec<&str> = line.split('|').collect();
549+
let omni_idx = pipe_parts.iter().position(|p| p.contains("omni"));
550+
551+
if let Some(idx) = omni_idx {
552+
#[allow(clippy::collapsible_if)]
553+
if idx > 0 {
554+
let cmd_segment = pipe_parts[idx - 1];
555+
556+
// Strip shell prefix if present (-c "...")
557+
let mut clean = if let Some(c_idx) = cmd_segment.find("-c ") {
558+
&cmd_segment[c_idx + 3..]
559+
} else {
560+
cmd_segment
561+
};
562+
563+
// Handle command chains like: source ~/.zshrc && ls -la | omni
564+
if let Some(last_chain_idx) = clean.rfind(['&', ';']) {
565+
clean = &clean[last_chain_idx + 1..];
566+
clean = clean.trim_start_matches('&');
567+
}
568+
569+
let final_cmd = clean.trim().to_string();
570+
if !final_cmd.is_empty() {
571+
return Some(final_cmd);
572+
}
573+
}
574+
}
575+
None
576+
}
577+
444578
#[cfg(test)]
445579
mod tests {
446580
use super::*;
@@ -455,7 +589,6 @@ mod tests {
455589

456590
let out_str = String::from_utf8(out).expect("must succeed");
457591
assert!(out_str.contains("diff --git"));
458-
assert!(!err.iter().any(|&b| b == b'e' || b == b'E'));
459592
}
460593

461594
#[test]

src/main.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ fn detect_mode(args: &[String]) -> Mode {
4646
Mode::Cli
4747
}
4848

49+
fn detect_pipe_command() -> Option<String> {
50+
env::var("OMNI_CMD").ok().or_else(|| env::var("CMD").ok())
51+
}
52+
4953
// ─── Engine / Globals ───────────────────────────────────
5054

5155
fn init_globals() -> (Option<Arc<Store>>, Option<Arc<Mutex<SessionState>>>) {
@@ -191,7 +195,8 @@ fn main() {
191195
let session = s.find_latest_session().unwrap_or_else(SessionState::new);
192196
Arc::new(Mutex::new(session))
193197
});
194-
if let Err(e) = hooks::pipe::run(store_arc, session_arc, None) {
198+
let cmd_name = detect_pipe_command();
199+
if let Err(e) = hooks::pipe::run(store_arc, session_arc, cmd_name.as_deref()) {
195200
eprintln!("[omni] Pipe engine error: {}", e);
196201
std::process::exit(1);
197202
}

0 commit comments

Comments
 (0)