Skip to main content

add-new-command

Guide for implementing new CLI commands in the deployer using outside-in development. Covers presentation layer scaffolding, application handler, confirmation prompts, E2E testing, and documentation. Use when adding new commands like validate, render, backup, or any console subcommand. Triggers on "add command", "new command", "implement command", "create subcommand", or "add CLI command".

소스 정보

저장소
torrust/torrust-tracker-deployer
최근 소스 활동
2026년 3월 1일 09:34
감지된 SKILL.md 언어
영어
스타
5
포크
2

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
add-new-command
description
Guide for implementing new CLI commands in the deployer using outside-in development. Covers presentation layer scaffolding, application handler, confirmation prompts, E2E testing, and documentation. Use when adding new commands like validate, render, backup, or any console subcommand. Triggers on "add command", "new command", "implement command", "create subcommand", or "add CLI command".
metadata
{"author":"torrust","version":"1.0"}
# Adding New Commands to the Deployer This skill guides you through implementing new CLI commands for the Torrust Tracker Deployer using an **outside-in** (presentation → application → domain) development approach. ## Why Outside-In? **Start from the outer layers (user interface) and work inward:** ```text Presentation (CLI) → Application (handlers) → Domain (business logic) ``` **Benefits for infrastructure applications:** 1. **Test immediately** - Run command and check output after each step 2. **Validate UX early** - Verify command interface before business logic 3. **Defer hard problems** - Infrastructure code (filesystems, APIs, databases) is hard to unit test 4. **E2E focus** - Integration tests work from day one **Alternative (inside-out)**: Starting from domain/business logic requires mocking external dependencies or waiting until full implementation to test real behavior. ## Implementation Phases ### Phase 1: Presentation Layer Stub **Goal**: Make command runnable with proper routing and empty implementation. **What to build**: ```rust // 1. Add CLI command variant // src/presentation/input/cli/commands.rs pub enum Commands { // ... existing commands YourCommand { name: String, #[arg(long)] force: bool, // If needed }, } // 2. Add routing // src/presentation/dispatch/router.rs Commands::YourCommand { name, force } => { self.handle_your_command(name, force).await } // 3. Create controller stub // src/presentation/controllers/your_command/handler.rs pub struct YourCommandController { // Dependencies (injected later) } impl YourCommandController { pub fn execute(&mut self, name: &str) -> Result<()> { // Stub: just show progress steps self.progress.start("Step 1: Validate input")?; self.progress.complete("Validation complete")?; Ok(()) } } // 4. Define presentation errors // src/presentation/controllers/your_command/errors.rs #[derive(Error, Debug)] pub enum YourCommandError { #[error("Invalid name: {0}")] InvalidName(String), } impl YourCommandError { pub fn help(&self) -> Option<String> { match self { Self::InvalidName(_) => Some( "Use lowercase alphanumeric with hyphens".to_string() ), } } } // 5. Wire in container // src/bootstrap/container.rs pub fn your_command_controller(&self) -> YourCommandController { YourCommandController::new(/* dependencies */) } ``` **Test Phase 1**: ```bash # Test 1: Help text displays correctly cargo run -- your-command --help # Test 2: Error handling works (file not found, invalid input) cargo run -- your-command /path/to/nonexistent # Test 3: Success path (stub shows progress) cargo run -- your-command valid-input ``` **Expected Behavior Phase 1**: ✅ Help text shows: - Command description - Arguments and flags - Usage examples - Options documentation ✅ Error handling shows: - Clear error message - Troubleshooting help (via `.help()` method) - Actionable guidance ✅ Success path shows: - Progress steps (1/N, 2/N, 3/N) - Step descriptions - Success message - Stub completion (no real work yet) **What Validates**: - CLI registration works - Routing is correct - Controller is wired - Error types have `.help()` methods - Progress reporting displays properly **What Doesn't Validate**: - Real business logic (Phase 2) - Domain validation (Phase 2) - Actual operations (Phase 2) **Commit**: `feat: [#ISSUE] add your-command presentation layer stub` > **Query commands (commands that return data)**: If your command returns a value rather than just performing an action (e.g., `exists`, `show`, `list`), the presentation layer needs a view layer in addition to the controller. Create: > > - `src/presentation/cli/views/commands/your_command/view_data/your_details.rs` — DTO populated with the result > - `src/presentation/cli/views/commands/your_command/views/text_view.rs` — `impl Render<YourResult>` > - `src/presentation/cli/views/commands/your_command/views/json_view.rs` — `impl Render<YourResult>` > > See [`src/presentation/cli/views/commands/exists/`](../../../src/presentation/cli/views/commands/exists/) as a reference. ### Phase 2: Application Handler **Goal**: Implement business logic that actually does the work. **What to build**: ```rust // 1. Create application handler // src/application/command_handlers/your_command/handler.rs pub struct YourCommandHandler { repository: Arc<dyn EnvironmentRepository>, working_directory: Arc<Path>, } impl YourCommandHandler { pub fn execute(&self, name: &EnvironmentName) -> Result<()> { // 1. Verify preconditions if !self.repository.exists(name)? { return Err(YourCommandError::NotFound); } // 2. Perform the actual work self.do_the_work(name)?; // 3. Update state if needed // repository.update(...)?; Ok(()) } fn do_the_work(&self, name: &EnvironmentName) -> Result<()> { // Real implementation: // - Call external tools (Ansible, OpenTofu) // - Modify filesystem // - Update database/registry // - Generate artifacts tracing::info!("Executing work for {}", name); Ok(()) } } // 2. Create application errors // src/application/command_handlers/your_command/errors.rs #[derive(Error, Debug)] pub enum YourCommandError { #[error("Environment not found: {0}")] NotFound(String), #[error("Operation failed: {0}")] OperationFailed(String), } // 3. Update controller to use handler // src/presentation/controllers/your_command/handler.rs pub struct YourCommandController { handler: YourCommandHandler, // Inject handler progress: ProgressReporter, } impl YourCommandController { pub fn execute(&mut self, name: &str) -> Result<()> { let env_name = EnvironmentName::try_from(name)?; self.progress.start("Validating environment")?; // Validation... self.progress.start("Performing operation")?; self.handler.execute(&env_name)?; // Delegate to handler self.progress.complete("Operation complete")?; Ok(()) } } ``` **Test Phase 2**: ```bash # Test 1: Valid input performs real work cargo run -- your-command valid-input # Test 2: Invalid preconditions fail gracefully cargo run -- your-command nonexistent # Test 3: Domain validation errors are caught cargo run -- your-command invalid-format # Test 4: Business rule violations show helpful errors cargo run -- your-command edge-case-input ``` **Expected Behavior Phase 2**: ✅ **Valid Input** - Real operations execute: - Progress steps complete with actual work - State changes occur (files created/updated, database modified) - Detailed success message with results - Operation completes successfully Example (validate command): ```text ⏳ [1/3] Loading configuration file... ⏳ ✓ Configuration file loaded (took 0ms) ⏳ [2/3] Validating JSON schema... ⏳ ✓ Schema validation passed (took 0ms) ⏳ [3/3] Validating configuration fields... ⏳ ✓ Field validation passed (took 0ms) ✅ Configuration file 'envs/lxd-local-example.json' is valid Environment Details: • Name: lxd-local-example • Provider: lxd • Prometheus: Enabled • Grafana: Enabled ``` ✅ **Invalid Preconditions** - Clear presentation errors: - Error message explains what's wrong - `.help()` provides actionable guidance - Exit code is non-zero Example: ```text ❌ Command failed: Configuration file not found: /tmp/nonexistent.json For detailed troubleshooting: Verify the file path is correct: /tmp/nonexistent.json Use 'create template' to generate a valid configuration file. ``` ✅ **Domain Validation Errors** - Application layer catches issues: - Specific error message (not generic) - Context about what was validated - Helpful troubleshooting steps Example (JSON parsing): ```text ❌ Validation failed for configuration file: /tmp/invalid.json For detailed troubleshooting: JSON parsing failed for file '/tmp/invalid.json'. Error details: key must be a string at line 1 column 3 Common issues: - Missing or extra commas - Unmatched braces or brackets - Invalid escape sequences ``` ✅ **Business Rule Violations** - Domain layer enforces constraints: - Detailed error about which rule failed - Why the rule exists - How to fix the problem Example (missing SSH keys): ```text ❌ Validation failed: SSH private key file not found: /tmp/nonexistent-key This means the configuration file has valid JSON syntax but violates domain constraints or business rules. Common issues: - SSH key files don't exist at specified paths - Invalid environment name (must be lowercase with dashes) - Invalid port numbers or IP addresses ``` **What Validates**: - Real business logic executes - Application handler performs actual work - Domain validation catches constraint violations - Error propagation works (domain → application → presentation) - State changes occur correctly **What Doesn't Validate**: - User confirmation prompts (Phase 3) - E2E integration across commands (Phase 4) - Full error coverage (Phase 4) **Verification Checklist**: ```bash # 1. Compilation succeeds cargo check # 2. Code quality passes cargo clippy --all-targets --all-features -- -D warnings cargo fmt # 3. Manual tests cover all scenarios # - Valid input success path # - Presentation errors (file not found, wrong type) # - Application errors (JSON parsing, schema validation) # - Domain errors (business rules, constraints) # 4. Verify state changes # - Check filesystem modifications # - Verify database/registry updates # - Confirm generated artifacts ``` **Commit**: `feat: [#ISSUE] add your-command application layer handler` #### Unit Tests for the Handler Write unit tests directly alongside the handler in `src/application/command_handlers/your_command/tests/mod.rs`. Cover: - Success path (e.g., operation succeeds when environment exists/does not exist) - Error path (repository failure propagates correctly) Use `FileEnvironmentRepository` (via `EnvironmentTestBuilder`) or a simple inline stub: ```rust struct FailingRepository; impl EnvironmentRepository for FailingRepository { fn find(...) -> Result<...> { Err(RepositoryError::...) } } ``` Add `#[cfg(test)] mod tests;` to `src/application/command_handlers/your_command/mod.rs`. #### SDK Layer Update If the command is useful for programmatic workflows, expose it through the SDK: 1. Add a method `your_command(...)` in both: - `packages/sdk/src/deployer.rs` - `src/presentation/sdk/deployer.rs` 2. Add a `From<YourCommandHandlerError>` impl in `packages/sdk/src/error.rs` (new `SdkError` variant). **Without this, doc examples using the `?` operator will fail doc tests**: ```rust // packages/sdk/src/error.rs /// [`super::deployer::Deployer::your_command`] failed. #[error(transparent)] YourCommand(#[from] YourCommandHandlerError), ``` 3. Verify doc tests pass: `cargo test --doc --workspace` ### Phase 3: User Confirmation (Optional) **Goal**: Add interactive confirmation for destructive/important operations. **When to add**: - ✅ Destructive operations (delete, purge, destroy) - ✅ Operations that modify infrastructure - ✅ Operations that cost money - ✅ Operations that are hard to undo **When to SKIP**: - ❌ Read-only operations (list, show, validate) - ❌ Dry-run commands that don't change state - ❌ Operations with `--dry-run` mode - ❌ Commands that generate artifacts without side effects **Decision**: If your command is read-only or non-destructive, **skip to Phase 4** (E2E Tests). **What to build** (if needed): ```rust // src/presentation/controllers/your_command/handler.rs impl YourCommandController { pub fn execute(&mut self, name: &str, force: bool) -> Result<()> { let env_name = EnvironmentName::try_from(name)?; // Show confirmation unless --force if !force && !self.confirm_operation(&env_name)? { return Err(YourCommandError::UserCancelled); } // Continue with operation... self.handler.execute(&env_name)?; Ok(()) } fn confirm_operation(&mut self, name: &EnvironmentName) -> Result<bool> { self.progress.blank_line()?; self.progress.output().lock().borrow_mut().warning( "⚠️ WARNING: This operation will [describe impact]" ); self.progress.output().lock().borrow_mut().warning( "This action cannot be undone!" ); self.progress.blank_line()?; print!("Continue? [y/N]: "); std::io::stdout().flush()?; let mut input = String::new(); std::io::stdin().read_line(&mut input) .map_err(|e| YourCommandError::IoError(e.to_string()))?;
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기