diff options
| author | diogo464 <[email protected]> | 2025-06-19 09:18:18 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-06-19 09:18:18 +0100 |
| commit | 1c2e20c56d7fdbb0f7b21d12137ec7d58cd839c8 (patch) | |
| tree | 9ea6d12018eacb4b9928fb7b667333682f3b5491 | |
| parent | a8e1212c07fb489cfa430ef64fb3a1c8df464a22 (diff) | |
Format code with rustfmt
- Apply standard Rust formatting conventions
- Improve code readability and consistency
- Reorganize imports alphabetically
- Fix line lengths and indentation
- All tests continue to pass
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
| -rw-r--r-- | src/main.rs | 302 | ||||
| -rw-r--r-- | tests/cli.rs | 105 |
2 files changed, 231 insertions, 176 deletions
diff --git a/src/main.rs b/src/main.rs index 37a7cdb..ac6e305 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -1,13 +1,13 @@ | |||
| 1 | use clap::{Parser, Subcommand, Args}; | 1 | use anyhow::{Context, Result}; |
| 2 | use clap::{Args, Parser, Subcommand}; | ||
| 3 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; | ||
| 2 | use std::fs::File; | 4 | use std::fs::File; |
| 3 | use std::io::{Write, Read, Seek, SeekFrom}; | 5 | use std::io::{Read, Seek, SeekFrom, Write}; |
| 6 | use std::path::Path; | ||
| 4 | use std::process::{Command, Stdio}; | 7 | use std::process::{Command, Stdio}; |
| 8 | use std::sync::mpsc::channel; | ||
| 5 | use std::thread; | 9 | use std::thread; |
| 6 | use std::time::Duration; | 10 | use std::time::Duration; |
| 7 | use std::path::Path; | ||
| 8 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; | ||
| 9 | use std::sync::mpsc::channel; | ||
| 10 | use anyhow::{Result, Context}; | ||
| 11 | 11 | ||
| 12 | #[derive(Parser)] | 12 | #[derive(Parser)] |
| 13 | #[command(name = "demon")] | 13 | #[command(name = "demon")] |
| @@ -23,25 +23,25 @@ struct Cli { | |||
| 23 | enum Commands { | 23 | enum Commands { |
| 24 | /// Spawn a background process and redirect stdout/stderr to files | 24 | /// Spawn a background process and redirect stdout/stderr to files |
| 25 | Run(RunArgs), | 25 | Run(RunArgs), |
| 26 | 26 | ||
| 27 | /// Stop a running daemon process | 27 | /// Stop a running daemon process |
| 28 | Stop(StopArgs), | 28 | Stop(StopArgs), |
| 29 | 29 | ||
| 30 | /// Tail daemon logs in real-time | 30 | /// Tail daemon logs in real-time |
| 31 | Tail(TailArgs), | 31 | Tail(TailArgs), |
| 32 | 32 | ||
| 33 | /// Display daemon log contents | 33 | /// Display daemon log contents |
| 34 | Cat(CatArgs), | 34 | Cat(CatArgs), |
| 35 | 35 | ||
| 36 | /// List all running daemon processes | 36 | /// List all running daemon processes |
| 37 | List(ListArgs), | 37 | List(ListArgs), |
| 38 | 38 | ||
| 39 | /// Check status of a daemon process | 39 | /// Check status of a daemon process |
| 40 | Status(StatusArgs), | 40 | Status(StatusArgs), |
| 41 | 41 | ||
| 42 | /// Clean up orphaned pid and log files | 42 | /// Clean up orphaned pid and log files |
| 43 | Clean, | 43 | Clean, |
| 44 | 44 | ||
| 45 | /// Output comprehensive usage guide for LLMs | 45 | /// Output comprehensive usage guide for LLMs |
| 46 | Llm, | 46 | Llm, |
| 47 | } | 47 | } |
| @@ -51,7 +51,7 @@ struct RunArgs { | |||
| 51 | /// Process identifier | 51 | /// Process identifier |
| 52 | #[arg(long)] | 52 | #[arg(long)] |
| 53 | id: String, | 53 | id: String, |
| 54 | 54 | ||
| 55 | /// Command and arguments to execute | 55 | /// Command and arguments to execute |
| 56 | command: Vec<String>, | 56 | command: Vec<String>, |
| 57 | } | 57 | } |
| @@ -61,7 +61,7 @@ struct StopArgs { | |||
| 61 | /// Process identifier | 61 | /// Process identifier |
| 62 | #[arg(long)] | 62 | #[arg(long)] |
| 63 | id: String, | 63 | id: String, |
| 64 | 64 | ||
| 65 | /// Timeout in seconds before sending SIGKILL after SIGTERM | 65 | /// Timeout in seconds before sending SIGKILL after SIGTERM |
| 66 | #[arg(long, default_value = "10")] | 66 | #[arg(long, default_value = "10")] |
| 67 | timeout: u64, | 67 | timeout: u64, |
| @@ -72,11 +72,11 @@ struct TailArgs { | |||
| 72 | /// Process identifier | 72 | /// Process identifier |
| 73 | #[arg(long)] | 73 | #[arg(long)] |
| 74 | id: String, | 74 | id: String, |
| 75 | 75 | ||
| 76 | /// Only tail stdout | 76 | /// Only tail stdout |
| 77 | #[arg(long)] | 77 | #[arg(long)] |
| 78 | stdout: bool, | 78 | stdout: bool, |
| 79 | 79 | ||
| 80 | /// Only tail stderr | 80 | /// Only tail stderr |
| 81 | #[arg(long)] | 81 | #[arg(long)] |
| 82 | stderr: bool, | 82 | stderr: bool, |
| @@ -87,11 +87,11 @@ struct CatArgs { | |||
| 87 | /// Process identifier | 87 | /// Process identifier |
| 88 | #[arg(long)] | 88 | #[arg(long)] |
| 89 | id: String, | 89 | id: String, |
| 90 | 90 | ||
| 91 | /// Only show stdout | 91 | /// Only show stdout |
| 92 | #[arg(long)] | 92 | #[arg(long)] |
| 93 | stdout: bool, | 93 | stdout: bool, |
| 94 | 94 | ||
| 95 | /// Only show stderr | 95 | /// Only show stderr |
| 96 | #[arg(long)] | 96 | #[arg(long)] |
| 97 | stderr: bool, | 97 | stderr: bool, |
| @@ -117,7 +117,7 @@ fn main() { | |||
| 117 | .init(); | 117 | .init(); |
| 118 | 118 | ||
| 119 | let cli = Cli::parse(); | 119 | let cli = Cli::parse(); |
| 120 | 120 | ||
| 121 | if let Err(e) = run_command(cli.command) { | 121 | if let Err(e) = run_command(cli.command) { |
| 122 | tracing::error!("Error: {}", e); | 122 | tracing::error!("Error: {}", e); |
| 123 | std::process::exit(1); | 123 | std::process::exit(1); |
| @@ -132,9 +132,7 @@ fn run_command(command: Commands) -> Result<()> { | |||
| 132 | } | 132 | } |
| 133 | run_daemon(&args.id, &args.command) | 133 | run_daemon(&args.id, &args.command) |
| 134 | } | 134 | } |
| 135 | Commands::Stop(args) => { | 135 | Commands::Stop(args) => stop_daemon(&args.id, args.timeout), |
| 136 | stop_daemon(&args.id, args.timeout) | ||
| 137 | } | ||
| 138 | Commands::Tail(args) => { | 136 | Commands::Tail(args) => { |
| 139 | let show_stdout = !args.stderr || args.stdout; | 137 | let show_stdout = !args.stderr || args.stdout; |
| 140 | let show_stderr = !args.stdout || args.stderr; | 138 | let show_stderr = !args.stdout || args.stderr; |
| @@ -145,15 +143,9 @@ fn run_command(command: Commands) -> Result<()> { | |||
| 145 | let show_stderr = !args.stdout || args.stderr; | 143 | let show_stderr = !args.stdout || args.stderr; |
| 146 | cat_logs(&args.id, show_stdout, show_stderr) | 144 | cat_logs(&args.id, show_stdout, show_stderr) |
| 147 | } | 145 | } |
| 148 | Commands::List(args) => { | 146 | Commands::List(args) => list_daemons(args.quiet), |
| 149 | list_daemons(args.quiet) | 147 | Commands::Status(args) => status_daemon(&args.id), |
| 150 | } | 148 | Commands::Clean => clean_orphaned_files(), |
| 151 | Commands::Status(args) => { | ||
| 152 | status_daemon(&args.id) | ||
| 153 | } | ||
| 154 | Commands::Clean => { | ||
| 155 | clean_orphaned_files() | ||
| 156 | } | ||
| 157 | Commands::Llm => { | 149 | Commands::Llm => { |
| 158 | print_llm_guide(); | 150 | print_llm_guide(); |
| 159 | Ok(()) | 151 | Ok(()) |
| @@ -165,26 +157,30 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> { | |||
| 165 | let pid_file = format!("{}.pid", id); | 157 | let pid_file = format!("{}.pid", id); |
| 166 | let stdout_file = format!("{}.stdout", id); | 158 | let stdout_file = format!("{}.stdout", id); |
| 167 | let stderr_file = format!("{}.stderr", id); | 159 | let stderr_file = format!("{}.stderr", id); |
| 168 | 160 | ||
| 169 | // Check if process is already running | 161 | // Check if process is already running |
| 170 | if is_process_running(&pid_file)? { | 162 | if is_process_running(&pid_file)? { |
| 171 | return Err(anyhow::anyhow!("Process '{}' is already running", id)); | 163 | return Err(anyhow::anyhow!("Process '{}' is already running", id)); |
| 172 | } | 164 | } |
| 173 | 165 | ||
| 174 | tracing::info!("Starting daemon '{}' with command: {:?}", id, command); | 166 | tracing::info!("Starting daemon '{}' with command: {:?}", id, command); |
| 175 | 167 | ||
| 176 | // Truncate/create output files | 168 | // Truncate/create output files |
| 177 | File::create(&stdout_file)?; | 169 | File::create(&stdout_file)?; |
| 178 | File::create(&stderr_file)?; | 170 | File::create(&stderr_file)?; |
| 179 | 171 | ||
| 180 | // Open files for redirection | 172 | // Open files for redirection |
| 181 | let stdout_redirect = File::create(&stdout_file)?; | 173 | let stdout_redirect = File::create(&stdout_file)?; |
| 182 | let stderr_redirect = File::create(&stderr_file)?; | 174 | let stderr_redirect = File::create(&stderr_file)?; |
| 183 | 175 | ||
| 184 | // Spawn the process | 176 | // Spawn the process |
| 185 | let program = &command[0]; | 177 | let program = &command[0]; |
| 186 | let args = if command.len() > 1 { &command[1..] } else { &[] }; | 178 | let args = if command.len() > 1 { |
| 187 | 179 | &command[1..] | |
| 180 | } else { | ||
| 181 | &[] | ||
| 182 | }; | ||
| 183 | |||
| 188 | let child = Command::new(program) | 184 | let child = Command::new(program) |
| 189 | .args(args) | 185 | .args(args) |
| 190 | .stdout(Stdio::from(stdout_redirect)) | 186 | .stdout(Stdio::from(stdout_redirect)) |
| @@ -192,16 +188,16 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> { | |||
| 192 | .stdin(Stdio::null()) | 188 | .stdin(Stdio::null()) |
| 193 | .spawn() | 189 | .spawn() |
| 194 | .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; | 190 | .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; |
| 195 | 191 | ||
| 196 | // Write PID to file | 192 | // Write PID to file |
| 197 | let mut pid_file_handle = File::create(&pid_file)?; | 193 | let mut pid_file_handle = File::create(&pid_file)?; |
| 198 | writeln!(pid_file_handle, "{}", child.id())?; | 194 | writeln!(pid_file_handle, "{}", child.id())?; |
| 199 | 195 | ||
| 200 | // Don't wait for the child - let it run detached | 196 | // Don't wait for the child - let it run detached |
| 201 | std::mem::forget(child); | 197 | std::mem::forget(child); |
| 202 | 198 | ||
| 203 | println!("Started daemon '{}' with PID written to {}", id, pid_file); | 199 | println!("Started daemon '{}' with PID written to {}", id, pid_file); |
| 204 | 200 | ||
| 205 | Ok(()) | 201 | Ok(()) |
| 206 | } | 202 | } |
| 207 | 203 | ||
| @@ -211,26 +207,26 @@ fn is_process_running(pid_file: &str) -> Result<bool> { | |||
| 211 | Ok(f) => f, | 207 | Ok(f) => f, |
| 212 | Err(_) => return Ok(false), // No PID file means no running process | 208 | Err(_) => return Ok(false), // No PID file means no running process |
| 213 | }; | 209 | }; |
| 214 | 210 | ||
| 215 | let mut contents = String::new(); | 211 | let mut contents = String::new(); |
| 216 | file.read_to_string(&mut contents)?; | 212 | file.read_to_string(&mut contents)?; |
| 217 | 213 | ||
| 218 | let pid: u32 = match contents.trim().parse() { | 214 | let pid: u32 = match contents.trim().parse() { |
| 219 | Ok(p) => p, | 215 | Ok(p) => p, |
| 220 | Err(_) => return Ok(false), // Invalid PID file | 216 | Err(_) => return Ok(false), // Invalid PID file |
| 221 | }; | 217 | }; |
| 222 | 218 | ||
| 223 | // Check if process is still running using kill -0 | 219 | // Check if process is still running using kill -0 |
| 224 | let output = Command::new("kill") | 220 | let output = Command::new("kill") |
| 225 | .args(&["-0", &pid.to_string()]) | 221 | .args(&["-0", &pid.to_string()]) |
| 226 | .output()?; | 222 | .output()?; |
| 227 | 223 | ||
| 228 | Ok(output.status.success()) | 224 | Ok(output.status.success()) |
| 229 | } | 225 | } |
| 230 | 226 | ||
| 231 | fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | 227 | fn stop_daemon(id: &str, timeout: u64) -> Result<()> { |
| 232 | let pid_file = format!("{}.pid", id); | 228 | let pid_file = format!("{}.pid", id); |
| 233 | 229 | ||
| 234 | // Check if PID file exists | 230 | // Check if PID file exists |
| 235 | let mut file = match File::open(&pid_file) { | 231 | let mut file = match File::open(&pid_file) { |
| 236 | Ok(f) => f, | 232 | Ok(f) => f, |
| @@ -239,11 +235,11 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | |||
| 239 | return Ok(()); | 235 | return Ok(()); |
| 240 | } | 236 | } |
| 241 | }; | 237 | }; |
| 242 | 238 | ||
| 243 | // Read PID | 239 | // Read PID |
| 244 | let mut contents = String::new(); | 240 | let mut contents = String::new(); |
| 245 | file.read_to_string(&mut contents)?; | 241 | file.read_to_string(&mut contents)?; |
| 246 | 242 | ||
| 247 | let pid: u32 = match contents.trim().parse() { | 243 | let pid: u32 = match contents.trim().parse() { |
| 248 | Ok(p) => p, | 244 | Ok(p) => p, |
| 249 | Err(_) => { | 245 | Err(_) => { |
| @@ -252,26 +248,34 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | |||
| 252 | return Ok(()); | 248 | return Ok(()); |
| 253 | } | 249 | } |
| 254 | }; | 250 | }; |
| 255 | 251 | ||
| 256 | tracing::info!("Stopping daemon '{}' (PID: {}) with timeout {}s", id, pid, timeout); | 252 | tracing::info!( |
| 257 | 253 | "Stopping daemon '{}' (PID: {}) with timeout {}s", | |
| 254 | id, | ||
| 255 | pid, | ||
| 256 | timeout | ||
| 257 | ); | ||
| 258 | |||
| 258 | // Check if process is running | 259 | // Check if process is running |
| 259 | if !is_process_running_by_pid(pid) { | 260 | if !is_process_running_by_pid(pid) { |
| 260 | println!("Process '{}' (PID: {}) is not running, cleaning up PID file", id, pid); | 261 | println!( |
| 262 | "Process '{}' (PID: {}) is not running, cleaning up PID file", | ||
| 263 | id, pid | ||
| 264 | ); | ||
| 261 | std::fs::remove_file(&pid_file)?; | 265 | std::fs::remove_file(&pid_file)?; |
| 262 | return Ok(()); | 266 | return Ok(()); |
| 263 | } | 267 | } |
| 264 | 268 | ||
| 265 | // Send SIGTERM | 269 | // Send SIGTERM |
| 266 | tracing::info!("Sending SIGTERM to PID {}", pid); | 270 | tracing::info!("Sending SIGTERM to PID {}", pid); |
| 267 | let output = Command::new("kill") | 271 | let output = Command::new("kill") |
| 268 | .args(&["-TERM", &pid.to_string()]) | 272 | .args(&["-TERM", &pid.to_string()]) |
| 269 | .output()?; | 273 | .output()?; |
| 270 | 274 | ||
| 271 | if !output.status.success() { | 275 | if !output.status.success() { |
| 272 | return Err(anyhow::anyhow!("Failed to send SIGTERM to PID {}", pid)); | 276 | return Err(anyhow::anyhow!("Failed to send SIGTERM to PID {}", pid)); |
| 273 | } | 277 | } |
| 274 | 278 | ||
| 275 | // Wait for the process to terminate | 279 | // Wait for the process to terminate |
| 276 | for i in 0..timeout { | 280 | for i in 0..timeout { |
| 277 | if !is_process_running_by_pid(pid) { | 281 | if !is_process_running_by_pid(pid) { |
| @@ -279,34 +283,41 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | |||
| 279 | std::fs::remove_file(&pid_file)?; | 283 | std::fs::remove_file(&pid_file)?; |
| 280 | return Ok(()); | 284 | return Ok(()); |
| 281 | } | 285 | } |
| 282 | 286 | ||
| 283 | if i == 0 { | 287 | if i == 0 { |
| 284 | tracing::info!("Waiting for process to terminate gracefully..."); | 288 | tracing::info!("Waiting for process to terminate gracefully..."); |
| 285 | } | 289 | } |
| 286 | 290 | ||
| 287 | thread::sleep(Duration::from_secs(1)); | 291 | thread::sleep(Duration::from_secs(1)); |
| 288 | } | 292 | } |
| 289 | 293 | ||
| 290 | // Process didn't terminate, send SIGKILL | 294 | // Process didn't terminate, send SIGKILL |
| 291 | tracing::warn!("Process {} didn't terminate after {}s, sending SIGKILL", pid, timeout); | 295 | tracing::warn!( |
| 296 | "Process {} didn't terminate after {}s, sending SIGKILL", | ||
| 297 | pid, | ||
| 298 | timeout | ||
| 299 | ); | ||
| 292 | let output = Command::new("kill") | 300 | let output = Command::new("kill") |
| 293 | .args(&["-KILL", &pid.to_string()]) | 301 | .args(&["-KILL", &pid.to_string()]) |
| 294 | .output()?; | 302 | .output()?; |
| 295 | 303 | ||
| 296 | if !output.status.success() { | 304 | if !output.status.success() { |
| 297 | return Err(anyhow::anyhow!("Failed to send SIGKILL to PID {}", pid)); | 305 | return Err(anyhow::anyhow!("Failed to send SIGKILL to PID {}", pid)); |
| 298 | } | 306 | } |
| 299 | 307 | ||
| 300 | // Wait a bit more for SIGKILL to take effect | 308 | // Wait a bit more for SIGKILL to take effect |
| 301 | thread::sleep(Duration::from_secs(1)); | 309 | thread::sleep(Duration::from_secs(1)); |
| 302 | 310 | ||
| 303 | if is_process_running_by_pid(pid) { | 311 | if is_process_running_by_pid(pid) { |
| 304 | return Err(anyhow::anyhow!("Process {} is still running after SIGKILL", pid)); | 312 | return Err(anyhow::anyhow!( |
| 313 | "Process {} is still running after SIGKILL", | ||
| 314 | pid | ||
| 315 | )); | ||
| 305 | } | 316 | } |
| 306 | 317 | ||
| 307 | println!("Process '{}' (PID: {}) terminated forcefully", id, pid); | 318 | println!("Process '{}' (PID: {}) terminated forcefully", id, pid); |
| 308 | std::fs::remove_file(&pid_file)?; | 319 | std::fs::remove_file(&pid_file)?; |
| 309 | 320 | ||
| 310 | Ok(()) | 321 | Ok(()) |
| 311 | } | 322 | } |
| 312 | 323 | ||
| @@ -314,7 +325,7 @@ fn is_process_running_by_pid(pid: u32) -> bool { | |||
| 314 | let output = Command::new("kill") | 325 | let output = Command::new("kill") |
| 315 | .args(&["-0", &pid.to_string()]) | 326 | .args(&["-0", &pid.to_string()]) |
| 316 | .output(); | 327 | .output(); |
| 317 | 328 | ||
| 318 | match output { | 329 | match output { |
| 319 | Ok(output) => output.status.success(), | 330 | Ok(output) => output.status.success(), |
| 320 | Err(_) => false, | 331 | Err(_) => false, |
| @@ -324,9 +335,9 @@ fn is_process_running_by_pid(pid: u32) -> bool { | |||
| 324 | fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | 335 | fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { |
| 325 | let stdout_file = format!("{}.stdout", id); | 336 | let stdout_file = format!("{}.stdout", id); |
| 326 | let stderr_file = format!("{}.stderr", id); | 337 | let stderr_file = format!("{}.stderr", id); |
| 327 | 338 | ||
| 328 | let mut files_found = false; | 339 | let mut files_found = false; |
| 329 | 340 | ||
| 330 | if show_stdout { | 341 | if show_stdout { |
| 331 | if let Ok(contents) = std::fs::read_to_string(&stdout_file) { | 342 | if let Ok(contents) = std::fs::read_to_string(&stdout_file) { |
| 332 | if !contents.is_empty() { | 343 | if !contents.is_empty() { |
| @@ -340,7 +351,7 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | |||
| 340 | tracing::warn!("Could not read {}", stdout_file); | 351 | tracing::warn!("Could not read {}", stdout_file); |
| 341 | } | 352 | } |
| 342 | } | 353 | } |
| 343 | 354 | ||
| 344 | if show_stderr { | 355 | if show_stderr { |
| 345 | if let Ok(contents) = std::fs::read_to_string(&stderr_file) { | 356 | if let Ok(contents) = std::fs::read_to_string(&stderr_file) { |
| 346 | if !contents.is_empty() { | 357 | if !contents.is_empty() { |
| @@ -354,21 +365,22 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | |||
| 354 | tracing::warn!("Could not read {}", stderr_file); | 365 | tracing::warn!("Could not read {}", stderr_file); |
| 355 | } | 366 | } |
| 356 | } | 367 | } |
| 357 | 368 | ||
| 358 | if !files_found { | 369 | if !files_found { |
| 359 | println!("No log files found for daemon '{}'", id); | 370 | println!("No log files found for daemon '{}'", id); |
| 360 | } | 371 | } |
| 361 | 372 | ||
| 362 | Ok(()) | 373 | Ok(()) |
| 363 | } | 374 | } |
| 364 | 375 | ||
| 365 | fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | 376 | fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { |
| 366 | let stdout_file = format!("{}.stdout", id); | 377 | let stdout_file = format!("{}.stdout", id); |
| 367 | let stderr_file = format!("{}.stderr", id); | 378 | let stderr_file = format!("{}.stderr", id); |
| 368 | 379 | ||
| 369 | // First, display existing content and set up initial positions | 380 | // First, display existing content and set up initial positions |
| 370 | let mut file_positions: std::collections::HashMap<String, u64> = std::collections::HashMap::new(); | 381 | let mut file_positions: std::collections::HashMap<String, u64> = |
| 371 | 382 | std::collections::HashMap::new(); | |
| 383 | |||
| 372 | if show_stdout && Path::new(&stdout_file).exists() { | 384 | if show_stdout && Path::new(&stdout_file).exists() { |
| 373 | let mut file = File::open(&stdout_file)?; | 385 | let mut file = File::open(&stdout_file)?; |
| 374 | let initial_content = read_file_content(&mut file)?; | 386 | let initial_content = read_file_content(&mut file)?; |
| @@ -381,7 +393,7 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | |||
| 381 | let position = file.seek(SeekFrom::Current(0))?; | 393 | let position = file.seek(SeekFrom::Current(0))?; |
| 382 | file_positions.insert(stdout_file.clone(), position); | 394 | file_positions.insert(stdout_file.clone(), position); |
| 383 | } | 395 | } |
| 384 | 396 | ||
| 385 | if show_stderr && Path::new(&stderr_file).exists() { | 397 | if show_stderr && Path::new(&stderr_file).exists() { |
| 386 | let mut file = File::open(&stderr_file)?; | 398 | let mut file = File::open(&stderr_file)?; |
| 387 | let initial_content = read_file_content(&mut file)?; | 399 | let initial_content = read_file_content(&mut file)?; |
| @@ -396,28 +408,31 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | |||
| 396 | let position = file.seek(SeekFrom::Current(0))?; | 408 | let position = file.seek(SeekFrom::Current(0))?; |
| 397 | file_positions.insert(stderr_file.clone(), position); | 409 | file_positions.insert(stderr_file.clone(), position); |
| 398 | } | 410 | } |
| 399 | 411 | ||
| 400 | if file_positions.is_empty() { | 412 | if file_positions.is_empty() { |
| 401 | println!("No log files found for daemon '{}'. Watching for new files...", id); | 413 | println!( |
| 414 | "No log files found for daemon '{}'. Watching for new files...", | ||
| 415 | id | ||
| 416 | ); | ||
| 402 | } | 417 | } |
| 403 | 418 | ||
| 404 | tracing::info!("Watching for changes to log files... Press Ctrl+C to stop."); | 419 | tracing::info!("Watching for changes to log files... Press Ctrl+C to stop."); |
| 405 | 420 | ||
| 406 | // Set up file watcher | 421 | // Set up file watcher |
| 407 | let (tx, rx) = channel(); | 422 | let (tx, rx) = channel(); |
| 408 | let mut watcher = RecommendedWatcher::new(tx, Config::default())?; | 423 | let mut watcher = RecommendedWatcher::new(tx, Config::default())?; |
| 409 | 424 | ||
| 410 | // Watch the current directory for new files and changes | 425 | // Watch the current directory for new files and changes |
| 411 | watcher.watch(Path::new("."), RecursiveMode::NonRecursive)?; | 426 | watcher.watch(Path::new("."), RecursiveMode::NonRecursive)?; |
| 412 | 427 | ||
| 413 | // Handle Ctrl+C gracefully | 428 | // Handle Ctrl+C gracefully |
| 414 | let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); | 429 | let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); |
| 415 | let r = running.clone(); | 430 | let r = running.clone(); |
| 416 | 431 | ||
| 417 | ctrlc::set_handler(move || { | 432 | ctrlc::set_handler(move || { |
| 418 | r.store(false, std::sync::atomic::Ordering::SeqCst); | 433 | r.store(false, std::sync::atomic::Ordering::SeqCst); |
| 419 | })?; | 434 | })?; |
| 420 | 435 | ||
| 421 | while running.load(std::sync::atomic::Ordering::SeqCst) { | 436 | while running.load(std::sync::atomic::Ordering::SeqCst) { |
| 422 | match rx.recv_timeout(Duration::from_millis(100)) { | 437 | match rx.recv_timeout(Duration::from_millis(100)) { |
| 423 | Ok(res) => { | 438 | Ok(res) => { |
| @@ -429,11 +444,15 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | |||
| 429 | }) => { | 444 | }) => { |
| 430 | for path in paths { | 445 | for path in paths { |
| 431 | let path_str = path.to_string_lossy().to_string(); | 446 | let path_str = path.to_string_lossy().to_string(); |
| 432 | 447 | ||
| 433 | if (show_stdout && path_str == stdout_file) || | 448 | if (show_stdout && path_str == stdout_file) |
| 434 | (show_stderr && path_str == stderr_file) { | 449 | || (show_stderr && path_str == stderr_file) |
| 435 | 450 | { | |
| 436 | if let Err(e) = handle_file_change(&path_str, &mut file_positions, show_stdout && show_stderr) { | 451 | if let Err(e) = handle_file_change( |
| 452 | &path_str, | ||
| 453 | &mut file_positions, | ||
| 454 | show_stdout && show_stderr, | ||
| 455 | ) { | ||
| 437 | tracing::error!("Error handling file change: {}", e); | 456 | tracing::error!("Error handling file change: {}", e); |
| 438 | } | 457 | } |
| 439 | } | 458 | } |
| @@ -447,14 +466,18 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | |||
| 447 | // Handle file creation | 466 | // Handle file creation |
| 448 | for path in paths { | 467 | for path in paths { |
| 449 | let path_str = path.to_string_lossy().to_string(); | 468 | let path_str = path.to_string_lossy().to_string(); |
| 450 | 469 | ||
| 451 | if (show_stdout && path_str == stdout_file) || | 470 | if (show_stdout && path_str == stdout_file) |
| 452 | (show_stderr && path_str == stderr_file) { | 471 | || (show_stderr && path_str == stderr_file) |
| 453 | 472 | { | |
| 454 | tracing::info!("New file detected: {}", path_str); | 473 | tracing::info!("New file detected: {}", path_str); |
| 455 | file_positions.insert(path_str.clone(), 0); | 474 | file_positions.insert(path_str.clone(), 0); |
| 456 | 475 | ||
| 457 | if let Err(e) = handle_file_change(&path_str, &mut file_positions, show_stdout && show_stderr) { | 476 | if let Err(e) = handle_file_change( |
| 477 | &path_str, | ||
| 478 | &mut file_positions, | ||
| 479 | show_stdout && show_stderr, | ||
| 480 | ) { | ||
| 458 | tracing::error!("Error handling new file: {}", e); | 481 | tracing::error!("Error handling new file: {}", e); |
| 459 | } | 482 | } |
| 460 | } | 483 | } |
| @@ -473,7 +496,7 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | |||
| 473 | } | 496 | } |
| 474 | } | 497 | } |
| 475 | } | 498 | } |
| 476 | 499 | ||
| 477 | println!("\nTailing stopped."); | 500 | println!("\nTailing stopped."); |
| 478 | Ok(()) | 501 | Ok(()) |
| 479 | } | 502 | } |
| @@ -485,32 +508,32 @@ fn read_file_content(file: &mut File) -> Result<String> { | |||
| 485 | } | 508 | } |
| 486 | 509 | ||
| 487 | fn handle_file_change( | 510 | fn handle_file_change( |
| 488 | file_path: &str, | 511 | file_path: &str, |
| 489 | positions: &mut std::collections::HashMap<String, u64>, | 512 | positions: &mut std::collections::HashMap<String, u64>, |
| 490 | show_headers: bool | 513 | show_headers: bool, |
| 491 | ) -> Result<()> { | 514 | ) -> Result<()> { |
| 492 | let mut file = File::open(file_path)?; | 515 | let mut file = File::open(file_path)?; |
| 493 | let current_pos = positions.get(file_path).copied().unwrap_or(0); | 516 | let current_pos = positions.get(file_path).copied().unwrap_or(0); |
| 494 | 517 | ||
| 495 | // Seek to the last read position | 518 | // Seek to the last read position |
| 496 | file.seek(SeekFrom::Start(current_pos))?; | 519 | file.seek(SeekFrom::Start(current_pos))?; |
| 497 | 520 | ||
| 498 | // Read new content | 521 | // Read new content |
| 499 | let mut new_content = String::new(); | 522 | let mut new_content = String::new(); |
| 500 | file.read_to_string(&mut new_content)?; | 523 | file.read_to_string(&mut new_content)?; |
| 501 | 524 | ||
| 502 | if !new_content.is_empty() { | 525 | if !new_content.is_empty() { |
| 503 | if show_headers { | 526 | if show_headers { |
| 504 | println!("==> {} <==", file_path); | 527 | println!("==> {} <==", file_path); |
| 505 | } | 528 | } |
| 506 | print!("{}", new_content); | 529 | print!("{}", new_content); |
| 507 | std::io::Write::flush(&mut std::io::stdout())?; | 530 | std::io::Write::flush(&mut std::io::stdout())?; |
| 508 | 531 | ||
| 509 | // Update position | 532 | // Update position |
| 510 | let new_pos = file.seek(SeekFrom::Current(0))?; | 533 | let new_pos = file.seek(SeekFrom::Current(0))?; |
| 511 | positions.insert(file_path.to_string(), new_pos); | 534 | positions.insert(file_path.to_string(), new_pos); |
| 512 | } | 535 | } |
| 513 | 536 | ||
| 514 | Ok(()) | 537 | Ok(()) |
| 515 | } | 538 | } |
| 516 | 539 | ||
| @@ -519,18 +542,18 @@ fn list_daemons(quiet: bool) -> Result<()> { | |||
| 519 | println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); | 542 | println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); |
| 520 | println!("{}", "-".repeat(50)); | 543 | println!("{}", "-".repeat(50)); |
| 521 | } | 544 | } |
| 522 | 545 | ||
| 523 | let mut found_any = false; | 546 | let mut found_any = false; |
| 524 | 547 | ||
| 525 | // Find all .pid files in current directory | 548 | // Find all .pid files in current directory |
| 526 | for entry in find_pid_files()? { | 549 | for entry in find_pid_files()? { |
| 527 | found_any = true; | 550 | found_any = true; |
| 528 | let path = entry.path(); | 551 | let path = entry.path(); |
| 529 | let path_str = path.to_string_lossy(); | 552 | let path_str = path.to_string_lossy(); |
| 530 | 553 | ||
| 531 | // Extract ID from filename (remove .pid extension) | 554 | // Extract ID from filename (remove .pid extension) |
| 532 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | 555 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); |
| 533 | 556 | ||
| 534 | // Read PID from file | 557 | // Read PID from file |
| 535 | match std::fs::read_to_string(&path) { | 558 | match std::fs::read_to_string(&path) { |
| 536 | Ok(contents) => { | 559 | Ok(contents) => { |
| @@ -542,7 +565,7 @@ fn list_daemons(quiet: bool) -> Result<()> { | |||
| 542 | } else { | 565 | } else { |
| 543 | "DEAD" | 566 | "DEAD" |
| 544 | }; | 567 | }; |
| 545 | 568 | ||
| 546 | if quiet { | 569 | if quiet { |
| 547 | println!("{}:{}:{}", id, pid, status); | 570 | println!("{}:{}:{}", id, pid, status); |
| 548 | } else { | 571 | } else { |
| @@ -556,7 +579,10 @@ fn list_daemons(quiet: bool) -> Result<()> { | |||
| 556 | if quiet { | 579 | if quiet { |
| 557 | println!("{}:INVALID:ERROR", id); | 580 | println!("{}:INVALID:ERROR", id); |
| 558 | } else { | 581 | } else { |
| 559 | println!("{:<20} {:<8} {:<10} {}", id, "INVALID", "ERROR", "Invalid PID file"); | 582 | println!( |
| 583 | "{:<20} {:<8} {:<10} {}", | ||
| 584 | id, "INVALID", "ERROR", "Invalid PID file" | ||
| 585 | ); | ||
| 560 | } | 586 | } |
| 561 | } | 587 | } |
| 562 | } | 588 | } |
| @@ -565,16 +591,22 @@ fn list_daemons(quiet: bool) -> Result<()> { | |||
| 565 | if quiet { | 591 | if quiet { |
| 566 | println!("{}:ERROR:ERROR", id); | 592 | println!("{}:ERROR:ERROR", id); |
| 567 | } else { | 593 | } else { |
| 568 | println!("{:<20} {:<8} {:<10} {}", id, "ERROR", "ERROR", format!("Cannot read: {}", e)); | 594 | println!( |
| 595 | "{:<20} {:<8} {:<10} {}", | ||
| 596 | id, | ||
| 597 | "ERROR", | ||
| 598 | "ERROR", | ||
| 599 | format!("Cannot read: {}", e) | ||
| 600 | ); | ||
| 569 | } | 601 | } |
| 570 | } | 602 | } |
| 571 | } | 603 | } |
| 572 | } | 604 | } |
| 573 | 605 | ||
| 574 | if !found_any && !quiet { | 606 | if !found_any && !quiet { |
| 575 | println!("No daemon processes found."); | 607 | println!("No daemon processes found."); |
| 576 | } | 608 | } |
| 577 | 609 | ||
| 578 | Ok(()) | 610 | Ok(()) |
| 579 | } | 611 | } |
| 580 | 612 | ||
| @@ -582,16 +614,16 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 582 | let pid_file = format!("{}.pid", id); | 614 | let pid_file = format!("{}.pid", id); |
| 583 | let stdout_file = format!("{}.stdout", id); | 615 | let stdout_file = format!("{}.stdout", id); |
| 584 | let stderr_file = format!("{}.stderr", id); | 616 | let stderr_file = format!("{}.stderr", id); |
| 585 | 617 | ||
| 586 | println!("Daemon: {}", id); | 618 | println!("Daemon: {}", id); |
| 587 | println!("PID file: {}", pid_file); | 619 | println!("PID file: {}", pid_file); |
| 588 | 620 | ||
| 589 | // Check if PID file exists | 621 | // Check if PID file exists |
| 590 | if !Path::new(&pid_file).exists() { | 622 | if !Path::new(&pid_file).exists() { |
| 591 | println!("Status: NOT FOUND (no PID file)"); | 623 | println!("Status: NOT FOUND (no PID file)"); |
| 592 | return Ok(()); | 624 | return Ok(()); |
| 593 | } | 625 | } |
| 594 | 626 | ||
| 595 | // Read PID from file | 627 | // Read PID from file |
| 596 | match std::fs::read_to_string(&pid_file) { | 628 | match std::fs::read_to_string(&pid_file) { |
| 597 | Ok(contents) => { | 629 | Ok(contents) => { |
| @@ -599,10 +631,10 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 599 | match pid_str.parse::<u32>() { | 631 | match pid_str.parse::<u32>() { |
| 600 | Ok(pid) => { | 632 | Ok(pid) => { |
| 601 | println!("PID: {}", pid); | 633 | println!("PID: {}", pid); |
| 602 | 634 | ||
| 603 | if is_process_running_by_pid(pid) { | 635 | if is_process_running_by_pid(pid) { |
| 604 | println!("Status: RUNNING"); | 636 | println!("Status: RUNNING"); |
| 605 | 637 | ||
| 606 | // Show file information | 638 | // Show file information |
| 607 | if Path::new(&stdout_file).exists() { | 639 | if Path::new(&stdout_file).exists() { |
| 608 | let metadata = std::fs::metadata(&stdout_file)?; | 640 | let metadata = std::fs::metadata(&stdout_file)?; |
| @@ -610,7 +642,7 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 610 | } else { | 642 | } else { |
| 611 | println!("Stdout file: {} (not found)", stdout_file); | 643 | println!("Stdout file: {} (not found)", stdout_file); |
| 612 | } | 644 | } |
| 613 | 645 | ||
| 614 | if Path::new(&stderr_file).exists() { | 646 | if Path::new(&stderr_file).exists() { |
| 615 | let metadata = std::fs::metadata(&stderr_file)?; | 647 | let metadata = std::fs::metadata(&stderr_file)?; |
| 616 | println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); | 648 | println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); |
| @@ -631,21 +663,21 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 631 | println!("Status: ERROR (cannot read PID file: {})", e); | 663 | println!("Status: ERROR (cannot read PID file: {})", e); |
| 632 | } | 664 | } |
| 633 | } | 665 | } |
| 634 | 666 | ||
| 635 | Ok(()) | 667 | Ok(()) |
| 636 | } | 668 | } |
| 637 | 669 | ||
| 638 | fn clean_orphaned_files() -> Result<()> { | 670 | fn clean_orphaned_files() -> Result<()> { |
| 639 | tracing::info!("Scanning for orphaned daemon files..."); | 671 | tracing::info!("Scanning for orphaned daemon files..."); |
| 640 | 672 | ||
| 641 | let mut cleaned_count = 0; | 673 | let mut cleaned_count = 0; |
| 642 | 674 | ||
| 643 | // Find all .pid files in current directory | 675 | // Find all .pid files in current directory |
| 644 | for entry in find_pid_files()? { | 676 | for entry in find_pid_files()? { |
| 645 | let path = entry.path(); | 677 | let path = entry.path(); |
| 646 | let path_str = path.to_string_lossy(); | 678 | let path_str = path.to_string_lossy(); |
| 647 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | 679 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); |
| 648 | 680 | ||
| 649 | // Read PID from file | 681 | // Read PID from file |
| 650 | match std::fs::read_to_string(&path) { | 682 | match std::fs::read_to_string(&path) { |
| 651 | Ok(contents) => { | 683 | Ok(contents) => { |
| @@ -655,14 +687,14 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 655 | // Check if process is still running | 687 | // Check if process is still running |
| 656 | if !is_process_running_by_pid(pid) { | 688 | if !is_process_running_by_pid(pid) { |
| 657 | println!("Cleaning up orphaned files for '{}' (PID: {})", id, pid); | 689 | println!("Cleaning up orphaned files for '{}' (PID: {})", id, pid); |
| 658 | 690 | ||
| 659 | // Remove PID file | 691 | // Remove PID file |
| 660 | if let Err(e) = std::fs::remove_file(&path) { | 692 | if let Err(e) = std::fs::remove_file(&path) { |
| 661 | tracing::warn!("Failed to remove {}: {}", path_str, e); | 693 | tracing::warn!("Failed to remove {}: {}", path_str, e); |
| 662 | } else { | 694 | } else { |
| 663 | tracing::info!("Removed {}", path_str); | 695 | tracing::info!("Removed {}", path_str); |
| 664 | } | 696 | } |
| 665 | 697 | ||
| 666 | // Remove stdout file if it exists | 698 | // Remove stdout file if it exists |
| 667 | let stdout_file = format!("{}.stdout", id); | 699 | let stdout_file = format!("{}.stdout", id); |
| 668 | if Path::new(&stdout_file).exists() { | 700 | if Path::new(&stdout_file).exists() { |
| @@ -672,7 +704,7 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 672 | tracing::info!("Removed {}", stdout_file); | 704 | tracing::info!("Removed {}", stdout_file); |
| 673 | } | 705 | } |
| 674 | } | 706 | } |
| 675 | 707 | ||
| 676 | // Remove stderr file if it exists | 708 | // Remove stderr file if it exists |
| 677 | let stderr_file = format!("{}.stderr", id); | 709 | let stderr_file = format!("{}.stderr", id); |
| 678 | if Path::new(&stderr_file).exists() { | 710 | if Path::new(&stderr_file).exists() { |
| @@ -682,10 +714,14 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 682 | tracing::info!("Removed {}", stderr_file); | 714 | tracing::info!("Removed {}", stderr_file); |
| 683 | } | 715 | } |
| 684 | } | 716 | } |
| 685 | 717 | ||
| 686 | cleaned_count += 1; | 718 | cleaned_count += 1; |
| 687 | } else { | 719 | } else { |
| 688 | tracing::info!("Skipping '{}' (PID: {}) - process is still running", id, pid); | 720 | tracing::info!( |
| 721 | "Skipping '{}' (PID: {}) - process is still running", | ||
| 722 | id, | ||
| 723 | pid | ||
| 724 | ); | ||
| 689 | } | 725 | } |
| 690 | } | 726 | } |
| 691 | Err(_) => { | 727 | Err(_) => { |
| @@ -710,18 +746,19 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 710 | } | 746 | } |
| 711 | } | 747 | } |
| 712 | } | 748 | } |
| 713 | 749 | ||
| 714 | if cleaned_count == 0 { | 750 | if cleaned_count == 0 { |
| 715 | println!("No orphaned files found."); | 751 | println!("No orphaned files found."); |
| 716 | } else { | 752 | } else { |
| 717 | println!("Cleaned up {} orphaned daemon(s).", cleaned_count); | 753 | println!("Cleaned up {} orphaned daemon(s).", cleaned_count); |
| 718 | } | 754 | } |
| 719 | 755 | ||
| 720 | Ok(()) | 756 | Ok(()) |
| 721 | } | 757 | } |
| 722 | 758 | ||
| 723 | fn print_llm_guide() { | 759 | fn print_llm_guide() { |
| 724 | println!(r#"# Demon - Daemon Process Management CLI | 760 | println!( |
| 761 | r#"# Demon - Daemon Process Management CLI | ||
| 725 | 762 | ||
| 726 | ## Overview | 763 | ## Overview |
| 727 | Demon is a command-line tool for spawning, managing, and monitoring background processes (daemons) on Linux systems. It redirects process stdout/stderr to files and provides commands to control and observe these processes. | 764 | Demon is a command-line tool for spawning, managing, and monitoring background processes (daemons) on Linux systems. It redirects process stdout/stderr to files and provides commands to control and observe these processes. |
| @@ -948,7 +985,8 @@ demon list --quiet > process_status.txt | |||
| 948 | - Use standard Unix signals for process control | 985 | - Use standard Unix signals for process control |
| 949 | - Log rotation should be handled by the application itself | 986 | - Log rotation should be handled by the application itself |
| 950 | 987 | ||
| 951 | This tool is designed for Linux environments and provides a simple interface for managing background processes with persistent logging."#); | 988 | This tool is designed for Linux environments and provides a simple interface for managing background processes with persistent logging."# |
| 989 | ); | ||
| 952 | } | 990 | } |
| 953 | 991 | ||
| 954 | fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { | 992 | fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { |
diff --git a/tests/cli.rs b/tests/cli.rs index 37398e6..e99e876 100644 --- a/tests/cli.rs +++ b/tests/cli.rs | |||
| @@ -32,7 +32,7 @@ fn test_version_output() { | |||
| 32 | #[test] | 32 | #[test] |
| 33 | fn test_run_missing_command() { | 33 | fn test_run_missing_command() { |
| 34 | let temp_dir = TempDir::new().unwrap(); | 34 | let temp_dir = TempDir::new().unwrap(); |
| 35 | 35 | ||
| 36 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 36 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 37 | cmd.current_dir(temp_dir.path()) | 37 | cmd.current_dir(temp_dir.path()) |
| 38 | .args(&["run", "--id", "test"]) | 38 | .args(&["run", "--id", "test"]) |
| @@ -44,19 +44,19 @@ fn test_run_missing_command() { | |||
| 44 | #[test] | 44 | #[test] |
| 45 | fn test_run_creates_files() { | 45 | fn test_run_creates_files() { |
| 46 | let temp_dir = TempDir::new().unwrap(); | 46 | let temp_dir = TempDir::new().unwrap(); |
| 47 | 47 | ||
| 48 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 48 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 49 | cmd.current_dir(temp_dir.path()) | 49 | cmd.current_dir(temp_dir.path()) |
| 50 | .args(&["run", "--id", "test", "echo", "hello"]) | 50 | .args(&["run", "--id", "test", "echo", "hello"]) |
| 51 | .assert() | 51 | .assert() |
| 52 | .success() | 52 | .success() |
| 53 | .stdout(predicate::str::contains("Started daemon 'test'")); | 53 | .stdout(predicate::str::contains("Started daemon 'test'")); |
| 54 | 54 | ||
| 55 | // Verify files were created | 55 | // Verify files were created |
| 56 | assert!(temp_dir.path().join("test.pid").exists()); | 56 | assert!(temp_dir.path().join("test.pid").exists()); |
| 57 | assert!(temp_dir.path().join("test.stdout").exists()); | 57 | assert!(temp_dir.path().join("test.stdout").exists()); |
| 58 | assert!(temp_dir.path().join("test.stderr").exists()); | 58 | assert!(temp_dir.path().join("test.stderr").exists()); |
| 59 | 59 | ||
| 60 | // Check that stdout contains our output | 60 | // Check that stdout contains our output |
| 61 | let stdout_content = fs::read_to_string(temp_dir.path().join("test.stdout")).unwrap(); | 61 | let stdout_content = fs::read_to_string(temp_dir.path().join("test.stdout")).unwrap(); |
| 62 | assert_eq!(stdout_content.trim(), "hello"); | 62 | assert_eq!(stdout_content.trim(), "hello"); |
| @@ -65,14 +65,14 @@ fn test_run_creates_files() { | |||
| 65 | #[test] | 65 | #[test] |
| 66 | fn test_run_duplicate_process() { | 66 | fn test_run_duplicate_process() { |
| 67 | let temp_dir = TempDir::new().unwrap(); | 67 | let temp_dir = TempDir::new().unwrap(); |
| 68 | 68 | ||
| 69 | // Start a long-running process | 69 | // Start a long-running process |
| 70 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 70 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 71 | cmd.current_dir(temp_dir.path()) | 71 | cmd.current_dir(temp_dir.path()) |
| 72 | .args(&["run", "--id", "long", "sleep", "30"]) | 72 | .args(&["run", "--id", "long", "sleep", "30"]) |
| 73 | .assert() | 73 | .assert() |
| 74 | .success(); | 74 | .success(); |
| 75 | 75 | ||
| 76 | // Try to start another with the same ID | 76 | // Try to start another with the same ID |
| 77 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 77 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 78 | cmd.current_dir(temp_dir.path()) | 78 | cmd.current_dir(temp_dir.path()) |
| @@ -80,7 +80,7 @@ fn test_run_duplicate_process() { | |||
| 80 | .assert() | 80 | .assert() |
| 81 | .failure() | 81 | .failure() |
| 82 | .stderr(predicate::str::contains("already running")); | 82 | .stderr(predicate::str::contains("already running")); |
| 83 | 83 | ||
| 84 | // Clean up the running process | 84 | // Clean up the running process |
| 85 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 85 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 86 | cmd.current_dir(temp_dir.path()) | 86 | cmd.current_dir(temp_dir.path()) |
| @@ -92,7 +92,7 @@ fn test_run_duplicate_process() { | |||
| 92 | #[test] | 92 | #[test] |
| 93 | fn test_list_empty() { | 93 | fn test_list_empty() { |
| 94 | let temp_dir = TempDir::new().unwrap(); | 94 | let temp_dir = TempDir::new().unwrap(); |
| 95 | 95 | ||
| 96 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 96 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 97 | cmd.current_dir(temp_dir.path()) | 97 | cmd.current_dir(temp_dir.path()) |
| 98 | .args(&["list"]) | 98 | .args(&["list"]) |
| @@ -107,14 +107,14 @@ fn test_list_empty() { | |||
| 107 | #[test] | 107 | #[test] |
| 108 | fn test_list_with_processes() { | 108 | fn test_list_with_processes() { |
| 109 | let temp_dir = TempDir::new().unwrap(); | 109 | let temp_dir = TempDir::new().unwrap(); |
| 110 | 110 | ||
| 111 | // Start a process | 111 | // Start a process |
| 112 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 112 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 113 | cmd.current_dir(temp_dir.path()) | 113 | cmd.current_dir(temp_dir.path()) |
| 114 | .args(&["run", "--id", "test", "echo", "done"]) | 114 | .args(&["run", "--id", "test", "echo", "done"]) |
| 115 | .assert() | 115 | .assert() |
| 116 | .success(); | 116 | .success(); |
| 117 | 117 | ||
| 118 | // List processes | 118 | // List processes |
| 119 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 119 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 120 | cmd.current_dir(temp_dir.path()) | 120 | cmd.current_dir(temp_dir.path()) |
| @@ -128,17 +128,22 @@ fn test_list_with_processes() { | |||
| 128 | #[test] | 128 | #[test] |
| 129 | fn test_cat_output() { | 129 | fn test_cat_output() { |
| 130 | let temp_dir = TempDir::new().unwrap(); | 130 | let temp_dir = TempDir::new().unwrap(); |
| 131 | 131 | ||
| 132 | // Create a process with output | 132 | // Create a process with output |
| 133 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 133 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 134 | cmd.current_dir(temp_dir.path()) | 134 | cmd.current_dir(temp_dir.path()) |
| 135 | .args(&[ | 135 | .args(&[ |
| 136 | "run", "--id", "test", "--", | 136 | "run", |
| 137 | "sh", "-c", "echo 'stdout line'; echo 'stderr line' >&2" | 137 | "--id", |
| 138 | "test", | ||
| 139 | "--", | ||
| 140 | "sh", | ||
| 141 | "-c", | ||
| 142 | "echo 'stdout line'; echo 'stderr line' >&2", | ||
| 138 | ]) | 143 | ]) |
| 139 | .assert() | 144 | .assert() |
| 140 | .success(); | 145 | .success(); |
| 141 | 146 | ||
| 142 | // Cat the output | 147 | // Cat the output |
| 143 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 148 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 144 | cmd.current_dir(temp_dir.path()) | 149 | cmd.current_dir(temp_dir.path()) |
| @@ -152,17 +157,22 @@ fn test_cat_output() { | |||
| 152 | #[test] | 157 | #[test] |
| 153 | fn test_cat_stdout_only() { | 158 | fn test_cat_stdout_only() { |
| 154 | let temp_dir = TempDir::new().unwrap(); | 159 | let temp_dir = TempDir::new().unwrap(); |
| 155 | 160 | ||
| 156 | // Create a process with output | 161 | // Create a process with output |
| 157 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 162 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 158 | cmd.current_dir(temp_dir.path()) | 163 | cmd.current_dir(temp_dir.path()) |
| 159 | .args(&[ | 164 | .args(&[ |
| 160 | "run", "--id", "test", "--", | 165 | "run", |
| 161 | "sh", "-c", "echo 'stdout line'; echo 'stderr line' >&2" | 166 | "--id", |
| 167 | "test", | ||
| 168 | "--", | ||
| 169 | "sh", | ||
| 170 | "-c", | ||
| 171 | "echo 'stdout line'; echo 'stderr line' >&2", | ||
| 162 | ]) | 172 | ]) |
| 163 | .assert() | 173 | .assert() |
| 164 | .success(); | 174 | .success(); |
| 165 | 175 | ||
| 166 | // Cat only stdout | 176 | // Cat only stdout |
| 167 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 177 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 168 | cmd.current_dir(temp_dir.path()) | 178 | cmd.current_dir(temp_dir.path()) |
| @@ -176,7 +186,7 @@ fn test_cat_stdout_only() { | |||
| 176 | #[test] | 186 | #[test] |
| 177 | fn test_status_nonexistent() { | 187 | fn test_status_nonexistent() { |
| 178 | let temp_dir = TempDir::new().unwrap(); | 188 | let temp_dir = TempDir::new().unwrap(); |
| 179 | 189 | ||
| 180 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 190 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 181 | cmd.current_dir(temp_dir.path()) | 191 | cmd.current_dir(temp_dir.path()) |
| 182 | .args(&["status", "--id", "nonexistent"]) | 192 | .args(&["status", "--id", "nonexistent"]) |
| @@ -188,14 +198,14 @@ fn test_status_nonexistent() { | |||
| 188 | #[test] | 198 | #[test] |
| 189 | fn test_status_dead_process() { | 199 | fn test_status_dead_process() { |
| 190 | let temp_dir = TempDir::new().unwrap(); | 200 | let temp_dir = TempDir::new().unwrap(); |
| 191 | 201 | ||
| 192 | // Create a short-lived process | 202 | // Create a short-lived process |
| 193 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 203 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 194 | cmd.current_dir(temp_dir.path()) | 204 | cmd.current_dir(temp_dir.path()) |
| 195 | .args(&["run", "--id", "dead", "echo", "hello"]) | 205 | .args(&["run", "--id", "dead", "echo", "hello"]) |
| 196 | .assert() | 206 | .assert() |
| 197 | .success(); | 207 | .success(); |
| 198 | 208 | ||
| 199 | // Check its status (should be dead) | 209 | // Check its status (should be dead) |
| 200 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 210 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 201 | cmd.current_dir(temp_dir.path()) | 211 | cmd.current_dir(temp_dir.path()) |
| @@ -208,7 +218,7 @@ fn test_status_dead_process() { | |||
| 208 | #[test] | 218 | #[test] |
| 209 | fn test_stop_nonexistent() { | 219 | fn test_stop_nonexistent() { |
| 210 | let temp_dir = TempDir::new().unwrap(); | 220 | let temp_dir = TempDir::new().unwrap(); |
| 211 | 221 | ||
| 212 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 222 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 213 | cmd.current_dir(temp_dir.path()) | 223 | cmd.current_dir(temp_dir.path()) |
| 214 | .args(&["stop", "--id", "nonexistent"]) | 224 | .args(&["stop", "--id", "nonexistent"]) |
| @@ -220,14 +230,14 @@ fn test_stop_nonexistent() { | |||
| 220 | #[test] | 230 | #[test] |
| 221 | fn test_stop_process() { | 231 | fn test_stop_process() { |
| 222 | let temp_dir = TempDir::new().unwrap(); | 232 | let temp_dir = TempDir::new().unwrap(); |
| 223 | 233 | ||
| 224 | // Start a long-running process | 234 | // Start a long-running process |
| 225 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 235 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 226 | cmd.current_dir(temp_dir.path()) | 236 | cmd.current_dir(temp_dir.path()) |
| 227 | .args(&["run", "--id", "long", "sleep", "10"]) | 237 | .args(&["run", "--id", "long", "sleep", "10"]) |
| 228 | .assert() | 238 | .assert() |
| 229 | .success(); | 239 | .success(); |
| 230 | 240 | ||
| 231 | // Stop it | 241 | // Stop it |
| 232 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 242 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 233 | cmd.current_dir(temp_dir.path()) | 243 | cmd.current_dir(temp_dir.path()) |
| @@ -235,7 +245,7 @@ fn test_stop_process() { | |||
| 235 | .assert() | 245 | .assert() |
| 236 | .success() | 246 | .success() |
| 237 | .stdout(predicate::str::contains("terminated gracefully")); | 247 | .stdout(predicate::str::contains("terminated gracefully")); |
| 238 | 248 | ||
| 239 | // Verify PID file is gone | 249 | // Verify PID file is gone |
| 240 | assert!(!temp_dir.path().join("long.pid").exists()); | 250 | assert!(!temp_dir.path().join("long.pid").exists()); |
| 241 | } | 251 | } |
| @@ -243,7 +253,7 @@ fn test_stop_process() { | |||
| 243 | #[test] | 253 | #[test] |
| 244 | fn test_clean_no_orphans() { | 254 | fn test_clean_no_orphans() { |
| 245 | let temp_dir = TempDir::new().unwrap(); | 255 | let temp_dir = TempDir::new().unwrap(); |
| 246 | 256 | ||
| 247 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 257 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 248 | cmd.current_dir(temp_dir.path()) | 258 | cmd.current_dir(temp_dir.path()) |
| 249 | .args(&["clean"]) | 259 | .args(&["clean"]) |
| @@ -255,14 +265,14 @@ fn test_clean_no_orphans() { | |||
| 255 | #[test] | 265 | #[test] |
| 256 | fn test_clean_with_orphans() { | 266 | fn test_clean_with_orphans() { |
| 257 | let temp_dir = TempDir::new().unwrap(); | 267 | let temp_dir = TempDir::new().unwrap(); |
| 258 | 268 | ||
| 259 | // Create a dead process | 269 | // Create a dead process |
| 260 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 270 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 261 | cmd.current_dir(temp_dir.path()) | 271 | cmd.current_dir(temp_dir.path()) |
| 262 | .args(&["run", "--id", "dead", "echo", "hello"]) | 272 | .args(&["run", "--id", "dead", "echo", "hello"]) |
| 263 | .assert() | 273 | .assert() |
| 264 | .success(); | 274 | .success(); |
| 265 | 275 | ||
| 266 | // Clean up orphaned files | 276 | // Clean up orphaned files |
| 267 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 277 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 268 | cmd.current_dir(temp_dir.path()) | 278 | cmd.current_dir(temp_dir.path()) |
| @@ -271,7 +281,7 @@ fn test_clean_with_orphans() { | |||
| 271 | .success() | 281 | .success() |
| 272 | .stdout(predicate::str::contains("Cleaned up")) | 282 | .stdout(predicate::str::contains("Cleaned up")) |
| 273 | .stdout(predicate::str::contains("orphaned")); | 283 | .stdout(predicate::str::contains("orphaned")); |
| 274 | 284 | ||
| 275 | // Verify files are gone | 285 | // Verify files are gone |
| 276 | assert!(!temp_dir.path().join("dead.pid").exists()); | 286 | assert!(!temp_dir.path().join("dead.pid").exists()); |
| 277 | assert!(!temp_dir.path().join("dead.stdout").exists()); | 287 | assert!(!temp_dir.path().join("dead.stdout").exists()); |
| @@ -281,19 +291,24 @@ fn test_clean_with_orphans() { | |||
| 281 | #[test] | 291 | #[test] |
| 282 | fn test_run_with_complex_command() { | 292 | fn test_run_with_complex_command() { |
| 283 | let temp_dir = TempDir::new().unwrap(); | 293 | let temp_dir = TempDir::new().unwrap(); |
| 284 | 294 | ||
| 285 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 295 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 286 | cmd.current_dir(temp_dir.path()) | 296 | cmd.current_dir(temp_dir.path()) |
| 287 | .args(&[ | 297 | .args(&[ |
| 288 | "run", "--id", "complex", "--", | 298 | "run", |
| 289 | "sh", "-c", "for i in 1 2 3; do echo \"line $i\"; done" | 299 | "--id", |
| 300 | "complex", | ||
| 301 | "--", | ||
| 302 | "sh", | ||
| 303 | "-c", | ||
| 304 | "for i in 1 2 3; do echo \"line $i\"; done", | ||
| 290 | ]) | 305 | ]) |
| 291 | .assert() | 306 | .assert() |
| 292 | .success(); | 307 | .success(); |
| 293 | 308 | ||
| 294 | // Give the process a moment to complete | 309 | // Give the process a moment to complete |
| 295 | std::thread::sleep(Duration::from_millis(100)); | 310 | std::thread::sleep(Duration::from_millis(100)); |
| 296 | 311 | ||
| 297 | // Check the output contains all lines | 312 | // Check the output contains all lines |
| 298 | let stdout_content = fs::read_to_string(temp_dir.path().join("complex.stdout")).unwrap(); | 313 | let stdout_content = fs::read_to_string(temp_dir.path().join("complex.stdout")).unwrap(); |
| 299 | assert!(stdout_content.contains("line 1")); | 314 | assert!(stdout_content.contains("line 1")); |
| @@ -304,14 +319,14 @@ fn test_run_with_complex_command() { | |||
| 304 | #[test] | 319 | #[test] |
| 305 | fn test_timeout_configuration() { | 320 | fn test_timeout_configuration() { |
| 306 | let temp_dir = TempDir::new().unwrap(); | 321 | let temp_dir = TempDir::new().unwrap(); |
| 307 | 322 | ||
| 308 | // Start a process | 323 | // Start a process |
| 309 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 324 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 310 | cmd.current_dir(temp_dir.path()) | 325 | cmd.current_dir(temp_dir.path()) |
| 311 | .args(&["run", "--id", "timeout-test", "sleep", "5"]) | 326 | .args(&["run", "--id", "timeout-test", "sleep", "5"]) |
| 312 | .assert() | 327 | .assert() |
| 313 | .success(); | 328 | .success(); |
| 314 | 329 | ||
| 315 | // Stop with custom timeout (should work normally since sleep responds to SIGTERM) | 330 | // Stop with custom timeout (should work normally since sleep responds to SIGTERM) |
| 316 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 331 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 317 | cmd.current_dir(temp_dir.path()) | 332 | cmd.current_dir(temp_dir.path()) |
| @@ -324,10 +339,10 @@ fn test_timeout_configuration() { | |||
| 324 | #[test] | 339 | #[test] |
| 325 | fn test_invalid_process_id() { | 340 | fn test_invalid_process_id() { |
| 326 | let temp_dir = TempDir::new().unwrap(); | 341 | let temp_dir = TempDir::new().unwrap(); |
| 327 | 342 | ||
| 328 | // Create an invalid PID file | 343 | // Create an invalid PID file |
| 329 | fs::write(temp_dir.path().join("invalid.pid"), "not-a-number").unwrap(); | 344 | fs::write(temp_dir.path().join("invalid.pid"), "not-a-number").unwrap(); |
| 330 | 345 | ||
| 331 | // Status should handle it gracefully | 346 | // Status should handle it gracefully |
| 332 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 347 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 333 | cmd.current_dir(temp_dir.path()) | 348 | cmd.current_dir(temp_dir.path()) |
| @@ -335,7 +350,7 @@ fn test_invalid_process_id() { | |||
| 335 | .assert() | 350 | .assert() |
| 336 | .success() | 351 | .success() |
| 337 | .stdout(predicate::str::contains("ERROR")); | 352 | .stdout(predicate::str::contains("ERROR")); |
| 338 | 353 | ||
| 339 | // Clean should remove it | 354 | // Clean should remove it |
| 340 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 355 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 341 | cmd.current_dir(temp_dir.path()) | 356 | cmd.current_dir(temp_dir.path()) |
| @@ -348,7 +363,7 @@ fn test_invalid_process_id() { | |||
| 348 | #[test] | 363 | #[test] |
| 349 | fn test_list_quiet_mode() { | 364 | fn test_list_quiet_mode() { |
| 350 | let temp_dir = TempDir::new().unwrap(); | 365 | let temp_dir = TempDir::new().unwrap(); |
| 351 | 366 | ||
| 352 | // Test quiet mode with no processes | 367 | // Test quiet mode with no processes |
| 353 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 368 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 354 | cmd.current_dir(temp_dir.path()) | 369 | cmd.current_dir(temp_dir.path()) |
| @@ -356,14 +371,14 @@ fn test_list_quiet_mode() { | |||
| 356 | .assert() | 371 | .assert() |
| 357 | .success() | 372 | .success() |
| 358 | .stdout(predicate::str::is_empty()); | 373 | .stdout(predicate::str::is_empty()); |
| 359 | 374 | ||
| 360 | // Create a process | 375 | // Create a process |
| 361 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 376 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 362 | cmd.current_dir(temp_dir.path()) | 377 | cmd.current_dir(temp_dir.path()) |
| 363 | .args(&["run", "--id", "quiet-test", "echo", "done"]) | 378 | .args(&["run", "--id", "quiet-test", "echo", "done"]) |
| 364 | .assert() | 379 | .assert() |
| 365 | .success(); | 380 | .success(); |
| 366 | 381 | ||
| 367 | // Test quiet mode with process - should output colon-separated format | 382 | // Test quiet mode with process - should output colon-separated format |
| 368 | let mut cmd = Command::cargo_bin("demon").unwrap(); | 383 | let mut cmd = Command::cargo_bin("demon").unwrap(); |
| 369 | cmd.current_dir(temp_dir.path()) | 384 | cmd.current_dir(temp_dir.path()) |
| @@ -384,7 +399,9 @@ fn test_llm_command() { | |||
| 384 | cmd.args(&["llm"]) | 399 | cmd.args(&["llm"]) |
| 385 | .assert() | 400 | .assert() |
| 386 | .success() | 401 | .success() |
| 387 | .stdout(predicate::str::contains("# Demon - Daemon Process Management CLI")) | 402 | .stdout(predicate::str::contains( |
| 403 | "# Demon - Daemon Process Management CLI", | ||
| 404 | )) | ||
| 388 | .stdout(predicate::str::contains("## Available Commands")) | 405 | .stdout(predicate::str::contains("## Available Commands")) |
| 389 | .stdout(predicate::str::contains("demon run")) | 406 | .stdout(predicate::str::contains("demon run")) |
| 390 | .stdout(predicate::str::contains("demon stop")) | 407 | .stdout(predicate::str::contains("demon stop")) |
| @@ -396,4 +413,4 @@ fn test_llm_command() { | |||
| 396 | .stdout(predicate::str::contains("Common Workflows")) | 413 | .stdout(predicate::str::contains("Common Workflows")) |
| 397 | .stdout(predicate::str::contains("Best Practices")) | 414 | .stdout(predicate::str::contains("Best Practices")) |
| 398 | .stdout(predicate::str::contains("Integration Tips")); | 415 | .stdout(predicate::str::contains("Integration Tips")); |
| 399 | } \ No newline at end of file | 416 | } |
