aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-06-19 17:11:09 +0100
committerdiogo464 <[email protected]>2025-06-19 17:11:09 +0100
commita365f12a6c6e7775a5bd3c6177050b74826c608c (patch)
treea940d89c741250aedacb4dc8d8f9e92ce9810fc4
parent9c3d24b08649ebf6d4a3614f3506ce2702aafd74 (diff)
Add configurable root directory for daemon files with git root discovery
Implement --root-dir global option to specify where daemon files (PID, stdout, stderr) are created. When not specified, automatically searches upward for git repository root. This addresses the issue of daemon files being scattered across various working directories. Key features: - Global --root-dir option available on all commands - Automatic git root discovery when --root-dir not specified - Proper error handling for invalid directories - Updated all file operations to use configurable root directory - Optimized HashMap usage with PathBuf for better performance - Fixed ID extraction to show daemon names instead of full paths - Updated tests to work with new directory structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
-rw-r--r--src/main.rs317
-rw-r--r--tests/cli.rs85
2 files changed, 251 insertions, 151 deletions
diff --git a/src/main.rs b/src/main.rs
index 89a32b4..05208ea 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,7 +3,7 @@ use clap::{Args, Parser, Subcommand};
3use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; 3use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
4use std::fs::File; 4use std::fs::File;
5use std::io::{Read, Seek, SeekFrom, Write}; 5use std::io::{Read, Seek, SeekFrom, Write};
6use std::path::Path; 6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio}; 7use std::process::{Command, Stdio};
8use std::sync::mpsc::channel; 8use std::sync::mpsc::channel;
9use std::thread; 9use std::thread;
@@ -117,6 +117,13 @@ struct Cli {
117 command: Commands, 117 command: Commands,
118} 118}
119 119
120#[derive(Args)]
121struct Global {
122 /// Root directory for daemon files (pid, logs). If not specified, searches for git root.
123 #[arg(long, global = true)]
124 root_dir: Option<PathBuf>,
125}
126
120#[derive(Subcommand)] 127#[derive(Subcommand)]
121enum Commands { 128enum Commands {
122 /// Spawn a background process and redirect stdout/stderr to files 129 /// Spawn a background process and redirect stdout/stderr to files
@@ -138,7 +145,7 @@ enum Commands {
138 Status(StatusArgs), 145 Status(StatusArgs),
139 146
140 /// Clean up orphaned pid and log files 147 /// Clean up orphaned pid and log files
141 Clean, 148 Clean(CleanArgs),
142 149
143 /// Output comprehensive usage guide for LLMs 150 /// Output comprehensive usage guide for LLMs
144 Llm, 151 Llm,
@@ -149,6 +156,9 @@ enum Commands {
149 156
150#[derive(Args)] 157#[derive(Args)]
151struct RunArgs { 158struct RunArgs {
159 #[clap(flatten)]
160 global: Global,
161
152 /// Process identifier 162 /// Process identifier
153 id: String, 163 id: String,
154 164
@@ -158,6 +168,9 @@ struct RunArgs {
158 168
159#[derive(Args)] 169#[derive(Args)]
160struct StopArgs { 170struct StopArgs {
171 #[clap(flatten)]
172 global: Global,
173
161 /// Process identifier 174 /// Process identifier
162 id: String, 175 id: String,
163 176
@@ -168,6 +181,9 @@ struct StopArgs {
168 181
169#[derive(Args)] 182#[derive(Args)]
170struct TailArgs { 183struct TailArgs {
184 #[clap(flatten)]
185 global: Global,
186
171 /// Process identifier 187 /// Process identifier
172 id: String, 188 id: String,
173 189
@@ -190,6 +206,9 @@ struct TailArgs {
190 206
191#[derive(Args)] 207#[derive(Args)]
192struct CatArgs { 208struct CatArgs {
209 #[clap(flatten)]
210 global: Global,
211
193 /// Process identifier 212 /// Process identifier
194 id: String, 213 id: String,
195 214
@@ -204,6 +223,9 @@ struct CatArgs {
204 223
205#[derive(Args)] 224#[derive(Args)]
206struct ListArgs { 225struct ListArgs {
226 #[clap(flatten)]
227 global: Global,
228
207 /// Quiet mode - output only process data without headers 229 /// Quiet mode - output only process data without headers
208 #[arg(short, long)] 230 #[arg(short, long)]
209 quiet: bool, 231 quiet: bool,
@@ -211,12 +233,24 @@ struct ListArgs {
211 233
212#[derive(Args)] 234#[derive(Args)]
213struct StatusArgs { 235struct StatusArgs {
236 #[clap(flatten)]
237 global: Global,
238
214 /// Process identifier 239 /// Process identifier
215 id: String, 240 id: String,
216} 241}
217 242
218#[derive(Args)] 243#[derive(Args)]
244struct CleanArgs {
245 #[clap(flatten)]
246 global: Global,
247}
248
249#[derive(Args)]
219struct WaitArgs { 250struct WaitArgs {
251 #[clap(flatten)]
252 global: Global,
253
220 /// Process identifier 254 /// Process identifier
221 id: String, 255 id: String,
222 256
@@ -248,34 +282,89 @@ fn run_command(command: Commands) -> Result<()> {
248 if args.command.is_empty() { 282 if args.command.is_empty() {
249 return Err(anyhow::anyhow!("Command cannot be empty")); 283 return Err(anyhow::anyhow!("Command cannot be empty"));
250 } 284 }
251 run_daemon(&args.id, &args.command) 285 let root_dir = resolve_root_dir(&args.global)?;
286 run_daemon(&args.id, &args.command, &root_dir)
287 }
288 Commands::Stop(args) => {
289 let root_dir = resolve_root_dir(&args.global)?;
290 stop_daemon(&args.id, args.timeout, &root_dir)
252 } 291 }
253 Commands::Stop(args) => stop_daemon(&args.id, args.timeout),
254 Commands::Tail(args) => { 292 Commands::Tail(args) => {
255 let show_stdout = !args.stderr || args.stdout; 293 let show_stdout = !args.stderr || args.stdout;
256 let show_stderr = !args.stdout || args.stderr; 294 let show_stderr = !args.stdout || args.stderr;
257 tail_logs(&args.id, show_stdout, show_stderr, args.follow, args.lines) 295 let root_dir = resolve_root_dir(&args.global)?;
296 tail_logs(&args.id, show_stdout, show_stderr, args.follow, args.lines, &root_dir)
258 } 297 }
259 Commands::Cat(args) => { 298 Commands::Cat(args) => {
260 let show_stdout = !args.stderr || args.stdout; 299 let show_stdout = !args.stderr || args.stdout;
261 let show_stderr = !args.stdout || args.stderr; 300 let show_stderr = !args.stdout || args.stderr;
262 cat_logs(&args.id, show_stdout, show_stderr) 301 let root_dir = resolve_root_dir(&args.global)?;
302 cat_logs(&args.id, show_stdout, show_stderr, &root_dir)
303 }
304 Commands::List(args) => {
305 let root_dir = resolve_root_dir(&args.global)?;
306 list_daemons(args.quiet, &root_dir)
307 }
308 Commands::Status(args) => {
309 let root_dir = resolve_root_dir(&args.global)?;
310 status_daemon(&args.id, &root_dir)
311 }
312 Commands::Clean(args) => {
313 let root_dir = resolve_root_dir(&args.global)?;
314 clean_orphaned_files(&root_dir)
263 } 315 }
264 Commands::List(args) => list_daemons(args.quiet),
265 Commands::Status(args) => status_daemon(&args.id),
266 Commands::Clean => clean_orphaned_files(),
267 Commands::Llm => { 316 Commands::Llm => {
268 print_llm_guide(); 317 print_llm_guide();
269 Ok(()) 318 Ok(())
270 } 319 }
271 Commands::Wait(args) => wait_daemon(&args.id, args.timeout, args.interval), 320 Commands::Wait(args) => {
321 let root_dir = resolve_root_dir(&args.global)?;
322 wait_daemon(&args.id, args.timeout, args.interval, &root_dir)
323 }
272 } 324 }
273} 325}
274 326
275fn run_daemon(id: &str, command: &[String]) -> Result<()> { 327fn find_git_root() -> Result<PathBuf> {
276 let pid_file = format!("{}.pid", id); 328 let mut current = std::env::current_dir()?;
277 let stdout_file = format!("{}.stdout", id); 329
278 let stderr_file = format!("{}.stderr", id); 330 loop {
331 let git_path = current.join(".git");
332 if git_path.exists() {
333 return Ok(current);
334 }
335
336 match current.parent() {
337 Some(parent) => current = parent.to_path_buf(),
338 None => return Err(anyhow::anyhow!(
339 "No git repository found. Please specify --root-dir or run from within a git repository"
340 )),
341 }
342 }
343}
344
345fn resolve_root_dir(global: &Global) -> Result<PathBuf> {
346 match &global.root_dir {
347 Some(dir) => {
348 if !dir.exists() {
349 return Err(anyhow::anyhow!("Specified root directory does not exist: {}", dir.display()));
350 }
351 if !dir.is_dir() {
352 return Err(anyhow::anyhow!("Specified root path is not a directory: {}", dir.display()));
353 }
354 Ok(dir.clone())
355 },
356 None => find_git_root(),
357 }
358}
359
360fn build_file_path(root_dir: &Path, id: &str, extension: &str) -> PathBuf {
361 root_dir.join(format!("{}.{}", id, extension))
362}
363
364fn run_daemon(id: &str, command: &[String], root_dir: &Path) -> Result<()> {
365 let pid_file = build_file_path(root_dir, id, "pid");
366 let stdout_file = build_file_path(root_dir, id, "stdout");
367 let stderr_file = build_file_path(root_dir, id, "stderr");
279 368
280 // Check if process is already running 369 // Check if process is already running
281 if is_process_running(&pid_file)? { 370 if is_process_running(&pid_file)? {
@@ -315,12 +404,12 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> {
315 // Don't wait for the child - let it run detached 404 // Don't wait for the child - let it run detached
316 std::mem::forget(child); 405 std::mem::forget(child);
317 406
318 println!("Started daemon '{}' with PID written to {}", id, pid_file); 407 println!("Started daemon '{}' with PID written to {}", id, pid_file.display());
319 408
320 Ok(()) 409 Ok(())
321} 410}
322 411
323fn is_process_running(pid_file: &str) -> Result<bool> { 412fn is_process_running<P: AsRef<Path>>(pid_file: P) -> Result<bool> {
324 let pid_file_data = match PidFile::read_from_file(pid_file) { 413 let pid_file_data = match PidFile::read_from_file(pid_file) {
325 Ok(data) => data, 414 Ok(data) => data,
326 Err(PidFileReadError::FileNotFound) => return Ok(false), // No PID file means no running process 415 Err(PidFileReadError::FileNotFound) => return Ok(false), // No PID file means no running process
@@ -336,8 +425,8 @@ fn is_process_running(pid_file: &str) -> Result<bool> {
336 Ok(output.status.success()) 425 Ok(output.status.success())
337} 426}
338 427
339fn stop_daemon(id: &str, timeout: u64) -> Result<()> { 428fn stop_daemon(id: &str, timeout: u64, root_dir: &Path) -> Result<()> {
340 let pid_file = format!("{}.pid", id); 429 let pid_file = build_file_path(root_dir, id, "pid");
341 430
342 // Check if PID file exists and read PID data 431 // Check if PID file exists and read PID data
343 let pid_file_data = match PidFile::read_from_file(&pid_file) { 432 let pid_file_data = match PidFile::read_from_file(&pid_file) {
@@ -441,9 +530,9 @@ fn is_process_running_by_pid(pid: u32) -> bool {
441 } 530 }
442} 531}
443 532
444fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { 533fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool, root_dir: &Path) -> Result<()> {
445 let stdout_file = format!("{}.stdout", id); 534 let stdout_file = build_file_path(root_dir, id, "stdout");
446 let stderr_file = format!("{}.stderr", id); 535 let stderr_file = build_file_path(root_dir, id, "stderr");
447 536
448 let mut files_found = false; 537 let mut files_found = false;
449 538
@@ -452,12 +541,12 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
452 if !contents.is_empty() { 541 if !contents.is_empty() {
453 files_found = true; 542 files_found = true;
454 if show_stderr { 543 if show_stderr {
455 println!("==> {} <==", stdout_file); 544 println!("==> {} <==", stdout_file.display());
456 } 545 }
457 print!("{}", contents); 546 print!("{}", contents);
458 } 547 }
459 } else { 548 } else {
460 tracing::warn!("Could not read {}", stdout_file); 549 tracing::warn!("Could not read {}", stdout_file.display());
461 } 550 }
462 } 551 }
463 552
@@ -466,12 +555,12 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
466 if !contents.is_empty() { 555 if !contents.is_empty() {
467 files_found = true; 556 files_found = true;
468 if show_stdout { 557 if show_stdout {
469 println!("==> {} <==", stderr_file); 558 println!("==> {} <==", stderr_file.display());
470 } 559 }
471 print!("{}", contents); 560 print!("{}", contents);
472 } 561 }
473 } else { 562 } else {
474 tracing::warn!("Could not read {}", stderr_file); 563 tracing::warn!("Could not read {}", stderr_file.display());
475 } 564 }
476 } 565 }
477 566
@@ -482,31 +571,38 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
482 Ok(()) 571 Ok(())
483} 572}
484 573
485fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines: usize) -> Result<()> { 574fn tail_logs(
486 let stdout_file = format!("{}.stdout", id); 575 id: &str,
487 let stderr_file = format!("{}.stderr", id); 576 show_stdout: bool,
577 show_stderr: bool,
578 follow: bool,
579 lines: usize,
580 root_dir: &Path,
581) -> Result<()> {
582 let stdout_file = build_file_path(root_dir, id, "stdout");
583 let stderr_file = build_file_path(root_dir, id, "stderr");
488 584
489 if !follow { 585 if !follow {
490 // Non-follow mode: just show the last n lines and exit 586 // Non-follow mode: just show the last n lines and exit
491 let mut files_found = false; 587 let mut files_found = false;
492 588
493 if show_stdout && Path::new(&stdout_file).exists() { 589 if show_stdout && stdout_file.exists() {
494 let content = read_last_n_lines(&stdout_file, lines)?; 590 let content = read_last_n_lines(&stdout_file, lines)?;
495 if !content.is_empty() { 591 if !content.is_empty() {
496 files_found = true; 592 files_found = true;
497 if show_stderr { 593 if show_stderr {
498 println!("==> {} <==", stdout_file); 594 println!("==> {} <==", stdout_file.display());
499 } 595 }
500 print!("{}", content); 596 print!("{}", content);
501 } 597 }
502 } 598 }
503 599
504 if show_stderr && Path::new(&stderr_file).exists() { 600 if show_stderr && stderr_file.exists() {
505 let content = read_last_n_lines(&stderr_file, lines)?; 601 let content = read_last_n_lines(&stderr_file, lines)?;
506 if !content.is_empty() { 602 if !content.is_empty() {
507 files_found = true; 603 files_found = true;
508 if show_stdout { 604 if show_stdout {
509 println!("==> {} <==", stderr_file); 605 println!("==> {} <==", stderr_file.display());
510 } 606 }
511 print!("{}", content); 607 print!("{}", content);
512 } 608 }
@@ -520,15 +616,15 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
520 } 616 }
521 617
522 // Follow mode: original real-time monitoring behavior 618 // Follow mode: original real-time monitoring behavior
523 let mut file_positions: std::collections::HashMap<String, u64> = 619 let mut file_positions: std::collections::HashMap<PathBuf, u64> =
524 std::collections::HashMap::new(); 620 std::collections::HashMap::new();
525 621
526 if show_stdout && Path::new(&stdout_file).exists() { 622 if show_stdout && stdout_file.exists() {
527 let mut file = File::open(&stdout_file)?; 623 let mut file = File::open(&stdout_file)?;
528 let initial_content = read_file_content(&mut file)?; 624 let initial_content = read_file_content(&mut file)?;
529 if !initial_content.is_empty() { 625 if !initial_content.is_empty() {
530 if show_stderr { 626 if show_stderr {
531 println!("==> {} <==", stdout_file); 627 println!("==> {} <==", stdout_file.display());
532 } 628 }
533 print!("{}", initial_content); 629 print!("{}", initial_content);
534 } 630 }
@@ -536,14 +632,14 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
536 file_positions.insert(stdout_file.clone(), position); 632 file_positions.insert(stdout_file.clone(), position);
537 } 633 }
538 634
539 if show_stderr && Path::new(&stderr_file).exists() { 635 if show_stderr && stderr_file.exists() {
540 let mut file = File::open(&stderr_file)?; 636 let mut file = File::open(&stderr_file)?;
541 let initial_content = read_file_content(&mut file)?; 637 let initial_content = read_file_content(&mut file)?;
542 if !initial_content.is_empty() { 638 if !initial_content.is_empty() {
543 if show_stdout && file_positions.len() > 0 { 639 if show_stdout && file_positions.len() > 0 {
544 println!("\n==> {} <==", stderr_file); 640 println!("\n==> {} <==", stderr_file.display());
545 } else if show_stdout { 641 } else if show_stdout {
546 println!("==> {} <==", stderr_file); 642 println!("==> {} <==", stderr_file.display());
547 } 643 }
548 print!("{}", initial_content); 644 print!("{}", initial_content);
549 } 645 }
@@ -564,8 +660,8 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
564 let (tx, rx) = channel(); 660 let (tx, rx) = channel();
565 let mut watcher = RecommendedWatcher::new(tx, Config::default())?; 661 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
566 662
567 // Watch the current directory for new files and changes 663 // Watch the root directory for new files and changes
568 watcher.watch(Path::new("."), RecursiveMode::NonRecursive)?; 664 watcher.watch(root_dir, RecursiveMode::NonRecursive)?;
569 665
570 // Handle Ctrl+C gracefully 666 // Handle Ctrl+C gracefully
571 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); 667 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
@@ -585,13 +681,11 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
585 .. 681 ..
586 }) => { 682 }) => {
587 for path in paths { 683 for path in paths {
588 let path_str = path.to_string_lossy().to_string(); 684 if (show_stdout && path == stdout_file)
589 685 || (show_stderr && path == stderr_file)
590 if (show_stdout && path_str == stdout_file)
591 || (show_stderr && path_str == stderr_file)
592 { 686 {
593 if let Err(e) = handle_file_change( 687 if let Err(e) = handle_file_change(
594 &path_str, 688 &path,
595 &mut file_positions, 689 &mut file_positions,
596 show_stdout && show_stderr, 690 show_stdout && show_stderr,
597 ) { 691 ) {
@@ -607,16 +701,14 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
607 }) => { 701 }) => {
608 // Handle file creation 702 // Handle file creation
609 for path in paths { 703 for path in paths {
610 let path_str = path.to_string_lossy().to_string(); 704 if (show_stdout && path == stdout_file)
611 705 || (show_stderr && path == stderr_file)
612 if (show_stdout && path_str == stdout_file)
613 || (show_stderr && path_str == stderr_file)
614 { 706 {
615 tracing::info!("New file detected: {}", path_str); 707 tracing::info!("New file detected: {}", path.display());
616 file_positions.insert(path_str.clone(), 0); 708 file_positions.insert(path.clone(), 0);
617 709
618 if let Err(e) = handle_file_change( 710 if let Err(e) = handle_file_change(
619 &path_str, 711 &path,
620 &mut file_positions, 712 &mut file_positions,
621 show_stdout && show_stderr, 713 show_stdout && show_stderr,
622 ) { 714 ) {
@@ -649,26 +741,22 @@ fn read_file_content(file: &mut File) -> Result<String> {
649 Ok(content) 741 Ok(content)
650} 742}
651 743
652fn read_last_n_lines(file_path: &str, n: usize) -> Result<String> { 744fn read_last_n_lines<P: AsRef<Path>>(file_path: P, n: usize) -> Result<String> {
653 let content = std::fs::read_to_string(file_path)?; 745 let content = std::fs::read_to_string(file_path)?;
654 if content.is_empty() { 746 if content.is_empty() {
655 return Ok(String::new()); 747 return Ok(String::new());
656 } 748 }
657 749
658 let lines: Vec<&str> = content.lines().collect(); 750 let lines: Vec<&str> = content.lines().collect();
659 let start_index = if lines.len() > n { 751 let start_index = if lines.len() > n { lines.len() - n } else { 0 };
660 lines.len() - n 752
661 } else {
662 0
663 };
664
665 let last_lines: Vec<&str> = lines[start_index..].to_vec(); 753 let last_lines: Vec<&str> = lines[start_index..].to_vec();
666 Ok(last_lines.join("\n") + if content.ends_with('\n') { "\n" } else { "" }) 754 Ok(last_lines.join("\n") + if content.ends_with('\n') { "\n" } else { "" })
667} 755}
668 756
669fn handle_file_change( 757fn handle_file_change(
670 file_path: &str, 758 file_path: &Path,
671 positions: &mut std::collections::HashMap<String, u64>, 759 positions: &mut std::collections::HashMap<PathBuf, u64>,
672 show_headers: bool, 760 show_headers: bool,
673) -> Result<()> { 761) -> Result<()> {
674 let mut file = File::open(file_path)?; 762 let mut file = File::open(file_path)?;
@@ -683,20 +771,20 @@ fn handle_file_change(
683 771
684 if !new_content.is_empty() { 772 if !new_content.is_empty() {
685 if show_headers { 773 if show_headers {
686 println!("==> {} <==", file_path); 774 println!("==> {} <==", file_path.display());
687 } 775 }
688 print!("{}", new_content); 776 print!("{}", new_content);
689 std::io::Write::flush(&mut std::io::stdout())?; 777 std::io::Write::flush(&mut std::io::stdout())?;
690 778
691 // Update position 779 // Update position
692 let new_pos = file.seek(SeekFrom::Current(0))?; 780 let new_pos = file.seek(SeekFrom::Current(0))?;
693 positions.insert(file_path.to_string(), new_pos); 781 positions.insert(file_path.to_path_buf(), new_pos);
694 } 782 }
695 783
696 Ok(()) 784 Ok(())
697} 785}
698 786
699fn list_daemons(quiet: bool) -> Result<()> { 787fn list_daemons(quiet: bool, root_dir: &Path) -> Result<()> {
700 if !quiet { 788 if !quiet {
701 println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); 789 println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND");
702 println!("{}", "-".repeat(50)); 790 println!("{}", "-".repeat(50));
@@ -704,14 +792,16 @@ fn list_daemons(quiet: bool) -> Result<()> {
704 792
705 let mut found_any = false; 793 let mut found_any = false;
706 794
707 // Find all .pid files in current directory 795 // Find all .pid files in root directory
708 for entry in find_pid_files()? { 796 for entry in find_pid_files(root_dir)? {
709 found_any = true; 797 found_any = true;
710 let path = entry.path(); 798 let path = entry.path();
711 let path_str = path.to_string_lossy(); 799 let filename = path.file_name()
800 .and_then(|name| name.to_str())
801 .unwrap_or_default();
712 802
713 // Extract ID from filename (remove .pid extension) 803 // Extract ID from filename (remove .pid extension)
714 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); 804 let id = filename.strip_suffix(".pid").unwrap_or(filename);
715 805
716 // Read PID data from file 806 // Read PID data from file
717 match PidFile::read_from_file(&path) { 807 match PidFile::read_from_file(&path) {
@@ -770,13 +860,13 @@ fn list_daemons(quiet: bool) -> Result<()> {
770 Ok(()) 860 Ok(())
771} 861}
772 862
773fn status_daemon(id: &str) -> Result<()> { 863fn status_daemon(id: &str, root_dir: &Path) -> Result<()> {
774 let pid_file = format!("{}.pid", id); 864 let pid_file = build_file_path(root_dir, id, "pid");
775 let stdout_file = format!("{}.stdout", id); 865 let stdout_file = build_file_path(root_dir, id, "stdout");
776 let stderr_file = format!("{}.stderr", id); 866 let stderr_file = build_file_path(root_dir, id, "stderr");
777 867
778 println!("Daemon: {}", id); 868 println!("Daemon: {}", id);
779 println!("PID file: {}", pid_file); 869 println!("PID file: {}", pid_file.display());
780 870
781 // Read PID data from file 871 // Read PID data from file
782 match PidFile::read_from_file(&pid_file) { 872 match PidFile::read_from_file(&pid_file) {
@@ -788,18 +878,18 @@ fn status_daemon(id: &str) -> Result<()> {
788 println!("Status: RUNNING"); 878 println!("Status: RUNNING");
789 879
790 // Show file information 880 // Show file information
791 if Path::new(&stdout_file).exists() { 881 if stdout_file.exists() {
792 let metadata = std::fs::metadata(&stdout_file)?; 882 let metadata = std::fs::metadata(&stdout_file)?;
793 println!("Stdout file: {} ({} bytes)", stdout_file, metadata.len()); 883 println!("Stdout file: {} ({} bytes)", stdout_file.display(), metadata.len());
794 } else { 884 } else {
795 println!("Stdout file: {} (not found)", stdout_file); 885 println!("Stdout file: {} (not found)", stdout_file.display());
796 } 886 }
797 887
798 if Path::new(&stderr_file).exists() { 888 if stderr_file.exists() {
799 let metadata = std::fs::metadata(&stderr_file)?; 889 let metadata = std::fs::metadata(&stderr_file)?;
800 println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); 890 println!("Stderr file: {} ({} bytes)", stderr_file.display(), metadata.len());
801 } else { 891 } else {
802 println!("Stderr file: {} (not found)", stderr_file); 892 println!("Stderr file: {} (not found)", stderr_file.display());
803 } 893 }
804 } else { 894 } else {
805 println!("Status: DEAD (process not running)"); 895 println!("Status: DEAD (process not running)");
@@ -820,16 +910,18 @@ fn status_daemon(id: &str) -> Result<()> {
820 Ok(()) 910 Ok(())
821} 911}
822 912
823fn clean_orphaned_files() -> Result<()> { 913fn clean_orphaned_files(root_dir: &Path) -> Result<()> {
824 tracing::info!("Scanning for orphaned daemon files..."); 914 tracing::info!("Scanning for orphaned daemon files...");
825 915
826 let mut cleaned_count = 0; 916 let mut cleaned_count = 0;
827 917
828 // Find all .pid files in current directory 918 // Find all .pid files in root directory
829 for entry in find_pid_files()? { 919 for entry in find_pid_files(root_dir)? {
830 let path = entry.path(); 920 let path = entry.path();
831 let path_str = path.to_string_lossy(); 921 let filename = path.file_name()
832 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); 922 .and_then(|name| name.to_str())
923 .unwrap_or_default();
924 let id = filename.strip_suffix(".pid").unwrap_or(filename);
833 925
834 // Read PID data from file 926 // Read PID data from file
835 match PidFile::read_from_file(&path) { 927 match PidFile::read_from_file(&path) {
@@ -843,28 +935,28 @@ fn clean_orphaned_files() -> Result<()> {
843 935
844 // Remove PID file 936 // Remove PID file
845 if let Err(e) = std::fs::remove_file(&path) { 937 if let Err(e) = std::fs::remove_file(&path) {
846 tracing::warn!("Failed to remove {}: {}", path_str, e); 938 tracing::warn!("Failed to remove {}: {}", path.display(), e);
847 } else { 939 } else {
848 tracing::info!("Removed {}", path_str); 940 tracing::info!("Removed {}", path.display());
849 } 941 }
850 942
851 // Remove stdout file if it exists 943 // Remove stdout file if it exists
852 let stdout_file = format!("{}.stdout", id); 944 let stdout_file = build_file_path(root_dir, id, "stdout");
853 if Path::new(&stdout_file).exists() { 945 if stdout_file.exists() {
854 if let Err(e) = std::fs::remove_file(&stdout_file) { 946 if let Err(e) = std::fs::remove_file(&stdout_file) {
855 tracing::warn!("Failed to remove {}: {}", stdout_file, e); 947 tracing::warn!("Failed to remove {}: {}", stdout_file.display(), e);
856 } else { 948 } else {
857 tracing::info!("Removed {}", stdout_file); 949 tracing::info!("Removed {}", stdout_file.display());
858 } 950 }
859 } 951 }
860 952
861 // Remove stderr file if it exists 953 // Remove stderr file if it exists
862 let stderr_file = format!("{}.stderr", id); 954 let stderr_file = build_file_path(root_dir, id, "stderr");
863 if Path::new(&stderr_file).exists() { 955 if stderr_file.exists() {
864 if let Err(e) = std::fs::remove_file(&stderr_file) { 956 if let Err(e) = std::fs::remove_file(&stderr_file) {
865 tracing::warn!("Failed to remove {}: {}", stderr_file, e); 957 tracing::warn!("Failed to remove {}: {}", stderr_file.display(), e);
866 } else { 958 } else {
867 tracing::info!("Removed {}", stderr_file); 959 tracing::info!("Removed {}", stderr_file.display());
868 } 960 }
869 } 961 }
870 962
@@ -879,14 +971,14 @@ fn clean_orphaned_files() -> Result<()> {
879 } 971 }
880 Err(PidFileReadError::FileNotFound) => { 972 Err(PidFileReadError::FileNotFound) => {
881 // This shouldn't happen since we found the file, but handle gracefully 973 // This shouldn't happen since we found the file, but handle gracefully
882 tracing::warn!("PID file {} disappeared during processing", path_str); 974 tracing::warn!("PID file {} disappeared during processing", path.display());
883 } 975 }
884 Err(PidFileReadError::FileInvalid(_)) | Err(PidFileReadError::IoError(_)) => { 976 Err(PidFileReadError::FileInvalid(_)) | Err(PidFileReadError::IoError(_)) => {
885 println!("Cleaning up invalid PID file: {}", path_str); 977 println!("Cleaning up invalid PID file: {}", path.display());
886 if let Err(e) = std::fs::remove_file(&path) { 978 if let Err(e) = std::fs::remove_file(&path) {
887 tracing::warn!("Failed to remove invalid PID file {}: {}", path_str, e); 979 tracing::warn!("Failed to remove invalid PID file {}: {}", path.display(), e);
888 } else { 980 } else {
889 tracing::info!("Removed invalid PID file {}", path_str); 981 tracing::info!("Removed invalid PID file {}", path.display());
890 cleaned_count += 1; 982 cleaned_count += 1;
891 } 983 }
892 } 984 }
@@ -1164,8 +1256,8 @@ This tool is designed for Linux environments and provides a simple interface for
1164 ); 1256 );
1165} 1257}
1166 1258
1167fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> { 1259fn wait_daemon(id: &str, timeout: u64, interval: u64, root_dir: &Path) -> Result<()> {
1168 let pid_file = format!("{}.pid", id); 1260 let pid_file = build_file_path(root_dir, id, "pid");
1169 1261
1170 // Check if PID file exists and read PID data 1262 // Check if PID file exists and read PID data
1171 let pid_file_data = match PidFile::read_from_file(&pid_file) { 1263 let pid_file_data = match PidFile::read_from_file(&pid_file) {
@@ -1174,10 +1266,18 @@ fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> {
1174 return Err(anyhow::anyhow!("Process '{}' not found (no PID file)", id)); 1266 return Err(anyhow::anyhow!("Process '{}' not found (no PID file)", id));
1175 } 1267 }
1176 Err(PidFileReadError::FileInvalid(reason)) => { 1268 Err(PidFileReadError::FileInvalid(reason)) => {
1177 return Err(anyhow::anyhow!("Process '{}' has invalid PID file: {}", id, reason)); 1269 return Err(anyhow::anyhow!(
1270 "Process '{}' has invalid PID file: {}",
1271 id,
1272 reason
1273 ));
1178 } 1274 }
1179 Err(PidFileReadError::IoError(err)) => { 1275 Err(PidFileReadError::IoError(err)) => {
1180 return Err(anyhow::anyhow!("Failed to read PID file for '{}': {}", id, err)); 1276 return Err(anyhow::anyhow!(
1277 "Failed to read PID file for '{}': {}",
1278 id,
1279 err
1280 ));
1181 } 1281 }
1182 }; 1282 };
1183 1283
@@ -1208,17 +1308,20 @@ fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> {
1208 tracing::info!("Process '{}' (PID: {}) has terminated", id, pid); 1308 tracing::info!("Process '{}' (PID: {}) has terminated", id, pid);
1209 return Ok(()); 1309 return Ok(());
1210 } 1310 }
1211 1311
1212 thread::sleep(Duration::from_secs(interval)); 1312 thread::sleep(Duration::from_secs(interval));
1213 elapsed += interval; 1313 elapsed += interval;
1214 } 1314 }
1215 1315
1216 // Timeout reached 1316 // Timeout reached
1217 Err(anyhow::anyhow!("Timeout reached waiting for process '{}' to terminate", id)) 1317 Err(anyhow::anyhow!(
1318 "Timeout reached waiting for process '{}' to terminate",
1319 id
1320 ))
1218} 1321}
1219 1322
1220fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { 1323fn find_pid_files(root_dir: &Path) -> Result<Vec<std::fs::DirEntry>> {
1221 let entries = std::fs::read_dir(".")? 1324 let entries = std::fs::read_dir(root_dir)?
1222 .filter_map(|entry| { 1325 .filter_map(|entry| {
1223 entry.ok().and_then(|e| { 1326 entry.ok().and_then(|e| {
1224 e.path() 1327 e.path()
diff --git a/tests/cli.rs b/tests/cli.rs
index 285eb70..37ceb7f 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -35,8 +35,7 @@ fn test_run_missing_command() {
35 let temp_dir = TempDir::new().unwrap(); 35 let temp_dir = TempDir::new().unwrap();
36 36
37 let mut cmd = Command::cargo_bin("demon").unwrap(); 37 let mut cmd = Command::cargo_bin("demon").unwrap();
38 cmd.current_dir(temp_dir.path()) 38 cmd.args(&["run", "--root-dir", temp_dir.path().to_str().unwrap(), "test"])
39 .args(&["run", "test"])
40 .assert() 39 .assert()
41 .failure() 40 .failure()
42 .stderr(predicate::str::contains("Command cannot be empty")); 41 .stderr(predicate::str::contains("Command cannot be empty"));
@@ -47,8 +46,7 @@ fn test_run_creates_files() {
47 let temp_dir = TempDir::new().unwrap(); 46 let temp_dir = TempDir::new().unwrap();
48 47
49 let mut cmd = Command::cargo_bin("demon").unwrap(); 48 let mut cmd = Command::cargo_bin("demon").unwrap();
50 cmd.current_dir(temp_dir.path()) 49 cmd.args(&["run", "--root-dir", temp_dir.path().to_str().unwrap(), "test", "echo", "hello"])
51 .args(&["run", "test", "echo", "hello"])
52 .assert() 50 .assert()
53 .success() 51 .success()
54 .stdout(predicate::str::contains("Started daemon 'test'")); 52 .stdout(predicate::str::contains("Started daemon 'test'"));
@@ -72,14 +70,13 @@ fn test_run_duplicate_process() {
72 70
73 // Start a long-running process 71 // Start a long-running process
74 let mut cmd = Command::cargo_bin("demon").unwrap(); 72 let mut cmd = Command::cargo_bin("demon").unwrap();
75 cmd.current_dir(temp_dir.path()) 73 cmd.args(&["run", "--root-dir", temp_dir.path().to_str().unwrap(), "long", "sleep", "30"])
76 .args(&["run", "long", "sleep", "30"])
77 .assert() 74 .assert()
78 .success(); 75 .success();
79 76
80 // Try to start another with the same ID 77 // Try to start another with the same ID
81 let mut cmd = Command::cargo_bin("demon").unwrap(); 78 let mut cmd = Command::cargo_bin("demon").unwrap();
82 cmd.current_dir(temp_dir.path()) 79 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
83 .args(&["run", "long", "sleep", "5"]) 80 .args(&["run", "long", "sleep", "5"])
84 .assert() 81 .assert()
85 .failure() 82 .failure()
@@ -87,7 +84,7 @@ fn test_run_duplicate_process() {
87 84
88 // Clean up the running process 85 // Clean up the running process
89 let mut cmd = Command::cargo_bin("demon").unwrap(); 86 let mut cmd = Command::cargo_bin("demon").unwrap();
90 cmd.current_dir(temp_dir.path()) 87 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
91 .args(&["stop", "long"]) 88 .args(&["stop", "long"])
92 .assert() 89 .assert()
93 .success(); 90 .success();
@@ -98,7 +95,7 @@ fn test_list_empty() {
98 let temp_dir = TempDir::new().unwrap(); 95 let temp_dir = TempDir::new().unwrap();
99 96
100 let mut cmd = Command::cargo_bin("demon").unwrap(); 97 let mut cmd = Command::cargo_bin("demon").unwrap();
101 cmd.current_dir(temp_dir.path()) 98 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
102 .args(&["list"]) 99 .args(&["list"])
103 .assert() 100 .assert()
104 .success() 101 .success()
@@ -114,14 +111,14 @@ fn test_list_with_processes() {
114 111
115 // Start a process 112 // Start a process
116 let mut cmd = Command::cargo_bin("demon").unwrap(); 113 let mut cmd = Command::cargo_bin("demon").unwrap();
117 cmd.current_dir(temp_dir.path()) 114 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
118 .args(&["run", "test", "echo", "done"]) 115 .args(&["run", "test", "echo", "done"])
119 .assert() 116 .assert()
120 .success(); 117 .success();
121 118
122 // List processes 119 // List processes
123 let mut cmd = Command::cargo_bin("demon").unwrap(); 120 let mut cmd = Command::cargo_bin("demon").unwrap();
124 cmd.current_dir(temp_dir.path()) 121 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
125 .args(&["list"]) 122 .args(&["list"])
126 .assert() 123 .assert()
127 .success() 124 .success()
@@ -135,7 +132,7 @@ fn test_cat_output() {
135 132
136 // Create a process with output 133 // Create a process with output
137 let mut cmd = Command::cargo_bin("demon").unwrap(); 134 let mut cmd = Command::cargo_bin("demon").unwrap();
138 cmd.current_dir(temp_dir.path()) 135 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
139 .args(&[ 136 .args(&[
140 "run", 137 "run",
141 "test", 138 "test",
@@ -149,7 +146,7 @@ fn test_cat_output() {
149 146
150 // Cat the output 147 // Cat the output
151 let mut cmd = Command::cargo_bin("demon").unwrap(); 148 let mut cmd = Command::cargo_bin("demon").unwrap();
152 cmd.current_dir(temp_dir.path()) 149 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
153 .args(&["cat", "test"]) 150 .args(&["cat", "test"])
154 .assert() 151 .assert()
155 .success() 152 .success()
@@ -163,7 +160,7 @@ fn test_cat_stdout_only() {
163 160
164 // Create a process with output 161 // Create a process with output
165 let mut cmd = Command::cargo_bin("demon").unwrap(); 162 let mut cmd = Command::cargo_bin("demon").unwrap();
166 cmd.current_dir(temp_dir.path()) 163 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
167 .args(&[ 164 .args(&[
168 "run", 165 "run",
169 "test", 166 "test",
@@ -177,7 +174,7 @@ fn test_cat_stdout_only() {
177 174
178 // Cat only stdout 175 // Cat only stdout
179 let mut cmd = Command::cargo_bin("demon").unwrap(); 176 let mut cmd = Command::cargo_bin("demon").unwrap();
180 cmd.current_dir(temp_dir.path()) 177 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
181 .args(&["cat", "test", "--stdout"]) 178 .args(&["cat", "test", "--stdout"])
182 .assert() 179 .assert()
183 .success() 180 .success()
@@ -190,7 +187,7 @@ fn test_status_nonexistent() {
190 let temp_dir = TempDir::new().unwrap(); 187 let temp_dir = TempDir::new().unwrap();
191 188
192 let mut cmd = Command::cargo_bin("demon").unwrap(); 189 let mut cmd = Command::cargo_bin("demon").unwrap();
193 cmd.current_dir(temp_dir.path()) 190 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
194 .args(&["status", "nonexistent"]) 191 .args(&["status", "nonexistent"])
195 .assert() 192 .assert()
196 .success() 193 .success()
@@ -203,14 +200,14 @@ fn test_status_dead_process() {
203 200
204 // Create a short-lived process 201 // Create a short-lived process
205 let mut cmd = Command::cargo_bin("demon").unwrap(); 202 let mut cmd = Command::cargo_bin("demon").unwrap();
206 cmd.current_dir(temp_dir.path()) 203 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
207 .args(&["run", "dead", "echo", "hello"]) 204 .args(&["run", "dead", "echo", "hello"])
208 .assert() 205 .assert()
209 .success(); 206 .success();
210 207
211 // Check its status (should be dead) 208 // Check its status (should be dead)
212 let mut cmd = Command::cargo_bin("demon").unwrap(); 209 let mut cmd = Command::cargo_bin("demon").unwrap();
213 cmd.current_dir(temp_dir.path()) 210 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
214 .args(&["status", "dead"]) 211 .args(&["status", "dead"])
215 .assert() 212 .assert()
216 .success() 213 .success()
@@ -222,7 +219,7 @@ fn test_stop_nonexistent() {
222 let temp_dir = TempDir::new().unwrap(); 219 let temp_dir = TempDir::new().unwrap();
223 220
224 let mut cmd = Command::cargo_bin("demon").unwrap(); 221 let mut cmd = Command::cargo_bin("demon").unwrap();
225 cmd.current_dir(temp_dir.path()) 222 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
226 .args(&["stop", "nonexistent"]) 223 .args(&["stop", "nonexistent"])
227 .assert() 224 .assert()
228 .success() 225 .success()
@@ -235,14 +232,14 @@ fn test_stop_process() {
235 232
236 // Start a long-running process 233 // Start a long-running process
237 let mut cmd = Command::cargo_bin("demon").unwrap(); 234 let mut cmd = Command::cargo_bin("demon").unwrap();
238 cmd.current_dir(temp_dir.path()) 235 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
239 .args(&["run", "long", "sleep", "10"]) 236 .args(&["run", "long", "sleep", "10"])
240 .assert() 237 .assert()
241 .success(); 238 .success();
242 239
243 // Stop it 240 // Stop it
244 let mut cmd = Command::cargo_bin("demon").unwrap(); 241 let mut cmd = Command::cargo_bin("demon").unwrap();
245 cmd.current_dir(temp_dir.path()) 242 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
246 .args(&["stop", "long"]) 243 .args(&["stop", "long"])
247 .assert() 244 .assert()
248 .success() 245 .success()
@@ -257,7 +254,7 @@ fn test_clean_no_orphans() {
257 let temp_dir = TempDir::new().unwrap(); 254 let temp_dir = TempDir::new().unwrap();
258 255
259 let mut cmd = Command::cargo_bin("demon").unwrap(); 256 let mut cmd = Command::cargo_bin("demon").unwrap();
260 cmd.current_dir(temp_dir.path()) 257 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
261 .args(&["clean"]) 258 .args(&["clean"])
262 .assert() 259 .assert()
263 .success() 260 .success()
@@ -270,14 +267,14 @@ fn test_clean_with_orphans() {
270 267
271 // Create a dead process 268 // Create a dead process
272 let mut cmd = Command::cargo_bin("demon").unwrap(); 269 let mut cmd = Command::cargo_bin("demon").unwrap();
273 cmd.current_dir(temp_dir.path()) 270 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
274 .args(&["run", "dead", "echo", "hello"]) 271 .args(&["run", "dead", "echo", "hello"])
275 .assert() 272 .assert()
276 .success(); 273 .success();
277 274
278 // Clean up orphaned files 275 // Clean up orphaned files
279 let mut cmd = Command::cargo_bin("demon").unwrap(); 276 let mut cmd = Command::cargo_bin("demon").unwrap();
280 cmd.current_dir(temp_dir.path()) 277 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
281 .args(&["clean"]) 278 .args(&["clean"])
282 .assert() 279 .assert()
283 .success() 280 .success()
@@ -295,7 +292,7 @@ fn test_run_with_complex_command() {
295 let temp_dir = TempDir::new().unwrap(); 292 let temp_dir = TempDir::new().unwrap();
296 293
297 let mut cmd = Command::cargo_bin("demon").unwrap(); 294 let mut cmd = Command::cargo_bin("demon").unwrap();
298 cmd.current_dir(temp_dir.path()) 295 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
299 .args(&[ 296 .args(&[
300 "run", 297 "run",
301 "complex", 298 "complex",
@@ -323,14 +320,14 @@ fn test_timeout_configuration() {
323 320
324 // Start a process 321 // Start a process
325 let mut cmd = Command::cargo_bin("demon").unwrap(); 322 let mut cmd = Command::cargo_bin("demon").unwrap();
326 cmd.current_dir(temp_dir.path()) 323 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
327 .args(&["run", "timeout-test", "sleep", "5"]) 324 .args(&["run", "timeout-test", "sleep", "5"])
328 .assert() 325 .assert()
329 .success(); 326 .success();
330 327
331 // Stop with custom timeout (should work normally since sleep responds to SIGTERM) 328 // Stop with custom timeout (should work normally since sleep responds to SIGTERM)
332 let mut cmd = Command::cargo_bin("demon").unwrap(); 329 let mut cmd = Command::cargo_bin("demon").unwrap();
333 cmd.current_dir(temp_dir.path()) 330 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
334 .args(&["stop", "timeout-test", "--timeout", "2"]) 331 .args(&["stop", "timeout-test", "--timeout", "2"])
335 .assert() 332 .assert()
336 .success() 333 .success()
@@ -346,7 +343,7 @@ fn test_invalid_process_id() {
346 343
347 // Status should handle it gracefully 344 // Status should handle it gracefully
348 let mut cmd = Command::cargo_bin("demon").unwrap(); 345 let mut cmd = Command::cargo_bin("demon").unwrap();
349 cmd.current_dir(temp_dir.path()) 346 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
350 .args(&["status", "invalid"]) 347 .args(&["status", "invalid"])
351 .assert() 348 .assert()
352 .success() 349 .success()
@@ -354,7 +351,7 @@ fn test_invalid_process_id() {
354 351
355 // Clean should remove it 352 // Clean should remove it
356 let mut cmd = Command::cargo_bin("demon").unwrap(); 353 let mut cmd = Command::cargo_bin("demon").unwrap();
357 cmd.current_dir(temp_dir.path()) 354 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
358 .args(&["clean"]) 355 .args(&["clean"])
359 .assert() 356 .assert()
360 .success() 357 .success()
@@ -367,7 +364,7 @@ fn test_list_quiet_mode() {
367 364
368 // Test quiet mode with no processes 365 // Test quiet mode with no processes
369 let mut cmd = Command::cargo_bin("demon").unwrap(); 366 let mut cmd = Command::cargo_bin("demon").unwrap();
370 cmd.current_dir(temp_dir.path()) 367 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
371 .args(&["list", "--quiet"]) 368 .args(&["list", "--quiet"])
372 .assert() 369 .assert()
373 .success() 370 .success()
@@ -375,14 +372,14 @@ fn test_list_quiet_mode() {
375 372
376 // Create a process 373 // Create a process
377 let mut cmd = Command::cargo_bin("demon").unwrap(); 374 let mut cmd = Command::cargo_bin("demon").unwrap();
378 cmd.current_dir(temp_dir.path()) 375 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
379 .args(&["run", "quiet-test", "echo", "done"]) 376 .args(&["run", "quiet-test", "echo", "done"])
380 .assert() 377 .assert()
381 .success(); 378 .success();
382 379
383 // Test quiet mode with process - should output colon-separated format 380 // Test quiet mode with process - should output colon-separated format
384 let mut cmd = Command::cargo_bin("demon").unwrap(); 381 let mut cmd = Command::cargo_bin("demon").unwrap();
385 cmd.current_dir(temp_dir.path()) 382 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
386 .args(&["list", "-q"]) 383 .args(&["list", "-q"])
387 .assert() 384 .assert()
388 .success() 385 .success()
@@ -422,7 +419,7 @@ fn test_wait_nonexistent_process() {
422 let temp_dir = TempDir::new().unwrap(); 419 let temp_dir = TempDir::new().unwrap();
423 420
424 let mut cmd = Command::cargo_bin("demon").unwrap(); 421 let mut cmd = Command::cargo_bin("demon").unwrap();
425 cmd.current_dir(temp_dir.path()) 422 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
426 .args(&["wait", "nonexistent"]) 423 .args(&["wait", "nonexistent"])
427 .assert() 424 .assert()
428 .failure() 425 .failure()
@@ -435,7 +432,7 @@ fn test_wait_already_dead_process() {
435 432
436 // Create a short-lived process 433 // Create a short-lived process
437 let mut cmd = Command::cargo_bin("demon").unwrap(); 434 let mut cmd = Command::cargo_bin("demon").unwrap();
438 cmd.current_dir(temp_dir.path()) 435 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
439 .args(&["run", "dead", "echo", "hello"]) 436 .args(&["run", "dead", "echo", "hello"])
440 .assert() 437 .assert()
441 .success(); 438 .success();
@@ -445,7 +442,7 @@ fn test_wait_already_dead_process() {
445 442
446 // Try to wait for it (should fail since it's already dead) 443 // Try to wait for it (should fail since it's already dead)
447 let mut cmd = Command::cargo_bin("demon").unwrap(); 444 let mut cmd = Command::cargo_bin("demon").unwrap();
448 cmd.current_dir(temp_dir.path()) 445 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
449 .args(&["wait", "dead"]) 446 .args(&["wait", "dead"])
450 .assert() 447 .assert()
451 .failure() 448 .failure()
@@ -458,14 +455,14 @@ fn test_wait_process_terminates() {
458 455
459 // Start a process that will run for 2 seconds 456 // Start a process that will run for 2 seconds
460 let mut cmd = Command::cargo_bin("demon").unwrap(); 457 let mut cmd = Command::cargo_bin("demon").unwrap();
461 cmd.current_dir(temp_dir.path()) 458 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
462 .args(&["run", "short", "sleep", "2"]) 459 .args(&["run", "short", "sleep", "2"])
463 .assert() 460 .assert()
464 .success(); 461 .success();
465 462
466 // Wait for it with a 5-second timeout (should succeed) 463 // Wait for it with a 5-second timeout (should succeed)
467 let mut cmd = Command::cargo_bin("demon").unwrap(); 464 let mut cmd = Command::cargo_bin("demon").unwrap();
468 cmd.current_dir(temp_dir.path()) 465 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
469 .args(&["wait", "short", "--timeout", "5"]) 466 .args(&["wait", "short", "--timeout", "5"])
470 .assert() 467 .assert()
471 .success(); 468 .success();
@@ -477,14 +474,14 @@ fn test_wait_timeout() {
477 474
478 // Start a long-running process 475 // Start a long-running process
479 let mut cmd = Command::cargo_bin("demon").unwrap(); 476 let mut cmd = Command::cargo_bin("demon").unwrap();
480 cmd.current_dir(temp_dir.path()) 477 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
481 .args(&["run", "long", "sleep", "10"]) 478 .args(&["run", "long", "sleep", "10"])
482 .assert() 479 .assert()
483 .success(); 480 .success();
484 481
485 // Wait with a very short timeout (should fail) 482 // Wait with a very short timeout (should fail)
486 let mut cmd = Command::cargo_bin("demon").unwrap(); 483 let mut cmd = Command::cargo_bin("demon").unwrap();
487 cmd.current_dir(temp_dir.path()) 484 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
488 .args(&["wait", "long", "--timeout", "2"]) 485 .args(&["wait", "long", "--timeout", "2"])
489 .assert() 486 .assert()
490 .failure() 487 .failure()
@@ -492,7 +489,7 @@ fn test_wait_timeout() {
492 489
493 // Clean up the still-running process 490 // Clean up the still-running process
494 let mut cmd = Command::cargo_bin("demon").unwrap(); 491 let mut cmd = Command::cargo_bin("demon").unwrap();
495 cmd.current_dir(temp_dir.path()) 492 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
496 .args(&["stop", "long"]) 493 .args(&["stop", "long"])
497 .assert() 494 .assert()
498 .success(); 495 .success();
@@ -504,14 +501,14 @@ fn test_wait_infinite_timeout() {
504 501
505 // Start a short process that will finish quickly 502 // Start a short process that will finish quickly
506 let mut cmd = Command::cargo_bin("demon").unwrap(); 503 let mut cmd = Command::cargo_bin("demon").unwrap();
507 cmd.current_dir(temp_dir.path()) 504 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
508 .args(&["run", "quick", "sleep", "1"]) 505 .args(&["run", "quick", "sleep", "1"])
509 .assert() 506 .assert()
510 .success(); 507 .success();
511 508
512 // Wait with infinite timeout (should succeed quickly) 509 // Wait with infinite timeout (should succeed quickly)
513 let mut cmd = Command::cargo_bin("demon").unwrap(); 510 let mut cmd = Command::cargo_bin("demon").unwrap();
514 cmd.current_dir(temp_dir.path()) 511 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
515 .args(&["wait", "quick", "--timeout", "0"]) 512 .args(&["wait", "quick", "--timeout", "0"])
516 .assert() 513 .assert()
517 .success(); 514 .success();
@@ -523,14 +520,14 @@ fn test_wait_custom_interval() {
523 520
524 // Start a short process 521 // Start a short process
525 let mut cmd = Command::cargo_bin("demon").unwrap(); 522 let mut cmd = Command::cargo_bin("demon").unwrap();
526 cmd.current_dir(temp_dir.path()) 523 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
527 .args(&["run", "interval-test", "sleep", "2"]) 524 .args(&["run", "interval-test", "sleep", "2"])
528 .assert() 525 .assert()
529 .success(); 526 .success();
530 527
531 // Wait with custom interval (should still succeed) 528 // Wait with custom interval (should still succeed)
532 let mut cmd = Command::cargo_bin("demon").unwrap(); 529 let mut cmd = Command::cargo_bin("demon").unwrap();
533 cmd.current_dir(temp_dir.path()) 530 cmd.args(["--root-dir", temp_dir.path().to_str().unwrap()])
534 .args(&["wait", "interval-test", "--timeout", "5", "--interval", "2"]) 531 .args(&["wait", "interval-test", "--timeout", "5", "--interval", "2"])
535 .assert() 532 .assert()
536 .success(); 533 .success();