diff options
| -rw-r--r-- | IMPROVEMENT_PLAN_V2.md | 248 | ||||
| -rw-r--r-- | src/main.rs | 54 |
2 files changed, 275 insertions, 27 deletions
diff --git a/IMPROVEMENT_PLAN_V2.md b/IMPROVEMENT_PLAN_V2.md new file mode 100644 index 0000000..abffa0e --- /dev/null +++ b/IMPROVEMENT_PLAN_V2.md | |||
| @@ -0,0 +1,248 @@ | |||
| 1 | # Demon CLI Improvement Plan v2 | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | This plan outlines four major improvements to enhance the demon CLI tool's usability, error handling, and documentation. | ||
| 5 | |||
| 6 | ## Task 1: Rename PidFileData to PidFile | ||
| 7 | |||
| 8 | ### Rationale | ||
| 9 | - Shorter, cleaner name | ||
| 10 | - More intuitive - it represents a PID file, not just data about one | ||
| 11 | - Follows Rust naming conventions better | ||
| 12 | |||
| 13 | ### Implementation Steps | ||
| 14 | 1. Rename struct `PidFileData` to `PidFile` | ||
| 15 | 2. Update all references throughout the codebase | ||
| 16 | 3. Update comments and documentation | ||
| 17 | 4. Verify compilation and tests pass | ||
| 18 | |||
| 19 | ### Files to modify | ||
| 20 | - `src/main.rs` - struct definition and all usages | ||
| 21 | |||
| 22 | ### Risk Assessment | ||
| 23 | - **Low risk** - Simple rename refactor | ||
| 24 | - No functional changes | ||
| 25 | - All tests should continue to pass | ||
| 26 | |||
| 27 | ## Task 2: Implement PidFileReadError Enum | ||
| 28 | |||
| 29 | ### Rationale | ||
| 30 | - Better error handling with specific error types | ||
| 31 | - Eliminates redundant file existence checks | ||
| 32 | - More idiomatic Rust error handling | ||
| 33 | - Cleaner code in error handling paths | ||
| 34 | |||
| 35 | ### Design | ||
| 36 | ```rust | ||
| 37 | #[derive(Debug)] | ||
| 38 | pub enum PidFileReadError { | ||
| 39 | /// The PID file does not exist | ||
| 40 | FileNotFound, | ||
| 41 | /// The PID file exists but has invalid content | ||
| 42 | FileInvalid(String), // Include reason for invalidity | ||
| 43 | /// IO error occurred while reading | ||
| 44 | IoError(std::io::Error), | ||
| 45 | } | ||
| 46 | ``` | ||
| 47 | |||
| 48 | ### Implementation Steps | ||
| 49 | 1. Define `PidFileReadError` enum with appropriate variants | ||
| 50 | 2. Implement `Display` and `Error` traits for the enum | ||
| 51 | 3. Update `PidFile::read_from_file()` to return `Result<PidFile, PidFileReadError>` | ||
| 52 | 4. Update all call sites to handle the specific error types: | ||
| 53 | - `is_process_running()` - handle FileNotFound and FileInvalid as "not running" | ||
| 54 | - `stop_daemon()` - handle FileNotFound as "not running", FileInvalid as "cleanup needed" | ||
| 55 | - `list_daemons()` - handle FileInvalid as "INVALID" entry | ||
| 56 | - `status_daemon()` - handle FileNotFound as "NOT FOUND", FileInvalid as "ERROR" | ||
| 57 | - `clean_orphaned_files()` - handle FileInvalid as "needs cleanup" | ||
| 58 | 5. Remove redundant `Path::new().exists()` checks where the error type provides this info | ||
| 59 | 6. Test all error scenarios | ||
| 60 | |||
| 61 | ### Files to modify | ||
| 62 | - `src/main.rs` - enum definition, read_from_file method, all usage sites | ||
| 63 | |||
| 64 | ### Risk Assessment | ||
| 65 | - **Medium risk** - Changes error handling logic | ||
| 66 | - Need thorough testing of error scenarios | ||
| 67 | - Must ensure all edge cases are handled properly | ||
| 68 | |||
| 69 | ## Task 3: Make --id a Positional Argument | ||
| 70 | |||
| 71 | ### Analysis | ||
| 72 | |||
| 73 | #### Current CLI Pattern | ||
| 74 | ```bash | ||
| 75 | demon run --id web-server python -m http.server 8080 | ||
| 76 | demon stop --id web-server | ||
| 77 | demon status --id web-server | ||
| 78 | ``` | ||
| 79 | |||
| 80 | #### Proposed CLI Pattern | ||
| 81 | ```bash | ||
| 82 | demon run web-server python -m http.server 8080 | ||
| 83 | demon stop web-server | ||
| 84 | demon status web-server | ||
| 85 | ``` | ||
| 86 | |||
| 87 | #### Pros | ||
| 88 | - **Better UX**: More natural and concise | ||
| 89 | - **Consistent with common tools**: Similar to git, docker, etc. | ||
| 90 | - **Faster to type**: No --id flag needed | ||
| 91 | - **More intuitive**: ID naturally comes first before the command | ||
| 92 | |||
| 93 | #### Cons | ||
| 94 | - **Breaking change**: Existing scripts/users need to update | ||
| 95 | - **Potential ambiguity**: ID could be confused with command in some cases | ||
| 96 | - **Parsing complexity**: Need careful handling of edge cases | ||
| 97 | |||
| 98 | #### Design Decisions | ||
| 99 | 1. **Make ID positional for all commands that currently use --id** | ||
| 100 | 2. **Keep -- separator support** for complex commands | ||
| 101 | 3. **Update help text** to reflect new usage | ||
| 102 | 4. **Maintain backward compatibility** by supporting both patterns initially (with deprecation warning) | ||
| 103 | |||
| 104 | #### Commands to Update | ||
| 105 | - `run <id> <command...>` - ID becomes first positional arg | ||
| 106 | - `stop <id>` - ID becomes positional arg, remove timeout flag positioning issues | ||
| 107 | - `tail <id>` - ID becomes positional arg | ||
| 108 | - `cat <id>` - ID becomes positional arg | ||
| 109 | - `status <id>` - ID becomes positional arg | ||
| 110 | |||
| 111 | #### Implementation Strategy | ||
| 112 | 1. **Phase 1**: Support both patterns with deprecation warnings | ||
| 113 | 2. **Phase 2**: Remove old pattern support (future version) | ||
| 114 | |||
| 115 | ### Implementation Steps | ||
| 116 | 1. Define new argument structures with positional ID fields | ||
| 117 | 2. Update clap derive macros to make ID positional | ||
| 118 | 3. Update help text and documentation strings | ||
| 119 | 4. Add deprecation warnings for --id usage (optional) | ||
| 120 | 5. Update all internal function calls | ||
| 121 | 6. Update tests to use new CLI pattern | ||
| 122 | 7. Update LLM guide output | ||
| 123 | |||
| 124 | ### Files to modify | ||
| 125 | - `src/main.rs` - argument structures, help text | ||
| 126 | - `tests/cli.rs` - all test commands | ||
| 127 | - LLM guide text | ||
| 128 | |||
| 129 | ### Risk Assessment | ||
| 130 | - **High risk** - Breaking change for users | ||
| 131 | - Need to update all tests | ||
| 132 | - Must carefully verify argument parsing edge cases | ||
| 133 | - Consider gradual migration strategy | ||
| 134 | |||
| 135 | ## Task 4: Write Comprehensive README.md | ||
| 136 | |||
| 137 | ### Target Audience | ||
| 138 | - Developers who need background process management | ||
| 139 | - LLM agents and their operators | ||
| 140 | - DevOps engineers running long-term tasks | ||
| 141 | - Anyone working with npm run dev, build processes, etc. | ||
| 142 | |||
| 143 | ### Content Structure | ||
| 144 | ```markdown | ||
| 145 | # Demon - Background Process Manager | ||
| 146 | |||
| 147 | ## Overview | ||
| 148 | Brief description focusing on core value proposition | ||
| 149 | |||
| 150 | ## Installation | ||
| 151 | - `cargo install demon` (when published) | ||
| 152 | - Building from source | ||
| 153 | - System requirements | ||
| 154 | |||
| 155 | ## Quick Start | ||
| 156 | - Basic examples | ||
| 157 | - Common workflows | ||
| 158 | |||
| 159 | ## Use Cases | ||
| 160 | - Development servers (npm run dev) | ||
| 161 | - Background tasks and scripts | ||
| 162 | - LLM agent process management | ||
| 163 | - CI/CD pipeline tasks | ||
| 164 | - Long-running computations | ||
| 165 | |||
| 166 | ## Command Reference | ||
| 167 | - Complete command documentation | ||
| 168 | - Examples for each command | ||
| 169 | - Common flags and options | ||
| 170 | |||
| 171 | ## Integration with LLM Agents | ||
| 172 | - How agents can use demon | ||
| 173 | - Machine-readable output formats | ||
| 174 | - Best practices for automation | ||
| 175 | |||
| 176 | ## Advanced Usage | ||
| 177 | - File management | ||
| 178 | - Process lifecycle | ||
| 179 | - Troubleshooting | ||
| 180 | - Performance considerations | ||
| 181 | |||
| 182 | ## Contributing | ||
| 183 | - Development setup | ||
| 184 | - Testing | ||
| 185 | - Contribution guidelines | ||
| 186 | ``` | ||
| 187 | |||
| 188 | ### Key Messages | ||
| 189 | 1. **Simplicity**: Easy background process management | ||
| 190 | 2. **Visibility**: Always know what's running and its status | ||
| 191 | 3. **Integration**: Built for automation and LLM agents | ||
| 192 | 4. **Reliability**: Robust process lifecycle management | ||
| 193 | |||
| 194 | ### Implementation Steps | ||
| 195 | 1. Research similar tools for README inspiration | ||
| 196 | 2. Write comprehensive content covering all sections | ||
| 197 | 3. Include practical examples and screenshots/command outputs | ||
| 198 | 4. Add badges for build status, crates.io, etc. (when applicable) | ||
| 199 | 5. Review and refine for clarity and completeness | ||
| 200 | |||
| 201 | ### Files to create | ||
| 202 | - `README.md` - comprehensive documentation | ||
| 203 | |||
| 204 | ### Risk Assessment | ||
| 205 | - **Low risk** - Documentation only | ||
| 206 | - No functional changes | ||
| 207 | - Easy to iterate and improve | ||
| 208 | |||
| 209 | ## Execution Order | ||
| 210 | |||
| 211 | 1. **Task 1**: Rename PidFileData to PidFile (Low risk, enables clean foundation) | ||
| 212 | 2. **Task 2**: Implement PidFileReadError enum (Medium risk, improves error handling) | ||
| 213 | 3. **Task 3**: Make --id positional (High risk, but significant UX improvement) | ||
| 214 | 4. **Task 4**: Write README.md (Low risk, improves project presentation) | ||
| 215 | |||
| 216 | ## Testing Strategy | ||
| 217 | |||
| 218 | After each task: | ||
| 219 | 1. Run `cargo build` to ensure compilation | ||
| 220 | 2. Run `cargo test` to ensure all tests pass | ||
| 221 | 3. Manual testing of affected functionality | ||
| 222 | 4. Format code with `cargo fmt` | ||
| 223 | 5. Commit changes with descriptive message | ||
| 224 | |||
| 225 | ## Success Criteria | ||
| 226 | |||
| 227 | - All tests pass after each change | ||
| 228 | - No regressions in functionality | ||
| 229 | - Improved error messages and handling | ||
| 230 | - Better CLI usability | ||
| 231 | - Comprehensive documentation | ||
| 232 | - Clean, maintainable code | ||
| 233 | |||
| 234 | ## Rollback Plan | ||
| 235 | |||
| 236 | Each task will be committed separately, allowing for easy rollback if issues arise: | ||
| 237 | 1. Git commit after each successful task | ||
| 238 | 2. If issues found, can revert specific commits | ||
| 239 | 3. Tests provide safety net for functionality | ||
| 240 | |||
| 241 | ## Timeline Estimate | ||
| 242 | |||
| 243 | - Task 1: 15-20 minutes (straightforward refactor) | ||
| 244 | - Task 2: 30-45 minutes (error handling logic) | ||
| 245 | - Task 3: 45-60 minutes (CLI argument changes + tests) | ||
| 246 | - Task 4: 30-45 minutes (documentation writing) | ||
| 247 | |||
| 248 | Total: ~2-3 hours \ No newline at end of file | ||
diff --git a/src/main.rs b/src/main.rs index c82208b..4ab1dd9 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -11,20 +11,20 @@ use std::time::Duration; | |||
| 11 | 11 | ||
| 12 | /// Represents the contents of a PID file | 12 | /// Represents the contents of a PID file |
| 13 | #[derive(Debug, Clone)] | 13 | #[derive(Debug, Clone)] |
| 14 | struct PidFileData { | 14 | struct PidFile { |
| 15 | /// Process ID | 15 | /// Process ID |
| 16 | pid: u32, | 16 | pid: u32, |
| 17 | /// Command that was executed (program + arguments) | 17 | /// Command that was executed (program + arguments) |
| 18 | command: Vec<String>, | 18 | command: Vec<String>, |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | impl PidFileData { | 21 | impl PidFile { |
| 22 | /// Create a new PidFileData instance | 22 | /// Create a new PidFile instance |
| 23 | fn new(pid: u32, command: Vec<String>) -> Self { | 23 | fn new(pid: u32, command: Vec<String>) -> Self { |
| 24 | Self { pid, command } | 24 | Self { pid, command } |
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | /// Write PID file data to a file | 27 | /// Write PID file to a file |
| 28 | fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> { | 28 | fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> { |
| 29 | let mut file = File::create(path)?; | 29 | let mut file = File::create(path)?; |
| 30 | writeln!(file, "{}", self.pid)?; | 30 | writeln!(file, "{}", self.pid)?; |
| @@ -34,7 +34,7 @@ impl PidFileData { | |||
| 34 | Ok(()) | 34 | Ok(()) |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | /// Read PID file data from a file | 37 | /// Read PID file from a file |
| 38 | fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self> { | 38 | fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self> { |
| 39 | let contents = std::fs::read_to_string(path)?; | 39 | let contents = std::fs::read_to_string(path)?; |
| 40 | let lines: Vec<&str> = contents.lines().collect(); | 40 | let lines: Vec<&str> = contents.lines().collect(); |
| @@ -244,8 +244,8 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> { | |||
| 244 | .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; | 244 | .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; |
| 245 | 245 | ||
| 246 | // Write PID and command to file | 246 | // Write PID and command to file |
| 247 | let pid_data = PidFileData::new(child.id(), command.to_vec()); | 247 | let pid_file_data = PidFile::new(child.id(), command.to_vec()); |
| 248 | pid_data.write_to_file(&pid_file)?; | 248 | pid_file_data.write_to_file(&pid_file)?; |
| 249 | 249 | ||
| 250 | // Don't wait for the child - let it run detached | 250 | // Don't wait for the child - let it run detached |
| 251 | std::mem::forget(child); | 251 | std::mem::forget(child); |
| @@ -261,14 +261,14 @@ fn is_process_running(pid_file: &str) -> Result<bool> { | |||
| 261 | return Ok(false); // No PID file means no running process | 261 | return Ok(false); // No PID file means no running process |
| 262 | } | 262 | } |
| 263 | 263 | ||
| 264 | let pid_data = match PidFileData::read_from_file(pid_file) { | 264 | let pid_file_data = match PidFile::read_from_file(pid_file) { |
| 265 | Ok(data) => data, | 265 | Ok(data) => data, |
| 266 | Err(_) => return Ok(false), // Invalid PID file | 266 | Err(_) => return Ok(false), // Invalid PID file |
| 267 | }; | 267 | }; |
| 268 | 268 | ||
| 269 | // Check if process is still running using kill -0 | 269 | // Check if process is still running using kill -0 |
| 270 | let output = Command::new("kill") | 270 | let output = Command::new("kill") |
| 271 | .args(&["-0", &pid_data.pid.to_string()]) | 271 | .args(&["-0", &pid_file_data.pid.to_string()]) |
| 272 | .output()?; | 272 | .output()?; |
| 273 | 273 | ||
| 274 | Ok(output.status.success()) | 274 | Ok(output.status.success()) |
| @@ -278,7 +278,7 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | |||
| 278 | let pid_file = format!("{}.pid", id); | 278 | let pid_file = format!("{}.pid", id); |
| 279 | 279 | ||
| 280 | // Check if PID file exists and read PID data | 280 | // Check if PID file exists and read PID data |
| 281 | let pid_data = match PidFileData::read_from_file(&pid_file) { | 281 | let pid_file_data = match PidFile::read_from_file(&pid_file) { |
| 282 | Ok(data) => data, | 282 | Ok(data) => data, |
| 283 | Err(_) => { | 283 | Err(_) => { |
| 284 | if Path::new(&pid_file).exists() { | 284 | if Path::new(&pid_file).exists() { |
| @@ -291,7 +291,7 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | |||
| 291 | } | 291 | } |
| 292 | }; | 292 | }; |
| 293 | 293 | ||
| 294 | let pid = pid_data.pid; | 294 | let pid = pid_file_data.pid; |
| 295 | 295 | ||
| 296 | tracing::info!( | 296 | tracing::info!( |
| 297 | "Stopping daemon '{}' (PID: {}) with timeout {}s", | 297 | "Stopping daemon '{}' (PID: {}) with timeout {}s", |
| @@ -599,19 +599,19 @@ fn list_daemons(quiet: bool) -> Result<()> { | |||
| 599 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | 599 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); |
| 600 | 600 | ||
| 601 | // Read PID data from file | 601 | // Read PID data from file |
| 602 | match PidFileData::read_from_file(&path) { | 602 | match PidFile::read_from_file(&path) { |
| 603 | Ok(pid_data) => { | 603 | Ok(pid_file_data) => { |
| 604 | let status = if is_process_running_by_pid(pid_data.pid) { | 604 | let status = if is_process_running_by_pid(pid_file_data.pid) { |
| 605 | "RUNNING" | 605 | "RUNNING" |
| 606 | } else { | 606 | } else { |
| 607 | "DEAD" | 607 | "DEAD" |
| 608 | }; | 608 | }; |
| 609 | 609 | ||
| 610 | if quiet { | 610 | if quiet { |
| 611 | println!("{}:{}:{}", id, pid_data.pid, status); | 611 | println!("{}:{}:{}", id, pid_file_data.pid, status); |
| 612 | } else { | 612 | } else { |
| 613 | let command = pid_data.command_string(); | 613 | let command = pid_file_data.command_string(); |
| 614 | println!("{:<20} {:<8} {:<10} {}", id, pid_data.pid, status, command); | 614 | println!("{:<20} {:<8} {:<10} {}", id, pid_file_data.pid, status, command); |
| 615 | } | 615 | } |
| 616 | } | 616 | } |
| 617 | Err(_) => { | 617 | Err(_) => { |
| @@ -649,12 +649,12 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 649 | } | 649 | } |
| 650 | 650 | ||
| 651 | // Read PID data from file | 651 | // Read PID data from file |
| 652 | match PidFileData::read_from_file(&pid_file) { | 652 | match PidFile::read_from_file(&pid_file) { |
| 653 | Ok(pid_data) => { | 653 | Ok(pid_file_data) => { |
| 654 | println!("PID: {}", pid_data.pid); | 654 | println!("PID: {}", pid_file_data.pid); |
| 655 | println!("Command: {}", pid_data.command_string()); | 655 | println!("Command: {}", pid_file_data.command_string()); |
| 656 | 656 | ||
| 657 | if is_process_running_by_pid(pid_data.pid) { | 657 | if is_process_running_by_pid(pid_file_data.pid) { |
| 658 | println!("Status: RUNNING"); | 658 | println!("Status: RUNNING"); |
| 659 | 659 | ||
| 660 | // Show file information | 660 | // Show file information |
| @@ -696,13 +696,13 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 696 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | 696 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); |
| 697 | 697 | ||
| 698 | // Read PID data from file | 698 | // Read PID data from file |
| 699 | match PidFileData::read_from_file(&path) { | 699 | match PidFile::read_from_file(&path) { |
| 700 | Ok(pid_data) => { | 700 | Ok(pid_file_data) => { |
| 701 | // Check if process is still running | 701 | // Check if process is still running |
| 702 | if !is_process_running_by_pid(pid_data.pid) { | 702 | if !is_process_running_by_pid(pid_file_data.pid) { |
| 703 | println!( | 703 | println!( |
| 704 | "Cleaning up orphaned files for '{}' (PID: {})", | 704 | "Cleaning up orphaned files for '{}' (PID: {})", |
| 705 | id, pid_data.pid | 705 | id, pid_file_data.pid |
| 706 | ); | 706 | ); |
| 707 | 707 | ||
| 708 | // Remove PID file | 708 | // Remove PID file |
| @@ -737,7 +737,7 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 737 | tracing::info!( | 737 | tracing::info!( |
| 738 | "Skipping '{}' (PID: {}) - process is still running", | 738 | "Skipping '{}' (PID: {}) - process is still running", |
| 739 | id, | 739 | id, |
| 740 | pid_data.pid | 740 | pid_file_data.pid |
| 741 | ); | 741 | ); |
| 742 | } | 742 | } |
| 743 | } | 743 | } |
