diff --git a/.github/skills/regenerate-cli-docs/skill.md b/.github/skills/regenerate-cli-docs/skill.md new file mode 100644 index 00000000..5e4d13c1 --- /dev/null +++ b/.github/skills/regenerate-cli-docs/skill.md @@ -0,0 +1,278 @@ +--- +name: regenerate-cli-docs +description: Regenerate machine-readable CLI documentation JSON after modifying the CLI interface. Use when adding/removing commands, changing arguments, updating help text, or when CLI structure changes. Triggers on "regenerate CLI docs", "update CLI documentation", "generate CLI JSON", or "refresh commands.json". +metadata: + author: torrust + version: "1.0" +--- + +# Regenerate CLI Documentation + +This skill helps you regenerate the machine-readable CLI documentation JSON after making changes to the CLI interface. + +## When to Use This Skill + +Use this skill when: + +- **After adding new commands** - Document new functionality in the CLI +- **After removing commands** - Remove outdated command documentation +- **After modifying command arguments** - Update parameter documentation +- **After changing help text** - Ensure descriptions are current +- **Before releasing a new version** - Create documentation snapshot +- **When CLI tests pass** - Ensure documentation matches implementation + +## What Gets Regenerated + +This skill regenerates: + +- **`docs/cli/commands.json`** - Complete CLI structure with all commands, subcommands, arguments, and options +- **Format**: `cli-documentation` v1.0 +- **Source**: Automatically extracted from Clap CLI definitions + +## Prerequisites + +1. **CLI code compiles** - Run `cargo check` first +2. **All tests pass** - Run `cargo test` to verify CLI functionality +3. **Working CLI implementation** - The `docs` command must be functional + +## Usage Instructions + +### Step 1: Verify CLI Compiles + +Before regenerating documentation, ensure the CLI code is valid: + +```bash +cargo check +``` + +### Step 2: Run Tests (Optional but Recommended) + +Verify CLI functionality: + +```bash +cargo test +``` + +### Step 3: Regenerate Documentation + +Generate the CLI documentation JSON: + +```bash +cargo run -- docs docs/cli/commands.json +``` + +**Expected Output**: + +```text +⏳ [1/1] Generating CLI JSON documentation... +⏳ ✓ CLI documentation written to file successfully (took 0ms) +✅ CLI documentation generation completed successfully +``` + +### Step 4: Verify Generation + +Check the file was created/updated: + +```bash +ls -lh docs/cli/commands.json +``` + +Verify JSON structure: + +```bash +jq -r '.format, .format_version, .cli.version' docs/cli/commands.json +``` + +Count documented commands: + +```bash +jq '.cli.commands | length' docs/cli/commands.json +``` + +### Step 5: Review Changes + +If regenerating after changes, review what changed: + +```bash +git diff docs/cli/commands.json +``` + +### Step 6: Commit Changes + +Stage and commit the updated documentation: + +```bash +git add docs/cli/commands.json +git commit -m "docs: update CLI documentation for new commands" +``` + +## Common Workflows + +### Workflow 1: After Adding a New Command + +```bash +# 1. Implement new command in src/presentation/input/cli/commands.rs +# 2. Add controller and routing +# 3. Verify compilation +cargo check + +# 4. Run tests +cargo test + +# 5. Regenerate documentation +cargo run -- docs docs/cli/commands.json + +# 6. Review changes +git diff docs/cli/commands.json + +# 7. Commit +git add docs/cli/commands.json +git commit -m "docs: update CLI docs for new validate command" +``` + +### Workflow 2: Before Version Release + +```bash +# 1. Ensure all CLI changes are complete and tested +cargo test + +# 2. Regenerate documentation for release +cargo run -- docs docs/cli/commands.json + +# 3. Verify version in JSON matches release +jq '.cli.version' docs/cli/commands.json + +# 4. Commit as part of release preparation +git add docs/cli/commands.json +git commit -m "docs: update CLI documentation for v0.2.0 release" +``` + +### Workflow 3: CI/CD Integration + +```bash +# Generate and verify documentation hasn't changed unexpectedly +cargo run -- docs > generated.json +diff docs/cli/commands.json generated.json || { + echo "❌ CLI documentation is out of sync!" + echo "Run: cargo run -- docs docs/cli/commands.json" + exit 1 +} +``` + +## What the Documentation Includes + +The generated JSON includes: + +- **Command names** - All top-level commands +- **Subcommands** - Nested command structures (e.g., `create template`, `create environment`) +- **Arguments** - Positional parameters with descriptions +- **Options** - Flags with short/long forms, value types, and help text +- **Descriptions** - Help text for every command and option +- **Metadata** - CLI name, version, and about text + +## Output Format + +The JSON follows this structure: + +```json +{ + "format": "cli-documentation", + "format_version": "1.0", + "cli": { + "name": "torrust-tracker-deployer", + "version": "0.1.0", + "about": "Deploy and manage Torrust Tracker instances", + "commands": [ + { + "name": "create", + "description": "Create environments and resources", + "subcommands": [...], + "args": [...] + }, + { + "name": "docs", + "description": "Generate CLI documentation in JSON format", + "args": [...] + } + ] + } +} +``` + +## Troubleshooting + +### Problem: Command fails with compilation error + +**Solution**: Fix compilation errors first: + +```bash +cargo check +# Fix any errors +cargo run -- docs docs/cli/commands.json +``` + +### Problem: JSON format is invalid + +**Solution**: Verify with jq: + +```bash +jq '.' docs/cli/commands.json +``` + +If invalid, check infrastructure layer implementation. + +### Problem: Documentation missing new commands + +**Solution**: Ensure commands are properly registered in Clap: + +1. Check `src/presentation/input/cli/commands.rs` +2. Verify enum variants are public +3. Rebuild and regenerate + +### Problem: Permission denied writing file + +**Solution**: Check directory permissions: + +```bash +ls -ld docs/cli/ +mkdir -p docs/cli +cargo run -- docs docs/cli/commands.json +``` + +## Related Commands + +- **`docs`** - The CLI command that generates documentation +- **`create schema`** - Generate JSON Schema for environment config (different purpose) +- **`validate`** - Validate environment configuration files + +## Related Documentation + +- [docs/user-guide/commands/docs.md](../../../docs/user-guide/commands/docs.md) - User guide for the docs command +- [docs/cli/README.md](../../../docs/cli/README.md) - CLI documentation directory overview +- [docs/experiments/cli-json-schema/](../../../docs/experiments/cli-json-schema/) - Implementation history + +## Notes + +- This command generates documentation from **compiled** code, ensuring accuracy +- The documentation is automatically synchronized with implementation +- No manual editing of `commands.json` should be needed +- The file should be committed to version control for history tracking +- Fast operation (completes in milliseconds) +- Safe to run multiple times (idempotent) + +## Success Criteria + +✅ `docs/cli/commands.json` file created/updated +✅ Valid JSON structure +✅ Format is `cli-documentation` v1.0 +✅ CLI version matches project version +✅ All commands documented (currently 14 commands) +✅ Git diff shows expected changes (if updating) + +## Best Practices + +1. **Regenerate after CLI changes** - Don't forget to update documentation +2. **Review before committing** - Use `git diff` to verify changes +3. **Version control** - Always commit the generated file +4. **CI validation** - Consider adding check in CI to ensure docs are current +5. **Release snapshots** - Commit documentation as part of version releases diff --git a/.gitignore b/.gitignore index 89145854..c595b5b1 100644 --- a/.gitignore +++ b/.gitignore @@ -68,8 +68,8 @@ data/ # Generated environment config files for E2E tests envs/ -# Experimental deployments with live secrets -experiments/ +# Experimental deployments with live secrets (root level only) +/experiments/ # Meson build directory builddir/ diff --git a/AGENTS.md b/AGENTS.md index 932ffa95..0380e942 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -216,7 +216,7 @@ The project provides Agent Skills in `.github/skills/` for specialized workflows Available skills: | Task | Skill to Load | -| ---------------------------- | --------------------------------------------------- | +| ---------------------------- | --------------------------------------------------- | --- | --------------------- | --------------------------------------------- | --- | --------------------------- | -------------------------------------------------- | | Adding commands | `.github/skills/add-new-command/skill.md` | | Cleaning up completed issues | `.github/skills/cleanup-completed-issues/skill.md` | | Cleaning LXD environments | `.github/skills/clean-lxd-environments/skill.md` | @@ -229,8 +229,7 @@ Available skills: | Creating feature specs | `.github/skills/create-feature-spec/skill.md` | | Creating issues | `.github/skills/create-issue/skill.md` | | Creating new skills | `.github/skills/add-new-skill/skill.md` | -| Creating refactor plans | `.github/skills/create-refactor-plan/skill.md` | -| Rendering tracker artifacts | `.github/skills/render-tracker-artifacts/skill.md` | +| Creating refactor plans | `.github/skills/create-refactor-plan/skill.md` | | Regenerating CLI docs | `.github/skills/regenerate-cli-docs/skill.md` | | Rendering tracker artifacts | `.github/skills/render-tracker-artifacts/skill.md` | | Reviewing pull requests | `.github/skills/review-pr/skill.md` | | Running linters | `.github/skills/run-linters/skill.md` | | Running local E2E tests | `.github/skills/run-local-e2e-test/skill.md` | diff --git a/docs/README.md b/docs/README.md index b603a5e2..324b5a1d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,6 +28,7 @@ docs/ ├── contributing/ 🤝 Contribution guidelines (branching, commits, DDD, errors, templates, testing) ├── decisions/ 📋 Architectural Decision Records (30+ ADRs with context and rationale) ├── e2e-testing/ 🧪 E2E test documentation (architecture, running, manual, troubleshooting) +├── experiments/ 🧪 Active experiments exploring potential features and improvements ├── features/ ✨ Feature specifications and development tracking (5 active features) ├── user-guide/ 📖 User documentation (commands, providers, quick start) ├── tech-stack/ 🛠️ Technology docs (Ansible, LXD, OpenTofu, SSH) @@ -231,6 +232,17 @@ Ansible testing strategy, Docker vs LXD, E2E testing, UX patterns, MVVM analysis +
+🧪 Experiments (Click to Expand) + +Active experiments exploring potential features and improvements + +**Active Experiments:** + +- [`experiments/cli-json-schema/`](experiments/cli-json-schema/) - JSON Schema generation for CLI interface (machine-readable, versionable CLI specification) + +
+
🔄 Other Documentation Categories (Click to Expand) diff --git a/docs/cli/README.md b/docs/cli/README.md new file mode 100644 index 00000000..eb850c7a --- /dev/null +++ b/docs/cli/README.md @@ -0,0 +1,64 @@ +# CLI Documentation + +This directory contains machine-readable documentation of the CLI interface. + +## Files + +- **`commands.json`** - Complete CLI structure including all commands, arguments, and options + +## Format + +The JSON file uses a custom format specifically designed for CLI documentation: + +```json +{ + "format": "cli-documentation", + "format_version": "1.0", + "cli": { + "name": "torrust-tracker-deployer", + "version": "0.1.0", + "about": "Deploy and manage Torrust Tracker instances", + "commands": [...] + } +} +``` + +## Usage + +### For AI Agents + +AI coding assistants can read this file to understand all available commands: + +```bash +jq '.cli.commands[] | {name, description}' docs/cli/commands.json +``` + +### For Documentation + +Track CLI interface changes across versions: + +```bash +git log -p docs/cli/commands.json +``` + +### For Validation + +Verify CLI structure in automated tests: + +```bash +jq '.cli.commands | length' docs/cli/commands.json +``` + +## Regenerating + +After modifying the CLI interface, regenerate this file: + +```bash +cargo run -- docs docs/cli/commands.json +``` + +The documentation is automatically generated from the actual CLI code using Clap's introspection capabilities. + +## Version History + +This file should be committed whenever the CLI interface changes, creating a version history of the command structure. diff --git a/docs/cli/commands.json b/docs/cli/commands.json new file mode 100644 index 00000000..e69de29b diff --git a/docs/roadmap.md b/docs/roadmap.md index 6a033bfd..8269b00e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -265,11 +265,15 @@ Add features and documentation that make the use of AI agents to operate the dep **Epic Issue**: [#348 - Add JSON output format support](https://github.com/torrust/torrust-tracker-deployer/issues/348) -Add machine-readable JSON output format (`--json` flag) for selected commands to improve automation and AI agent integration. Initial phase focuses on commands where structured output provides the most value. +Add machine-readable JSON output format to all commands to improve automation and AI agent integration. Once all commands support JSON output, **JSON will become the default format** (replacing `text`) because the deployer is primarily used by AI agents and automation workflows. Use `--output-format text` for human-friendly terminal output. -**Context**: JSON output enables programmatic parsing, making it easier for scripts and AI agents to extract specific information (like IP addresses, service URLs, environment names) without parsing human-readable text. +**Context**: JSON output enables programmatic parsing, making it easier for scripts and AI agents to extract specific information (like IP addresses, service URLs, environment names) without parsing human-readable text. We are prioritizing AI UX over human UX because human UX is increasingly the use of AI agents. -**Phase 1 - High-Value Commands:** +#### Decision: JSON as Future Default Format + +Once all commands have JSON output implemented (Phase 2 complete), the default output format will switch to JSON (`--output-format json`). Users who prefer human-readable output will need to explicitly pass `--output-format text`. This decision prioritizes AI agent UX since AI agents can more easily process structured JSON than natural language output. The switch cannot happen before all commands support JSON, otherwise the application would panic for commands with no JSON implementation. + +**Phase 1 - High-Value Commands (Completed):** - [x] **12.1** Add JSON output to `create` command ✅ Completed - [Issue #349](https://github.com/torrust/torrust-tracker-deployer/issues/349), [PR #351](https://github.com/torrust/torrust-tracker-deployer/pull/351) - Rationale: Contains info about where to find more detailed information (paths, configuration references) @@ -292,7 +296,46 @@ Add machine-readable JSON output format (`--json` flag) for selected commands to - Rationale: Shows full environment names without truncation, enabling unambiguous identification - Table format truncates long names - JSON provides complete information -**Future Enhancement:** JSON output can be extended to all commands (`configure`, `release`, `test`, `destroy`, `validate`, `render`, `purge`) based on user demand and use cases. +**Phase 2 - Remaining Commands:** + +- [ ] **12.6** Add JSON output to `configure` command [Issue #371](https://github.com/torrust/torrust-tracker-deployer/issues/371) + - Rationale: Contains the list of installed/configured components (Docker, security updates, firewall) and their status + - Allows automation to verify successful configuration before proceeding to release + +- [ ] **12.7** Add JSON output to `release` command + - Rationale: Contains the list of deployed artifacts and their remote paths on the VM + - Allows automation to verify all files were correctly transferred before running + +- [ ] **12.8** Add JSON output to `test` command + - Rationale: Contains test results for each verified component (cloud-init, Docker, Docker Compose) + - Structured pass/fail per component enables CI/CD pipelines to gate on specific checks + +- [ ] **12.9** Add JSON output to `destroy` command + - Rationale: Confirms which resources were destroyed and the final environment state + - Enables automation to verify cleanup before proceeding + +- [ ] **12.10** Add JSON output to `validate` command + - Rationale: Contains validation results per field with error details + - Allows automation and AI agents to surface configuration errors programmatically + +- [ ] **12.11** Add JSON output to `render` command + - Rationale: Contains the list of generated artifact paths and their destinations + - Enables automation to locate and process the generated files + +- [ ] **12.12** Add JSON output to `purge` command + - Rationale: Lists the directories and files that were removed + - Provides a machine-readable record of what was cleaned up + +- [ ] **12.13** Add JSON output to `register` command + - Rationale: Confirms the registered instance details (IP, SSH port, state transition) + - Enables automation to verify successful registration before proceeding to configure + +- [ ] **12.14** Switch default output format from `text` to `json` (after 12.6–12.13 complete) + - Prerequisite: All commands must have JSON output implemented to avoid panics + - Change `#[default]` in `OutputFormat` enum from `Text` to `Json` + - Update `default_value = "text"` to `default_value = "json"` in CLI args + - Update all doctests and documentation referencing the old default + - Rationale: Prioritize AI agent UX — JSON is easier for agents to parse than human-readable text --- diff --git a/docs/user-guide/commands/README.md b/docs/user-guide/commands/README.md index 5c33f958..c5386c1a 100644 --- a/docs/user-guide/commands/README.md +++ b/docs/user-guide/commands/README.md @@ -18,6 +18,10 @@ This directory contains detailed guides for all Torrust Tracker Deployer command - **[show](show.md)** - Display environment information with state-aware details +### CLI Documentation + +- **[docs](docs.md)** - Generate machine-readable JSON documentation of CLI interface + ### Infrastructure Management - **[provision](provision.md)** - Provision VM infrastructure diff --git a/docs/user-guide/commands/docs.md b/docs/user-guide/commands/docs.md new file mode 100644 index 00000000..d097e348 --- /dev/null +++ b/docs/user-guide/commands/docs.md @@ -0,0 +1,225 @@ +# `docs` - Generate CLI Documentation + +Generate machine-readable JSON documentation of the CLI interface structure. + +## Purpose + +Provides a complete, structured description of all available commands, arguments, and options in JSON format. This documentation is automatically generated from the actual CLI code, ensuring it always stays synchronized with the implementation. + +## Command Syntax + +```bash +torrust-tracker-deployer docs [OUTPUT_PATH] +``` + +## Arguments + +- `[OUTPUT_PATH]` (optional) - Path to write JSON documentation file. If omitted, outputs to stdout. + +## Prerequisites + +None. This command can be run without any environment setup. + +## Use Cases + +### For AI Agents + +AI coding assistants can consume this JSON to understand the complete CLI interface: + +```bash +# Generate docs for AI consumption +torrust-tracker-deployer docs > cli-interface.json +``` + +The JSON structure includes: + +- All available commands and subcommands +- Argument names, types, and descriptions +- Option flags with short/long forms +- Help text for every command + +### For Documentation Tools + +Generate versioned CLI documentation that can be tracked in version control: + +```bash +# Generate documentation for version tracking +torrust-tracker-deployer docs docs/cli/commands.json +git add docs/cli/commands.json +git commit -m "docs: update CLI documentation for v0.2.0" +``` + +### For IDE Integration + +Shell completion scripts or IDE plugins can use this to provide autocomplete: + +```bash +# Generate for IDE tooling +torrust-tracker-deployer docs > ~/.config/torrust-cli/commands.json +``` + +### For Testing + +Automated tests can validate CLI structure consistency: + +```bash +# Generate for test validation +torrust-tracker-deployer docs | jq '.cli.commands | length' +``` + +## JSON Format + +The generated JSON follows this structure: + +```json +{ + "format": "cli-documentation", + "format_version": "1.0", + "cli": { + "name": "torrust-tracker-deployer", + "version": "0.1.0", + "about": "Deploy and manage Torrust Tracker instances", + "commands": [ + { + "name": "create", + "description": "Create environments and resources", + "subcommands": [...], + "args": [...] + } + ] + } +} +``` + +Key fields: + +- **`format`**: Document type identifier (`cli-documentation`) +- **`format_version`**: Documentation format version +- **`cli.name`**: Application name +- **`cli.version`**: Application version +- **`cli.commands`**: Array of all top-level commands with their structure + +## Examples + +### Output to stdout + +```bash +torrust-tracker-deployer docs +``` + +**Output**: JSON printed to stdout (can be piped to other tools) + +### Save to file + +```bash +torrust-tracker-deployer docs docs/cli/commands.json +``` + +**Result**: Documentation written to `docs/cli/commands.json` + +### Query with jq + +```bash +# List all command names +torrust-tracker-deployer docs | jq '.cli.commands[].name' + +# Find commands with subcommands +torrust-tracker-deployer docs | jq '.cli.commands[] | select(.subcommands) | .name' + +# Get help text for a specific command +torrust-tracker-deployer docs | jq '.cli.commands[] | select(.name == "create")' +``` + +### Compare versions + +```bash +# Compare CLI changes between versions +git show v0.1.0:docs/cli/commands.json > old.json +torrust-tracker-deployer docs > new.json +diff <(jq -S . old.json) <(jq -S . new.json) +``` + +## Output Format + +### Text Output + +Not applicable - this command only generates JSON output. + +### JSON Output + +Complete CLI structure in JSON format. Use `-o json` flag for other commands if you need JSON output from operational commands. + +## Common Workflows + +### Integrating with AI Tools + +```bash +# 1. Generate CLI documentation +torrust-tracker-deployer docs > cli-docs.json + +# 2. Provide to AI agent/tool +# AI can now understand all available commands and their usage +``` + +### Tracking CLI Changes + +```bash +# 1. After modifying CLI structure +cargo build + +# 2. Regenerate documentation +torrust-tracker-deployer docs docs/cli/commands.json + +# 3. Review changes +git diff docs/cli/commands.json + +# 4. Commit with version +git add docs/cli/commands.json +git commit -m "docs: update CLI interface for new validate command" +``` + +### Validation in CI/CD + +```bash +# Verify CLI structure hasn't changed unexpectedly +torrust-tracker-deployer docs > generated.json +diff expected-cli.json generated.json || { + echo "CLI interface has changed!" + exit 1 +} +``` + +## When to Use This Command + +- **Before sharing with AI agents**: Help AI understand your CLI interface +- **After CLI changes**: Document interface modifications +- **For version releases**: Include CLI documentation snapshot +- **When building tooling**: Provide structured CLI data for autocomplete/plugins + +## Related Commands + +- **`create template`** - Generate environment configuration templates +- **`validate`** - Validate environment configuration files +- **`show`** - Display environment information + +## Notes + +- This command generates documentation from the **compiled** CLI code +- Documentation is always accurate and up-to-date with implementation +- No network access or environment setup required +- Fast operation (completes in milliseconds) +- Safe to run in any context (read-only, no side effects) + +## Comparison: `docs` vs JSON Schema + +This command is different from JSON schemas used elsewhere: + +| Aspect | `docs` command | `create schema` command | +| ------------ | ------------------------ | ---------------------------- | +| **Purpose** | Document CLI structure | Validate config files | +| **Output** | CLI command hierarchy | JSON Schema for validation | +| **Use case** | AI agents, documentation | IDE autocomplete, validation | +| **Format** | Custom JSON format | JSON Schema standard | +| **Target** | CLI interface | User configuration files | + +For validating environment configuration files, use `create schema` instead. diff --git a/project-words.txt b/project-words.txt index f42bb727..a749e453 100644 --- a/project-words.txt +++ b/project-words.txt @@ -291,6 +291,7 @@ pids pingoo pingooio pipefail +pipeable pipx pkcs pkill @@ -419,6 +420,7 @@ utmp vbqajnc venv venvs +versionable viewmodel vulns webservers diff --git a/schemas/README.md b/schemas/README.md index 6d2c7870..fec3fe10 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -1,24 +1,50 @@ # JSON Schemas -This directory contains JSON Schema files for validating configuration files used in this project. +This directory contains the JSON Schema file used for validating user environment configuration files. + +--- ## Environment Configuration Schema **File**: `environment-config.json` -This schema validates user-provided environment configuration files (stored in the `envs/` directory). It ensures that configuration files have: +**Purpose**: Validates the JSON files that users create to define deployment environments. + +### What It's For + +This schema validates user-provided environment configuration files stored in the `envs/` directory. These are the configuration files you create when you want to deploy a new Torrust Tracker instance. + +**Example file**: `envs/my-deployment.json` + +### What It Validates + +- **Environment settings**: Name, instance name +- **SSH credentials**: Key paths, username, port +- **Provider configuration**: LXD profiles or Hetzner server settings +- **Tracker configuration**: Database, UDP/HTTP trackers, API settings + +### How to Use It -- Correct structure and required fields -- Valid provider configurations (LXD, Hetzner) -- Proper SSH credentials format -- Valid tracker configuration +**In your IDE** (VS Code, IntelliJ, etc.): -## Regenerating the Schema +Configure your editor to associate `envs/*.json` files with this schema for autocomplete and validation. See the [JSON Schema IDE Setup Guide](../docs/user-guide/json-schema-ide-setup.md) for detailed instructions. -To regenerate the schema after code changes: +**Creating a new environment**: ```bash -cargo run --bin torrust-tracker-deployer -- create schema > schemas/environment-config.json +# 1. Create your configuration file in envs/ +vim envs/my-deployment.json + +# 2. Your IDE will provide autocomplete using this schema + +# 3. Deploy using your configuration +cargo run -- create environment --env-file envs/my-deployment.json +``` + +### Regenerating the Schema + +```bash +cargo run -- create schema > schemas/environment-config.json ``` **When to regenerate:** @@ -27,19 +53,14 @@ cargo run --bin torrust-tracker-deployer -- create schema > schemas/environment- - After changing validation rules or types - After modifying enums or provider options -## IDE Setup +**Important Note**: This schema does NOT apply to internal application state files (`data/*/environment.json`), which have a different structure managed by the application. -For instructions on configuring your IDE to use this schema for autocomplete and validation, see: +--- -📖 **[JSON Schema IDE Setup Guide](../docs/user-guide/json-schema-ide-setup.md)** +## Notes -## What This Schema Validates +For CLI documentation, see the `docs` command which generates machine-readable documentation of the CLI interface. -The schema applies to files matching the pattern `envs/*.json` and validates: - -- **Environment settings**: Name, instance name -- **SSH credentials**: Key paths, username, port -- **Provider configuration**: LXD profiles or Hetzner server settings -- **Tracker configuration**: Database, UDP/HTTP trackers, API settings +## Additional Resources -**Note**: This schema does NOT apply to internal application state files (`data/*/environment.json`), which have a different structure. +📖 **[JSON Schema IDE Setup Guide](../docs/user-guide/json-schema-ide-setup.md)** - Configure your IDE for environment configuration autocomplete diff --git a/src/bootstrap/container.rs b/src/bootstrap/container.rs index 426c0953..569006db 100644 --- a/src/bootstrap/container.rs +++ b/src/bootstrap/container.rs @@ -18,6 +18,7 @@ use crate::presentation::controllers::create::subcommands::environment::CreateEn use crate::presentation::controllers::create::subcommands::schema::CreateSchemaCommandController; use crate::presentation::controllers::create::subcommands::template::CreateTemplateCommandController; use crate::presentation::controllers::destroy::DestroyCommandController; +use crate::presentation::controllers::docs::DocsCommandController; use crate::presentation::controllers::list::ListCommandController; use crate::presentation::controllers::provision::ProvisionCommandController; use crate::presentation::controllers::purge::PurgeCommandController; @@ -218,6 +219,12 @@ impl Container { CreateSchemaCommandController::new(&self.user_output()) } + /// Create a new `DocsCommandController` + #[must_use] + pub fn create_docs_controller(&self) -> DocsCommandController { + DocsCommandController::new(&self.user_output()) + } + /// Create a new `ProvisionCommandController` #[must_use] pub fn create_provision_controller(&self) -> ProvisionCommandController { diff --git a/src/infrastructure/cli_docs/errors.rs b/src/infrastructure/cli_docs/errors.rs new file mode 100644 index 00000000..d05ec89d --- /dev/null +++ b/src/infrastructure/cli_docs/errors.rs @@ -0,0 +1,54 @@ +//! CLI Documentation Generation Errors +//! +//! Error types for CLI JSON documentation generation failures. + +use thiserror::Error; + +/// Errors that can occur during CLI documentation generation +#[derive(Debug, Error)] +pub enum CliDocsGenerationError { + /// Failed to serialize documentation to JSON + #[error("Failed to serialize CLI documentation to JSON")] + SerializationFailed { + /// The underlying serialization error + #[source] + source: serde_json::Error, + }, +} + +impl CliDocsGenerationError { + /// Returns actionable help text for resolving this error + /// + /// Following the project's tiered help system pattern. + #[must_use] + pub fn help(&self) -> String { + match self { + Self::SerializationFailed { .. } => { + "CLI documentation serialization failed. This is likely a bug in the documentation generator.\n\ + \n\ + What to do:\n\ + 1. Check if the CLI structure is valid\n\ + 2. Verify all metadata can be extracted from Clap\n\ + 3. Report this as a bug if the error persists\n\ + 4. Include the full error message in your bug report" + .to_string() + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_should_provide_help_text_for_serialization_error() { + let error = CliDocsGenerationError::SerializationFailed { + source: serde_json::Error::io(std::io::Error::other("test")), + }; + + let help = error.help(); + assert!(help.contains("What to do:")); + assert!(help.contains("bug")); + } +} diff --git a/src/infrastructure/cli_docs/generator.rs b/src/infrastructure/cli_docs/generator.rs new file mode 100644 index 00000000..ee0a5cd1 --- /dev/null +++ b/src/infrastructure/cli_docs/generator.rs @@ -0,0 +1,220 @@ +//! CLI Documentation Generator +//! +//! Generates JSON documentation representation of CLI structure using Clap introspection. +//! This provides a machine-readable, versionable specification of the CLI interface. + +use clap::{Command, CommandFactory}; +use serde_json::{json, Value}; + +use super::errors::CliDocsGenerationError; +use super::schema_builder; + +/// CLI documentation generator for creating JSON documentation from Clap CLI structures +/// +/// This is a stateless utility that uses Clap's introspection APIs to extract +/// comprehensive CLI metadata and convert it to a structured JSON documentation format. +/// +/// # Architecture +/// +/// - Uses `CommandFactory` trait to access CLI structure +/// - Recursively traverses commands and subcommands +/// - Delegates JSON construction to `schema_builder` module +/// +/// # Examples +/// +/// ```rust +/// use clap::Parser; +/// use torrust_tracker_deployer_lib::infrastructure::cli_docs::CliDocsGenerator; +/// +/// #[derive(Parser)] +/// struct MyCli { +/// #[arg(short, long)] +/// verbose: bool, +/// } +/// +/// let docs = CliDocsGenerator::generate::()?; +/// assert!(docs.contains("\"name\"")); +/// # Ok::<(), Box>(()) +/// ``` +pub struct CliDocsGenerator; + +impl CliDocsGenerator { + /// Generates JSON documentation for the given CLI type + /// + /// The type must implement `CommandFactory` from Clap, which is automatically + /// provided by the `#[derive(Parser)]` macro. + /// + /// # Type Parameters + /// + /// * `T` - The CLI type to generate documentation for (must implement `CommandFactory`) + /// + /// # Returns + /// + /// * `Ok(String)` - The JSON documentation as a pretty-printed JSON string + /// * `Err(CliDocsGenerationError)` - If documentation generation or serialization fails + /// + /// # Examples + /// + /// ```rust + /// use clap::Parser; + /// use torrust_tracker_deployer_lib::infrastructure::cli_docs::CliDocsGenerator; + /// + /// #[derive(Parser)] + /// #[command(name = "my-app", version = "1.0.0", about = "My application")] + /// struct MyCli { + /// #[arg(short, long)] + /// verbose: bool, + /// } + /// + /// let docs = CliDocsGenerator::generate::()?; + /// assert!(docs.contains("my-app")); + /// assert!(docs.contains("verbose")); + /// # Ok::<(), Box>(()) + /// ``` + /// + /// # Errors + /// + /// Returns `CliDocsGenerationError::SerializationFailed` if the documentation + /// cannot be serialized to JSON (this should be extremely rare). + pub fn generate() -> Result { + let command = T::command(); + let schema = Self::build_schema(&command); + + // Serialize to pretty-printed JSON + serde_json::to_string_pretty(&schema) + .map_err(|source| CliDocsGenerationError::SerializationFailed { source }) + } + + /// Builds the complete documentation structure from a Clap command + /// + /// Creates a JSON object with: + /// - `format`: Documentation format identifier ("cli-documentation") + /// - `format_version`: Format version ("1.0") + /// - `cli`: Complete CLI metadata + /// - Application info (name, version, description) + /// - Global arguments + /// - Subcommands (recursively) + /// + /// # Arguments + /// + /// * `command` - The Clap command structure to introspect + /// + /// # Returns + /// + /// A `serde_json::Value` containing the complete documentation + fn build_schema(command: &Command) -> Value { + let mut cli = schema_builder::build_app_metadata(command); + + // Extract global arguments (excluding built-in help/version) + let global_args: Vec = command + .get_arguments() + .filter(|arg| { + let id = arg.get_id().as_str(); + id != "help" && id != "version" + }) + .map(|arg| Value::Object(schema_builder::build_argument_schema(arg))) + .collect(); + + if !global_args.is_empty() { + cli.insert("global_arguments".to_string(), json!(global_args)); + } + + // Extract subcommands (excluding built-in help) + let subcommands: Vec = command + .get_subcommands() + .filter(|cmd| cmd.get_name() != "help") + .map(|cmd| Value::Object(schema_builder::build_subcommand_schema(cmd))) + .collect(); + + if !subcommands.is_empty() { + cli.insert("commands".to_string(), json!(subcommands)); + } + + // Build complete documentation with format metadata + json!({ + "format": "cli-documentation", + "format_version": "1.0", + "cli": cli + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::{Parser, Subcommand}; + + // Test CLI structure + #[derive(Parser)] + #[command(name = "test-cli", version = "1.0.0", about = "A test CLI")] + struct TestCli { + /// Verbose output + #[arg(short, long)] + verbose: bool, + + #[command(subcommand)] + command: Option, + } + + #[derive(Subcommand)] + enum TestCommands { + /// Create a resource + Create { + /// Resource name + name: String, + }, + } + + #[test] + fn it_should_generate_valid_json_schema_when_given_valid_cli() { + let result = CliDocsGenerator::generate::(); + assert!(result.is_ok()); + + let docs = result.unwrap(); + assert!(docs.contains("\"format\"")); + assert!(docs.contains("cli-documentation")); + assert!(docs.contains("test-cli")); + } + + #[test] + fn it_should_include_app_metadata_in_schema() { + let schema = CliDocsGenerator::generate::().unwrap(); + assert!(schema.contains("\"name\"")); + assert!(schema.contains("\"version\"")); + assert!(schema.contains("\"description\"")); + assert!(schema.contains("test-cli")); + assert!(schema.contains("1.0.0")); + } + + #[test] + fn it_should_include_global_arguments_in_schema() { + let schema = CliDocsGenerator::generate::().unwrap(); + assert!(schema.contains("global_arguments")); + assert!(schema.contains("verbose")); + } + + #[test] + fn it_should_include_subcommands_in_schema() { + let schema = CliDocsGenerator::generate::().unwrap(); + assert!(schema.contains("commands")); + assert!(schema.contains("Create")); + } + + #[test] + fn it_should_generate_pretty_printed_json_output() { + let schema = CliDocsGenerator::generate::().unwrap(); + // Pretty-printed JSON has newlines + assert!(schema.contains('\n')); + // And indentation + assert!(schema.contains(" ")); + } + + #[test] + fn it_should_include_json_schema_version() { + let docs = CliDocsGenerator::generate::().unwrap(); + assert!(docs.contains("\"format\"")); + assert!(docs.contains("cli-documentation")); + assert!(docs.contains("\"format_version\"")); + assert!(docs.contains("1.0")); + } +} diff --git a/src/infrastructure/cli_docs/mod.rs b/src/infrastructure/cli_docs/mod.rs new file mode 100644 index 00000000..9fa7a3a6 --- /dev/null +++ b/src/infrastructure/cli_docs/mod.rs @@ -0,0 +1,41 @@ +//! CLI JSON Documentation Generation Infrastructure +//! +//! This module provides infrastructure for generating JSON documentation from Clap CLI +//! structures using runtime introspection. It provides a machine-readable, +//! versionable specification of the CLI interface. +//! +//! ## Architecture +//! +//! This is an **Infrastructure Layer** component because: +//! - Uses external dependency (Clap) introspection APIs +//! - Pure technical mechanism with no business logic +//! - Provides serialization/export functionality +//! +//! ## Usage +//! +//! ```rust +//! use clap::Parser; +//! use torrust_tracker_deployer_lib::infrastructure::cli_docs::CliDocsGenerator; +//! +//! #[derive(Parser)] +//! struct MyCli { +//! #[arg(short, long)] +//! verbose: bool, +//! } +//! +//! let docs = CliDocsGenerator::generate::()?; +//! # Ok::<(), Box>(()) +//! ``` +//! +//! ## Module Structure +//! +//! - `generator` - Main documentation generator (`CliDocsGenerator`) +//! - `schema_builder` - JSON building utilities +//! - `errors` - Error types + +mod errors; +mod generator; +mod schema_builder; + +pub use errors::CliDocsGenerationError; +pub use generator::CliDocsGenerator; diff --git a/src/infrastructure/cli_docs/schema_builder.rs b/src/infrastructure/cli_docs/schema_builder.rs new file mode 100644 index 00000000..efe9c9c2 --- /dev/null +++ b/src/infrastructure/cli_docs/schema_builder.rs @@ -0,0 +1,296 @@ +//! CLI Documentation JSON Builder +//! +//! Utilities for building JSON documentation structures from Clap command metadata. + +use clap::{Arg, Command}; +use serde_json::{json, Map, Value}; + +/// Builds JSON object with application metadata +/// +/// Extracts name, version, and description from the Clap command. +/// +/// # Examples +/// +/// ```rust,ignore +/// use clap::CommandFactory; +/// use torrust_tracker_deployer_lib::infrastructure::cli_docs::schema_builder; +/// +/// let command = MyCli::command(); +/// let metadata = schema_builder::build_app_metadata(&command); +/// assert!(metadata.contains_key("name")); +/// ``` +#[must_use] +pub fn build_app_metadata(command: &Command) -> Map { + let mut metadata = Map::new(); + + metadata.insert("name".to_string(), json!(command.get_name())); + + if let Some(version) = command.get_version() { + metadata.insert("version".to_string(), json!(version)); + } + + if let Some(about) = command.get_about() { + metadata.insert("description".to_string(), json!(about.to_string())); + } + + if let Some(long_about) = command.get_long_about() { + metadata.insert( + "long_description".to_string(), + json!(long_about.to_string()), + ); + } + + metadata +} + +/// Builds JSON documentation for a single argument +/// +/// Extracts all metadata from a Clap argument including flags, help text, +/// required status, and value names. +/// +/// # Examples +/// +/// ```rust,ignore +/// use clap::Arg; +/// use torrust_tracker_deployer_lib::infrastructure::cli_docs::schema_builder; +/// +/// let arg = Arg::new("verbose").short('v').long("verbose"); +/// let schema = schema_builder::build_argument_schema(&arg); +/// assert!(schema.contains_key("id")); +/// ``` +#[must_use] +pub fn build_argument_schema(arg: &Arg) -> Map { + let mut schema = Map::new(); + + schema.insert("id".to_string(), json!(arg.get_id().as_str())); + + if let Some(short) = arg.get_short() { + schema.insert("short".to_string(), json!(short.to_string())); + } + + if let Some(long) = arg.get_long() { + schema.insert("long".to_string(), json!(long)); + } + + if let Some(help) = arg.get_help() { + schema.insert("help".to_string(), json!(help.to_string())); + } + + if let Some(long_help) = arg.get_long_help() { + schema.insert("long_help".to_string(), json!(long_help.to_string())); + } + + schema.insert("required".to_string(), json!(arg.is_required_set())); + + if let Some(value_names) = arg.get_value_names() { + let names: Vec = value_names + .iter() + .map(std::string::ToString::to_string) + .collect(); + schema.insert("value_names".to_string(), json!(names)); + } + + // Add action type if it's a flag (no value) vs option (takes value) + let action_type = if arg.get_num_args().is_some_and(|n| n.max_values() == 0) { + "flag" + } else { + "option" + }; + schema.insert("type".to_string(), json!(action_type)); + + schema +} + +/// Builds JSON schema for a subcommand +/// +/// Recursively extracts subcommand metadata including its own arguments +/// and any nested subcommands. +/// +/// # Examples +/// +/// ```rust,ignore +/// use clap::Command; +/// use torrust_tracker_deployer_lib::infrastructure::cli_docs::schema_builder; +/// +/// let subcommand = Command::new("create").about("Create resource"); +/// let schema = schema_builder::build_subcommand_schema(&subcommand); +/// assert!(schema.contains_key("name")); +/// ``` +#[must_use] +pub fn build_subcommand_schema(subcommand: &Command) -> Map { + let mut schema = Map::new(); + + schema.insert("name".to_string(), json!(subcommand.get_name())); + + if let Some(about) = subcommand.get_about() { + schema.insert("description".to_string(), json!(about.to_string())); + } + + if let Some(long_about) = subcommand.get_long_about() { + schema.insert( + "long_description".to_string(), + json!(long_about.to_string()), + ); + } + + // Extract arguments (excluding built-in help/version) + let arguments: Vec = subcommand + .get_arguments() + .filter(|arg| { + let id = arg.get_id().as_str(); + id != "help" && id != "version" + }) + .map(|arg| Value::Object(build_argument_schema(arg))) + .collect(); + + if !arguments.is_empty() { + schema.insert("arguments".to_string(), json!(arguments)); + } + + // Recursively extract nested subcommands + let nested_subcommands: Vec = subcommand + .get_subcommands() + .filter(|cmd| cmd.get_name() != "help") + .map(|cmd| Value::Object(build_subcommand_schema(cmd))) + .collect(); + + if !nested_subcommands.is_empty() { + schema.insert("subcommands".to_string(), json!(nested_subcommands)); + } + + schema +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::{Arg, Command}; + + #[test] + fn it_should_extract_app_metadata() { + let command = Command::new("test-app") + .version("1.0.0") + .about("A test application"); + + let metadata = build_app_metadata(&command); + + assert_eq!(metadata.get("name"), Some(&json!("test-app"))); + assert_eq!(metadata.get("version"), Some(&json!("1.0.0"))); + assert_eq!( + metadata.get("description"), + Some(&json!("A test application")) + ); + } + + #[test] + fn it_should_extract_argument_with_short_and_long_flags() { + let arg = Arg::new("verbose") + .short('v') + .long("verbose") + .help("Enable verbose output"); + + let schema = build_argument_schema(&arg); + + assert_eq!(schema.get("id"), Some(&json!("verbose"))); + assert_eq!(schema.get("short"), Some(&json!("v"))); + assert_eq!(schema.get("long"), Some(&json!("verbose"))); + assert_eq!(schema.get("help"), Some(&json!("Enable verbose output"))); + } + + #[test] + fn it_should_mark_required_arguments() { + let required_arg = Arg::new("name").required(true); + let optional_arg = Arg::new("description"); + + let required_schema = build_argument_schema(&required_arg); + let optional_schema = build_argument_schema(&optional_arg); + + assert_eq!(required_schema.get("required"), Some(&json!(true))); + assert_eq!(optional_schema.get("required"), Some(&json!(false))); + } + + #[test] + fn it_should_extract_subcommand_with_arguments() { + let subcommand = Command::new("create") + .about("Create a resource") + .arg(Arg::new("name").required(true).help("Resource name")); + + let schema = build_subcommand_schema(&subcommand); + + assert_eq!(schema.get("name"), Some(&json!("create"))); + assert_eq!(schema.get("description"), Some(&json!("Create a resource"))); + assert!(schema.contains_key("arguments")); + } + + #[test] + fn it_should_handle_nested_subcommands() { + let nested = Command::new("template").about("Generate template"); + let parent = Command::new("create") + .about("Create operations") + .subcommand(nested); + + let schema = build_subcommand_schema(&parent); + + assert!(schema.contains_key("subcommands")); + if let Some(Value::Array(subcommands)) = schema.get("subcommands") { + assert_eq!(subcommands.len(), 1); + } else { + panic!("Expected subcommands array"); + } + } + + #[test] + fn it_should_extract_long_description_from_app_metadata() { + let command = Command::new("test-app") + .about("Short description") + .long_about("Long description with multiple paragraphs.\n\nThis provides more detail."); + + let metadata = build_app_metadata(&command); + + assert_eq!( + metadata.get("description"), + Some(&json!("Short description")) + ); + assert_eq!( + metadata.get("long_description"), + Some(&json!( + "Long description with multiple paragraphs.\n\nThis provides more detail." + )) + ); + } + + #[test] + fn it_should_extract_long_help_from_arguments() { + let arg = Arg::new("config") + .help("Path to config file") + .long_help("Path to the configuration file.\n\nThis file should contain valid JSON."); + + let schema = build_argument_schema(&arg); + + assert_eq!(schema.get("help"), Some(&json!("Path to config file"))); + assert_eq!( + schema.get("long_help"), + Some(&json!( + "Path to the configuration file.\n\nThis file should contain valid JSON." + )) + ); + } + + #[test] + fn it_should_extract_long_description_from_subcommands() { + let subcommand = Command::new("deploy") + .about("Deploy application") + .long_about("Deploy the application to the specified environment.\n\nThis will provision infrastructure and configure services."); + + let schema = build_subcommand_schema(&subcommand); + + assert_eq!( + schema.get("description"), + Some(&json!("Deploy application")) + ); + assert_eq!( + schema.get("long_description"), + Some(&json!("Deploy the application to the specified environment.\n\nThis will provision infrastructure and configure services.")) + ); + } +} diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index 6020f46c..36e1117d 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -16,8 +16,10 @@ //! - `persistence` - Persistence infrastructure (repositories, file locking, storage) //! - `trace` - Trace file generation for error analysis //! - `schema` - JSON Schema generation from Rust types +//! - `cli_docs` - CLI JSON documentation generation from Clap structures //! - `dns` - DNS resolution for domain validation +pub mod cli_docs; pub mod dns; pub mod external_validators; pub mod persistence; diff --git a/src/presentation/controllers/create/errors.rs b/src/presentation/controllers/create/errors.rs index 12efca1e..c1b0f314 100644 --- a/src/presentation/controllers/create/errors.rs +++ b/src/presentation/controllers/create/errors.rs @@ -1,7 +1,7 @@ //! Unified Create Command Errors //! //! This module defines a unified error type that encompasses all create subcommand errors, -//! providing a single interface for environment, template, and schema command errors. +//! providing a single interface for environment, template, schema, and CLI schema command errors. use thiserror::Error; @@ -13,7 +13,8 @@ use super::subcommands::{ /// Unified error type for all create subcommands /// /// This error type provides a unified interface for errors that can occur during -/// any create subcommand execution (environment creation, template generation, or schema generation). +/// any create subcommand execution (environment creation, template generation, +/// or schema generation). /// It wraps the specific command errors while preserving their context and help methods. #[derive(Debug, Error)] pub enum CreateCommandError { diff --git a/src/presentation/controllers/create/router.rs b/src/presentation/controllers/create/router.rs index 760ee2b5..52087efa 100644 --- a/src/presentation/controllers/create/router.rs +++ b/src/presentation/controllers/create/router.rs @@ -12,11 +12,11 @@ use super::errors::CreateCommandError; /// Route the create command to its appropriate subcommand /// -/// This function routes between different create subcommands (environment, template, or schema). +/// This function routes between different create subcommands (environment, template, schema, or cli-schema). /// /// # Arguments /// -/// * `action` - The create action to perform (environment creation, template generation, or schema generation) +/// * `action` - The create action to perform (environment creation, template generation, schema generation, or CLI schema generation) /// * `working_dir` - Root directory for environment data storage /// * `context` - Execution context providing access to application services /// diff --git a/src/presentation/controllers/docs/errors.rs b/src/presentation/controllers/docs/errors.rs new file mode 100644 index 00000000..f09e1bb8 --- /dev/null +++ b/src/presentation/controllers/docs/errors.rs @@ -0,0 +1,198 @@ +//! Errors for Docs Command Controller (Presentation Layer) + +use std::path::PathBuf; +use thiserror::Error; + +use crate::infrastructure::cli_docs::CliDocsGenerationError; +use crate::presentation::views::progress::ProgressReporterError; + +/// Errors that can occur during CLI documentation creation in the presentation layer +#[derive(Debug, Error)] +pub enum DocsCommandError { + /// CLI documentation generation failed (infrastructure layer) + #[error("Failed to generate CLI documentation")] + SchemaGenerationFailed { + /// The underlying infrastructure error + #[source] + source: CliDocsGenerationError, + }, + + /// Failed to create parent directory for documentation file + #[error("Failed to create parent directory: {path}")] + DirectoryCreationFailed { + /// Path that couldn't be created + path: PathBuf, + /// The underlying I/O error + #[source] + source: std::io::Error, + }, + + /// Failed to write documentation file + #[error("Failed to write documentation file: {path}")] + FileWriteFailed { + /// Path that couldn't be written + path: PathBuf, + /// The underlying I/O error + #[source] + source: std::io::Error, + }, + + /// Progress reporter error + #[error("Progress reporter error")] + ProgressReporterFailed { + /// The underlying progress reporter error + #[source] + source: ProgressReporterError, + }, +} + +// Enable automatic conversion from ProgressReporterError +impl From for DocsCommandError { + fn from(source: ProgressReporterError) -> Self { + Self::ProgressReporterFailed { source } + } +} + +impl DocsCommandError { + /// Returns actionable help text for resolving this error + #[must_use] + pub fn help(&self) -> String { + match self { + Self::SchemaGenerationFailed { source } => { + format!( + "CLI documentation generation failed.\n\ + \n\ + {}\n\ + \n\ + If you need further assistance, check the documentation or report an issue.", + source.help() + ) + } + Self::DirectoryCreationFailed { path, source } => { + format!( + "Failed to create parent directory: {}\n\ + \n\ + Error: {}\n\ + \n\ + What to do:\n\ + 1. Check that you have write permissions to the parent directory\n\ + 2. Verify the path is valid for your operating system\n\ + 3. Ensure the disk is not full\n\ + 4. Try specifying a different output directory", + path.display(), + source + ) + } + Self::FileWriteFailed { path, source } => { + format!( + "Failed to write CLI documentation file: {}\n\ + \n\ + Error: {}\n\ + \n\ + What to do:\n\ + 1. Check that you have write permissions to the directory\n\ + 2. Verify the path is valid\n\ + 3. Ensure the disk is not full\n\ + 4. Try a different output path", + path.display(), + source + ) + } + Self::ProgressReporterFailed { .. } => "Progress reporting failed.\n\ + \n\ + This is an internal error with the progress display system.\n\ + \n\ + What to do:\n\ + 1. The command may have still succeeded - check the output\n\ + 2. If the problem persists, report it as a bug" + .to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_should_provide_help_text_for_schema_generation_failed() { + let error = DocsCommandError::SchemaGenerationFailed { + source: CliDocsGenerationError::SerializationFailed { + source: serde_json::Error::io(std::io::Error::other("test")), + }, + }; + + let help = error.help(); + assert!(help.contains("What to do:")); + } + + #[test] + fn it_should_provide_help_text_for_directory_creation_failed() { + let error = DocsCommandError::DirectoryCreationFailed { + path: PathBuf::from("/invalid/path"), + source: std::io::Error::other("test error"), + }; + + let help = error.help(); + assert!(help.contains("What to do:")); + assert!(help.contains("write permissions")); + assert!(help.contains("/invalid/path")); + } + + #[test] + fn it_should_provide_help_text_for_file_write_failed() { + let error = DocsCommandError::FileWriteFailed { + path: PathBuf::from("/test/schema.json"), + source: std::io::Error::other("disk full"), + }; + + let help = error.help(); + assert!(help.contains("What to do:")); + assert!(help.contains("write permissions")); + assert!(help.contains("/test/schema.json")); + } + + #[test] + fn it_should_provide_help_text_for_progress_reporter_failed() { + let error = DocsCommandError::ProgressReporterFailed { + source: ProgressReporterError::UserOutputMutexPoisoned, + }; + let help = error.help(); + assert!(help.contains("What to do:")); + } + + #[test] + fn it_should_convert_from_progress_reporter_error() { + let progress_error = ProgressReporterError::UserOutputMutexPoisoned; + + let error: DocsCommandError = progress_error.into(); + match error { + DocsCommandError::ProgressReporterFailed { .. } => {} + _ => panic!("Expected ProgressReporterFailed variant"), + } + } + + #[test] + fn it_should_implement_error_trait() { + let error = DocsCommandError::SchemaGenerationFailed { + source: CliDocsGenerationError::SerializationFailed { + source: serde_json::Error::io(std::io::Error::other("test")), + }, + }; + + // Should be able to use as std::error::Error + assert!(std::error::Error::source(&error).is_some()); + } + + #[test] + fn it_should_implement_debug_trait() { + let error = DocsCommandError::SchemaGenerationFailed { + source: CliDocsGenerationError::SerializationFailed { + source: serde_json::Error::io(std::io::Error::other("test")), + }, + }; + + let debug_str = format!("{error:?}"); + assert!(debug_str.contains("SchemaGenerationFailed")); + } +} diff --git a/src/presentation/controllers/docs/handler.rs b/src/presentation/controllers/docs/handler.rs new file mode 100644 index 00000000..89fe125a --- /dev/null +++ b/src/presentation/controllers/docs/handler.rs @@ -0,0 +1,205 @@ +//! Docs Command Controller (Presentation Layer) +//! +//! Handles the presentation layer concerns for CLI JSON documentation generation, +//! including user output and progress reporting. +//! +//! ## Architecture Note +//! +//! This controller directly uses infrastructure-layer services without going +//! through the application layer. This is architecturally correct because: +//! +//! - CLI documentation generation is a **presentation concern** (self-documentation) +//! - There is no business logic or orchestration (not a use case) +//! - Application layer would be unnecessary indirection +//! +//! Compare with `create schema` which generates business DTOs - that correctly +//! goes through application layer because it documents business configuration. + +use std::cell::RefCell; +use std::path::PathBuf; +use std::sync::Arc; + +use parking_lot::ReentrantMutex; + +use crate::infrastructure::cli_docs::CliDocsGenerator; +use crate::presentation::input::cli::Cli; +use crate::presentation::views::progress::ProgressReporter; +use crate::presentation::views::UserOutput; + +use super::errors::DocsCommandError; + +/// Steps for CLI documentation generation workflow +enum DocsStep { + GenerateDocs, +} + +impl DocsStep { + fn description(&self) -> &str { + match self { + Self::GenerateDocs => "Generating CLI JSON documentation", + } + } + + fn count() -> usize { + 1 + } +} + +/// Controller for docs command +/// +/// Handles the presentation layer for CLI JSON documentation generation, +/// coordinating between the command handler and user output. +pub struct DocsCommandController { + progress: ProgressReporter, +} + +impl DocsCommandController { + /// Create a new CLI documentation generation command controller + pub fn new(user_output: &Arc>>) -> Self { + let progress = ProgressReporter::new(user_output.clone(), DocsStep::count()); + + Self { progress } + } + + /// Execute the CLI documentation generation command + /// + /// Generates CLI JSON documentation and either writes to file or outputs to stdout. + /// + /// # Arguments + /// + /// * `output_path` - Optional path to write schema file. If `None`, outputs to stdout. + /// + /// # Returns + /// + /// Returns `Ok(())` on success, or error if generation or output fails. + /// + /// # Errors + /// + /// Returns error if: + /// - CLI documentation generation fails + /// - File write fails (when path provided) + /// - Parent directory creation fails (when path provided) + /// - Stdout write fails (when no path provided) + pub fn execute(&mut self, output_path: Option<&PathBuf>) -> Result<(), DocsCommandError> { + // Generate CLI documentation using infrastructure layer directly + let docs = CliDocsGenerator::generate::() + .map_err(|source| DocsCommandError::SchemaGenerationFailed { source })?; + + // Handle output based on destination + if let Some(path) = output_path { + // When writing to file, show progress to user + self.progress + .start_step(DocsStep::GenerateDocs.description())?; + + // Create parent directories if needed + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|source| { + DocsCommandError::DirectoryCreationFailed { + path: parent.to_path_buf(), + source, + } + })?; + } + + // Write documentation to file + std::fs::write(path, &docs).map_err(|source| DocsCommandError::FileWriteFailed { + path: path.clone(), + source, + })?; + + self.progress + .complete_step(Some("CLI documentation written to file successfully"))?; + self.progress + .complete("CLI documentation generation completed successfully")?; + } else { + // When writing to stdout, only output the documentation (no progress messages) + // This enables clean piping: `cmd docs > file.json` + self.progress.result(&docs)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::presentation::views::testing::test_user_output::TestUserOutput; + use crate::presentation::views::VerbosityLevel; + use tempfile::TempDir; + + #[test] + fn it_should_generate_cli_schema_to_file_when_path_provided() { + let temp_dir = TempDir::new().unwrap(); + let schema_path = temp_dir.path().join("docs.json"); + + let (user_output, _capture, _capture_stderr) = + TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped(); + let mut controller = DocsCommandController::new(&user_output); + + let result = controller.execute(Some(&schema_path)); + assert!(result.is_ok()); + + // Verify file was created + assert!(schema_path.exists()); + + // Verify file contains valid CLI documentation + let content = std::fs::read_to_string(&schema_path).unwrap(); + assert!(content.contains("\"format\"")); + assert!(content.contains("\"format_version\"")); + assert!(content.contains("\"cli\"")); + } + + #[test] + fn it_should_complete_progress_when_generating_cli_schema() { + let (user_output, _capture, _capture_stderr) = + TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped(); + let mut controller = DocsCommandController::new(&user_output); + + let temp_dir = TempDir::new().unwrap(); + let schema_path = temp_dir.path().join("docs.json"); + + let result = controller.execute(Some(&schema_path)); + assert!(result.is_ok()); + } + + #[test] + fn it_should_output_to_stdout_when_no_path_provided() { + let (user_output, capture, _capture_stderr) = + TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped(); + let mut controller = DocsCommandController::new(&user_output); + + let result = controller.execute(None); + assert!(result.is_ok()); + + // Verify documentation was written to stdout + let output = String::from_utf8(capture.lock().clone()).unwrap(); + assert!(output.contains("\"format\"")); + assert!(output.contains("\"format_version\"")); + assert!(output.contains("\"cli\"")); + } + + #[test] + fn it_should_generate_valid_cli_schema_structure() { + let (user_output, capture, _capture_stderr) = + TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped(); + let mut controller = DocsCommandController::new(&user_output); + + let result = controller.execute(None); + assert!(result.is_ok()); + + let output = String::from_utf8(capture.lock().clone()).unwrap(); + let json: serde_json::Value = serde_json::from_str(&output).unwrap(); + + // Verify CLI documentation structure + assert!(json.get("format").is_some()); + assert_eq!(json.get("format").unwrap(), "cli-documentation"); + assert!(json.get("format_version").is_some()); + assert!(json.get("cli").is_some()); + + let cli = json.get("cli").unwrap(); + assert!(cli.get("name").is_some()); + assert!(cli.get("version").is_some()); + assert!(cli.get("commands").is_some()); + } +} diff --git a/src/presentation/controllers/docs/mod.rs b/src/presentation/controllers/docs/mod.rs new file mode 100644 index 00000000..45e8c73a --- /dev/null +++ b/src/presentation/controllers/docs/mod.rs @@ -0,0 +1,38 @@ +//! Docs Command Controller (Presentation Layer) +//! +//! This module handles the presentation layer concerns for CLI JSON documentation +//! generation, including user output and progress reporting. +//! +//! # Architecture +//! +//! - **Controller**: Coordinates between application handler and user output +//! - **Errors**: Presentation layer error wrapping +//! - **Progress Reporting**: Provides user feedback during generation +//! +//! # Usage +//! +//! ```rust,no_run +//! use std::sync::Arc; +//! use std::path::PathBuf; +//! use torrust_tracker_deployer_lib::presentation::controllers::docs::DocsCommandController; +//! use torrust_tracker_deployer_lib::presentation::views::{UserOutput, VerbosityLevel}; +//! use std::cell::RefCell; +//! use parking_lot::ReentrantMutex; +//! +//! // Setup user output +//! let user_output = Arc::new(ReentrantMutex::new(RefCell::new( +//! UserOutput::new(VerbosityLevel::Normal) +//! ))); +//! let mut controller = DocsCommandController::new(&user_output); +//! +//! // Generate to file +//! let output_path = PathBuf::from("docs/cli/commands.json"); +//! controller.execute(Some(&output_path))?; +//! # Ok::<(), Box>(()) +//! ``` + +mod errors; +mod handler; + +pub use errors::DocsCommandError; +pub use handler::DocsCommandController; diff --git a/src/presentation/controllers/mod.rs b/src/presentation/controllers/mod.rs index 2fa4f8a4..f2dba705 100644 --- a/src/presentation/controllers/mod.rs +++ b/src/presentation/controllers/mod.rs @@ -254,6 +254,7 @@ pub mod configure; pub mod constants; pub mod create; pub mod destroy; +pub mod docs; pub mod list; pub mod provision; pub mod purge; diff --git a/src/presentation/dispatch/router.rs b/src/presentation/dispatch/router.rs index a0d587b4..e81acff8 100644 --- a/src/presentation/dispatch/router.rs +++ b/src/presentation/dispatch/router.rs @@ -226,5 +226,12 @@ pub async fn route_command( .execute(output_format)?; Ok(()) } + Commands::Docs { output_path } => { + context + .container() + .create_docs_controller() + .execute(output_path.as_ref())?; + Ok(()) + } } } diff --git a/src/presentation/errors.rs b/src/presentation/errors.rs index 09707224..4c7a04cd 100644 --- a/src/presentation/errors.rs +++ b/src/presentation/errors.rs @@ -21,7 +21,7 @@ use thiserror::Error; use crate::presentation::controllers::{ configure::ConfigureSubcommandError, create::CreateCommandError, - destroy::DestroySubcommandError, list::ListSubcommandError, + destroy::DestroySubcommandError, docs::DocsCommandError, list::ListSubcommandError, provision::ProvisionSubcommandError, purge::PurgeSubcommandError, register::errors::RegisterSubcommandError, release::ReleaseSubcommandError, render::errors::RenderCommandError, run::RunSubcommandError, show::ShowSubcommandError, @@ -49,6 +49,13 @@ pub enum CommandError { #[error("Destroy command failed: {0}")] Destroy(Box), + /// Docs command specific errors + /// + /// Encapsulates all errors that can occur during CLI documentation generation. + /// Use `.help()` for detailed troubleshooting steps. + #[error("Docs command failed: {0}")] + Docs(Box), + /// Provision command specific errors /// /// Encapsulates all errors that can occur during infrastructure provisioning. @@ -146,6 +153,12 @@ impl From for CommandError { } } +impl From for CommandError { + fn from(error: DocsCommandError) -> Self { + Self::Docs(Box::new(error)) + } +} + impl From for CommandError { fn from(error: ProvisionSubcommandError) -> Self { Self::Provision(Box::new(error)) @@ -250,6 +263,7 @@ impl CommandError { match self { Self::Create(e) => e.help(), Self::Destroy(e) => e.help().to_string(), + Self::Docs(e) => e.help(), Self::Provision(e) => e.help().to_string(), Self::Configure(e) => e.help().to_string(), Self::Register(e) => e.help().to_string(), diff --git a/src/presentation/input/cli/commands.rs b/src/presentation/input/cli/commands.rs index c24971e6..9dc2332a 100644 --- a/src/presentation/input/cli/commands.rs +++ b/src/presentation/input/cli/commands.rs @@ -29,6 +29,31 @@ pub enum Commands { /// This command will tear down all infrastructure associated with the /// specified environment, including virtual machines, networks, and /// persistent data. This operation is irreversible. + /// + /// WHAT GETS DESTROYED: + /// • VM instance (deleted permanently) + /// • Virtual networks (removed) + /// • Remote data (tracker database, logs, configs on VM) + /// • Running services (stopped and removed) + /// + /// WHAT GETS PRESERVED: + /// • Local data directory: data/{env-name}/ (use 'purge' to remove) + /// • Build artifacts: build/{env-name}/ (use 'purge' to remove) + /// • Environment state file (allows reusing the name after purge) + /// + /// SAFETY WARNINGS: + /// • Operation is IRREVERSIBLE - infrastructure cannot be recovered + /// • Remote data (tracker database) will be permanently lost + /// • Always backup important data before destroying + /// + /// NEXT STEPS: + /// After destroying, you can: + /// • Purge local data to reuse environment name: purge {env-name} + /// • Keep local data for reference or audit trail + /// + /// EXECUTION TIME: + /// Typical duration: 1-3 minutes + /// Factors: provider API response, resource cleanup timing Destroy { /// Name of the environment to destroy /// @@ -40,30 +65,39 @@ pub enum Commands { /// Purge local data for an environment /// /// This command removes all local data directories for an environment, - /// including the `data/{env-name}/` and `build/{env-name}/` directories. - /// - /// ### When to Use - /// - /// - After destroying an environment to reuse the environment name - /// - To free up disk space from environments that are no longer needed - /// - To clean up state when infrastructure was destroyed independently - /// - /// ### Important Notes - /// - /// - Always prompts for confirmation unless `--force` is provided - /// - Works on any environment state, but most commonly used after `destroy` - /// - For running environments, only removes local data (does NOT destroy infrastructure) - /// - This operation is irreversible - all local environment data will be permanently deleted - /// - /// ### Examples - /// - /// ```text - /// # After destroying an environment - /// torrust-tracker-deployer purge my-env - /// - /// # Skip confirmation (for automation/scripts) - /// torrust-tracker-deployer purge my-env --force - /// ``` + /// including the data/{env-name}/ and build/{env-name}/ directories. + /// + /// WORKFLOW POSITION (Step 9 - After destroy, optional): + /// ... → destroy → \[PURGE\] (optional cleanup to reuse environment name) + /// + /// COMPARISON WITH DESTROY: + /// • destroy: Removes REMOTE infrastructure (VMs, cloud resources) + /// • purge: Removes LOCAL data (state files, build artifacts) + /// Typical sequence: destroy (step 8) → purge (step 9, optional) + /// + /// WHEN TO USE: + /// • After destroying an environment to reuse the environment name + /// • To free up disk space from environments no longer needed + /// • To clean up state when infrastructure was destroyed independently + /// + /// WHAT GETS REMOVED: + /// • data/{env-name}/environment.json (state file) + /// • data/{env-name}/logs/ (execution logs) + /// • build/{env-name}/ (rendered templates, Terraform state) + /// After purge, the environment name becomes available for reuse + /// + /// SAFETY WARNINGS: + /// • Always prompts for confirmation unless --force is provided + /// • Operation is IRREVERSIBLE - local data permanently deleted + /// • For running environments: only removes LOCAL data, does NOT destroy infrastructure + /// • Best practice: only purge after destroy completes successfully + /// + /// EXAMPLES: + /// After destroying an environment: + /// torrust-tracker-deployer purge my-env + /// + /// Skip confirmation (for automation/scripts): + /// torrust-tracker-deployer purge my-env --force Purge { /// Name of the environment to purge /// @@ -85,12 +119,40 @@ pub enum Commands { /// This command provisions the virtual machine infrastructure for a deployment /// environment that was previously created. It will: /// - Render and apply `OpenTofu` templates - /// - Create LXD VM instances + /// - Create VM instances /// - Configure networking /// - Wait for SSH connectivity /// - Wait for cloud-init completion /// /// The environment must be in "Created" state (use 'create environment' first). + /// + /// STATE TRANSITION: + /// • Prerequisites: Environment must be in Created state + /// - Use 'create environment' first + /// - Check state: show {env-name} + /// • After Success: Environment transitions to Provisioned state + /// • Infrastructure Created: VM instance, networking, SSH access + /// • On Failure: Remains in Created state + /// + /// WORKFLOW POSITION (Step 2 of 8): + /// create environment → \[PROVISION\] → configure → release → run + /// ↓ + /// (alternative: register) + /// + /// NEXT STEPS: + /// After provisioning: + /// 1. Verify infrastructure (optional): test {env-name} + /// 2. Configure the instance: configure {env-name} + /// + /// ALTERNATIVE: Register Existing Infrastructure + /// If you already have a server/VM, use 'register' instead: + /// register {env-name} --instance-ip \ + /// This skips infrastructure provisioning. + /// + /// COMMON ERRORS: + /// • "Environment not in Created state": Run 'create environment' first + /// • "Provider credentials missing": Check environment config file + /// • "SSH connection failed": Verify network connectivity Provision { /// Name of the environment to provision /// @@ -108,6 +170,25 @@ pub enum Commands { /// - Configure system services /// /// The environment must be in "Provisioned" state (use 'provision' command first). + /// + /// STATE TRANSITION: + /// • Prerequisites: Environment must be in Provisioned state + /// - Use 'provision' or 'register' first + /// • After Success: Environment transitions to Configured state + /// • Configuration Applied: Docker, Docker Compose, system packages, firewall + /// • On Failure: Remains in Provisioned state + /// + /// WORKFLOW POSITION (Step 3 of 8): + /// provision/register → \[CONFIGURE\] → release → run + /// + /// WHAT THIS COMMAND DOES NOT DO: + /// • Does not deploy application files (use 'release') + /// • Does not start services (use 'run') + /// • Does not provision infrastructure (use 'provision'/'register' first) + /// + /// EXECUTION TIME: + /// Typical duration: 2-5 minutes + /// Factors: network speed, package downloads, instance specifications Configure { /// Name of the environment to configure /// @@ -125,6 +206,27 @@ pub enum Commands { /// - Verify Docker Compose installation /// /// The environment must have an instance IP set (use 'provision' command first). + /// + /// WHAT GETS VALIDATED: + /// • Cloud-init completion (system initialization finished) + /// • Docker engine installed and running + /// • Docker Compose installed and accessible + /// • SSH connectivity to instance + /// + /// WHEN TO RUN (Recommended Checkpoints): + /// • After 'provision' - verify infrastructure is ready + /// • After 'register' - verify existing instance meets requirements + /// • Before 'configure' - confirm base system is operational + /// • Troubleshooting - diagnose infrastructure issues + /// + /// EXIT CODES: + /// • 0: All checks passed - infrastructure ready + /// • Non-zero: One or more checks failed - see output for details + /// + /// WHAT THIS DOES NOT VALIDATE: + /// • Application deployment (use after 'release' to check that) + /// • Running services (use 'show' after 'run' to check that) + /// • Configuration file syntax (use 'validate' for that) Test { /// Name of the environment to test /// @@ -151,12 +253,33 @@ pub enum Commands { /// - CI/CD pipelines checking configurations /// - Troubleshooting configuration issues /// - /// # Examples - /// - /// ```text - /// torrust-tracker-deployer validate --env-file envs/my-config.json - /// torrust-tracker-deployer validate -f production.json - /// ``` + /// PRE-DEPLOYMENT USAGE: + /// Always validate BEFORE 'create environment' to catch errors early + /// Saves time by detecting issues before resource provisioning + /// + /// WHAT GETS VALIDATED: + /// • JSON syntax and schema compliance + /// • Environment name (format, length, characters) + /// • Provider configuration (type, credentials structure) + /// • SSH credentials (key file paths exist, format valid) + /// • Network settings (port ranges, IP formats) + /// • Tracker configuration (modes, database type) + /// + /// BENEFITS OF EARLY VALIDATION: + /// • Fast feedback (no network calls or resource creation) + /// • Clear error messages with specific field issues + /// • Prevents partial deployments from invalid configs + /// • Saves time and costs (catch before provisioning) + /// + /// EXAMPLE WORKFLOW INTEGRATION: + /// 1. Create template: create template --provider \ + /// 2. Edit configuration: vim environment-template.json + /// 3. Validate config: validate --env-file environment-template.json + /// 4. Create environment: create environment --env-file environment-template.json + /// + /// EXAMPLES: + /// torrust-tracker-deployer validate --env-file envs/my-config.json + /// torrust-tracker-deployer validate -f production.json Validate { /// Path to the environment configuration file /// @@ -177,11 +300,34 @@ pub enum Commands { /// After registration, the environment transitions to "Provisioned" state /// and can continue with 'configure', 'release', and 'run' commands. /// - /// Instance Requirements: - /// - Ubuntu 24.04 LTS - /// - SSH connectivity with credentials from 'create environment' - /// - Public SSH key installed for access - /// - Username with sudo access + /// STATE TRANSITION: + /// • Prerequisites: Environment must be in Created state + /// • After Success: Environment transitions to Provisioned state + /// • Infrastructure: Existing instance registered (not created) + /// • On Failure: Remains in Created state + /// + /// WORKFLOW POSITION (Alternative to Step 2): + /// create environment → \[REGISTER\] → configure → release → run + /// ↓ + /// (alternative: provision) + /// + /// WHEN TO USE REGISTER VS PROVISION: + /// Use REGISTER when: + /// • You have an existing server/VM + /// • You want to use bare-metal hardware + /// • You already provisioned infrastructure externally + /// • You're deploying to a Docker container (for testing) + /// + /// Use PROVISION when: + /// • You want automated infrastructure creation + /// • You're using supported cloud providers + /// • You want reproducible infrastructure + /// + /// INSTANCE REQUIREMENTS: + /// • Ubuntu 24.04 LTS + /// • SSH connectivity with credentials from 'create environment' + /// • Public SSH key installed for access + /// • Username with sudo access Register { /// Name of the environment to register the instance with /// @@ -217,12 +363,27 @@ pub enum Commands { /// - Environment transitions to "Released" state /// - You can then run `run ` to start the services /// - /// # Examples - /// - /// ```text - /// torrust-tracker-deployer release my-env - /// torrust-tracker-deployer release production - /// ``` + /// STATE TRANSITION: + /// • Prerequisites: Environment must be in Configured state + /// • After Success: Environment transitions to Released state + /// • Files Deployed: + /// - docker-compose.yml to /opt/torrust/ + /// - Tracker configuration to /opt/torrust/storage/tracker/etc/ + /// - Environment variables to /opt/torrust/.env + /// - Monitoring configs (if enabled) + /// • On Failure: Remains in Configured state + /// + /// WORKFLOW POSITION (Step 4 of 8): + /// configure → \[RELEASE\] → run + /// + /// WHAT THIS DOES NOT DO: + /// • Does not start containers (use 'run') + /// • Does not install Docker (done in 'configure') + /// • Does not provision infrastructure (done in 'provision') + /// + /// EXAMPLES: + /// torrust-tracker-deployer release my-env + /// torrust-tracker-deployer release production Release { /// Name of the environment to release to /// @@ -237,27 +398,44 @@ pub enum Commands { /// tracker configuration, Ansible playbooks, etc.) to the build directory /// without executing any deployment operations. /// - /// **Important**: This command is ONLY for environments in the "Created" state. - /// If the environment is already provisioned, artifacts already exist in the - /// build directory and will not be regenerated. + /// NOT PART OF NORMAL DEPLOYMENT WORKFLOW: + /// This is a preview/debugging command. Normal workflow automatically + /// generates artifacts during provision/configure/release commands. + /// + /// COMPARISON WITH NORMAL WORKFLOW: + /// Normal: create → provision (generates artifacts + creates infrastructure) + /// Render: create → render (generates artifacts only, no infrastructure) + /// Use render when you want to inspect what will be deployed before + /// committing to infrastructure provisioning. + /// + /// TWO MODES: + /// 1. From existing environment (--env-name): + /// Read-only preview of what would be deployed + /// Works at any state, does not modify environment /// - /// Use cases: - /// - Preview artifacts before provisioning infrastructure - /// - Generate artifacts from config file without creating environment - /// - Inspect what will be deployed before committing to provision + /// 2. From configuration file (--env-file): + /// Generate artifacts without creating environment + /// Useful for validating configurations or generating examples /// - /// # Examples + /// USE CASES: + /// • Preview artifacts before provisioning infrastructure + /// • Inspect what will be deployed before committing to provision + /// • Generate artifacts for documentation or examples + /// • Test configuration changes without affecting environment /// - /// ```text - /// # Generate from existing environment - /// torrust-tracker-deployer render --env-name my-env --instance-ip 10.0.0.1 --output-dir ./preview + /// OUTPUT DIRECTORY: + /// Must be different from standard build/{env}/ directory to prevent + /// conflicts with real deployments. Use custom path like ./preview/ /// - /// # Generate from config file (no environment creation) - /// torrust-tracker-deployer render --env-file envs/my-config.json --instance-ip 10.0.0.1 --output-dir /tmp/artifacts + /// EXAMPLES: + /// Generate from existing environment: + /// torrust-tracker-deployer render --env-name my-env --instance-ip 10.0.0.1 --output-dir ./preview /// - /// # Overwrite existing output directory - /// torrust-tracker-deployer render --env-name my-env --instance-ip 10.0.0.1 --output-dir ./preview --force - /// ``` + /// Generate from config file (no environment creation): + /// torrust-tracker-deployer render --env-file envs/my-config.json --instance-ip 10.0.0.1 --output-dir /tmp/artifacts + /// + /// Overwrite existing output directory: + /// torrust-tracker-deployer render --env-name my-env --instance-ip 10.0.0.1 --output-dir ./preview --force Render { /// Name of existing environment (mutually exclusive with --env-file) /// @@ -312,12 +490,33 @@ pub enum Commands { /// - Environment transitions to "Running" state /// - Services are accessible on the VM /// - /// # Examples - /// - /// ```text - /// torrust-tracker-deployer run my-env - /// torrust-tracker-deployer run production - /// ``` + /// STATE TRANSITION: + /// • Prerequisites: Environment must be in Released state + /// • After Success: Environment transitions to Running state + /// • Services Started: Tracker (UDP/HTTP), Database, Prometheus, Grafana + /// • On Failure: Remains in Released state + /// + /// WORKFLOW POSITION (Step 5 of 8 - Final deployment step): + /// release → \[RUN\] → (services running) + /// + /// SERVICE ACCESS: + /// Once running, services are accessible at: + /// • Tracker UDP: (if enabled) + /// • Tracker HTTP: (if enabled) + /// • Tracker API: (if enabled) + /// • Grafana: (if enabled) + /// • Prometheus: (if enabled) + /// + /// VERIFYING SERVICES: + /// Check services after running: + /// - View environment status: show {env-name} + /// - SSH to check containers: ssh -i ~/.ssh/key user@instance-ip + /// - Check container status: docker compose ps + /// - View logs: docker compose logs tracker + /// + /// EXAMPLES: + /// torrust-tracker-deployer run my-env + /// torrust-tracker-deployer run production Run { /// Name of the environment to run /// @@ -338,12 +537,30 @@ pub enum Commands { /// - Service URLs when running /// - Next-step guidance based on current state /// - /// # Examples - /// - /// ```text - /// torrust-tracker-deployer show my-env - /// torrust-tracker-deployer show production - /// ``` + /// STATE-DEPENDENT INFORMATION: + /// Created state: Shows environment name, provider, config + /// Provisioned state: Adds instance IP, SSH details + /// Configured state: Adds system configuration status + /// Released state: Adds deployment file locations + /// Running state: Adds service URLs and access information + /// + /// COMMON USAGE SCENARIOS: + /// • Check current state before running next command + /// • Get instance IP for SSH access + /// • Get service URLs after deployment + /// • Verify environment exists before operations + /// • Quick status check without network calls + /// + /// OUTPUT FORMAT OPTIONS: + /// Use --output-format json for machine-readable output + /// Default: Human-readable text with tables + /// + /// PERFORMANCE NOTE: + /// Fast operation - reads local state file only (no network calls) + /// + /// EXAMPLES: + /// torrust-tracker-deployer show my-env + /// torrust-tracker-deployer show production Show { /// Name of the environment to show /// @@ -357,18 +574,83 @@ pub enum Commands { /// names, states, and providers. It scans the local data directory and /// does not make any network calls. /// - /// The output includes: - /// - Environment name - /// - Current state (Created, Provisioned, Configured, Released, Running, Destroyed) - /// - Provider (LXD, Hetzner Cloud) - /// - Creation timestamp + /// NOT PART OF DEPLOYMENT WORKFLOW: + /// This is an informational command that can be run at any time to + /// see what environments exist and their current states. + /// + /// OUTPUT INFORMATION: + /// For each environment, displays: + /// • Environment name + /// • Current state (Created, Provisioned, Configured, Released, Running, Destroyed) + /// • Provider type (generic, e.g., LXD, Hetzner) + /// • Creation timestamp /// - /// # Examples + /// WHEN TO USE: + /// • Check which environments exist before creating a new one + /// • Verify environment states before running commands + /// • Quick audit of all deployment environments + /// • See what can be purged to free up space /// - /// ```text - /// torrust-tracker-deployer list - /// ``` + /// PERFORMANCE: + /// Fast operation - only reads local JSON files, no network calls + /// + /// EXAMPLE: + /// torrust-tracker-deployer list List, + + /// Generate CLI documentation in JSON format + /// + /// This command generates machine-readable documentation for all CLI + /// commands, arguments, and their descriptions. The output is a structured + /// JSON document suitable for AI agents, documentation generators, and + /// IDE integrations. + /// + /// NOT PART OF DEPLOYMENT WORKFLOW: + /// This is a meta-command for generating documentation. It's used by + /// maintainers and AI agents, not part of normal deployment operations. + /// + /// OUTPUT FORMAT OPTIONS: + /// • No path: Outputs JSON to stdout (pipeable) + /// • With path: Writes JSON to file + /// + /// WHEN TO REGENERATE: + /// After modifying command documentation in source code: + /// • Added/changed command descriptions + /// • Updated argument help text + /// • Modified command structure + /// Regeneration ensures JSON docs stay in sync with CLI + /// + /// USAGE SCENARIOS: + /// • AI agents: Read command descriptions and argument details + /// • Documentation sites: Generate command reference automatically + /// • IDE plugins: Provide autocomplete and inline help + /// • Shell completion: Generate dynamic completion scripts + /// + /// FORMAT DETAILS: + /// JSON includes for each command: + /// • Name and description + /// • All arguments with types and help text + /// • Subcommands (if applicable) + /// • Default values and constraints + /// + /// EXAMPLES: + /// Generate to stdout: + /// torrust-tracker-deployer docs + /// + /// Save to file: + /// torrust-tracker-deployer docs docs/cli/commands.json + /// + /// Pipe to other tools: + /// torrust-tracker-deployer docs | jq '.cli.commands[] | .name' + Docs { + /// Output path for CLI documentation file (optional) + /// + /// If not provided, documentation is written to stdout. + /// + /// Recommended location: `docs/cli/commands.json` + #[arg(value_name = "PATH")] + output_path: Option, + }, } /// Actions available for the create command #[derive(Debug, Subcommand)] @@ -378,6 +660,19 @@ pub enum CreateAction { /// This subcommand creates a new deployment environment based on a /// configuration file. The configuration file specifies the environment /// name, SSH credentials, and other settings required for creation. + /// + /// STATE TRANSITION: + /// • Prerequisites: None (first command in workflow) + /// • After Success: Environment transitions to Created state + /// • Creates: data/{env-name}/ and build/{env-name}/ directories + /// + /// WORKFLOW POSITION (Step 1 of 8): + /// [CREATE ENVIRONMENT] → provision/register → configure → release → run + /// + /// NEXT STEPS: + /// After creating an environment, choose one: + /// 1. Provision new infrastructure: provision {env-name} + /// 2. Register existing infrastructure: register {env-name} --instance-ip \ Environment { /// Path to the environment configuration file /// @@ -393,6 +688,29 @@ pub enum CreateAction { /// placeholder values. Edit the template to provide your actual /// configuration values, then use 'create environment' to create /// the environment. + /// + /// WORKFLOW POSITION (Step 0 - Before everything): + /// [CREATE TEMPLATE] → edit → validate → create environment → provision → ... + /// + /// AVAILABLE PROVIDERS: + /// Templates are provider-specific and include appropriate defaults: + /// • Local VM providers (e.g., LXD) - for development/testing + /// • Cloud providers (e.g., Hetzner) - for production deployments + /// Each provider template includes provider-specific configuration fields + /// + /// CUSTOMIZATION REQUIRED: + /// Generated templates have placeholder values that MUST be edited: + /// • Environment name (must be unique) + /// • SSH credentials (username, key paths) + /// • Provider settings (varies by provider) + /// • Tracker configuration (UDP/HTTP ports, database type) + /// • Optional: monitoring, backup, HTTPS settings + /// + /// NEXT STEPS: + /// 1. Generate template: create template --provider \ + /// 2. Edit template: vim environment-template.json + /// 3. Validate config: validate --env-file environment-template.json + /// 4. Create environment: create environment --env-file environment-template.json Template { /// Output path for the template file (optional) /// @@ -417,6 +735,42 @@ pub enum CreateAction { /// and validation rules for environment configuration files. The schema /// can be used by IDEs, editors, and AI assistants for autocomplete, /// validation, and inline documentation. + /// + /// NOT PART OF DEPLOYMENT WORKFLOW: + /// This is a meta-command for IDE integration. Generate once and + /// configure your editor to use it for environment JSON files. + /// + /// IDE INTEGRATION BENEFITS: + /// • Autocomplete: Suggestions for configuration fields as you type + /// • Validation: Real-time error checking for invalid values + /// • Documentation: Inline help text for each configuration option + /// • Type checking: Ensures correct data types (strings, numbers, booleans) + /// + /// SETUP INSTRUCTIONS: + /// 1. Generate schema: + /// torrust-tracker-deployer create schema schemas/environment-config.json + /// + /// 2. Configure your IDE: + /// VS Code: Add to settings.json: + /// "json.schemas": [{ + /// "fileMatch": ["envs/*.json"], + /// "url": "./schemas/environment-config.json" + /// }] + /// + /// `JetBrains` IDEs: File → Settings → Languages & Frameworks → + /// Schemas and DTDs → JSON Schema Mappings + /// + /// WHEN TO REGENERATE: + /// After modifying configuration structure in source code: + /// • Added new configuration fields + /// • Changed validation rules or constraints + /// • Updated field descriptions + /// + /// USAGE SCENARIOS: + /// • First-time setup: Enable IDE autocomplete for config files + /// • After updates: Regenerate to sync with code changes + /// • CI/CD: Validate configs against schema in automated tests + /// • AI agents: Provide schema for better config generation Schema { /// Output path for the schema file (optional) /// diff --git a/src/presentation/input/cli/mod.rs b/src/presentation/input/cli/mod.rs index 458a63fa..e2b6ae66 100644 --- a/src/presentation/input/cli/mod.rs +++ b/src/presentation/input/cli/mod.rs @@ -60,7 +60,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Destroy command") } } @@ -89,7 +90,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Destroy command") } } @@ -143,7 +145,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Destroy command") } } @@ -241,7 +244,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Create command") } } @@ -279,7 +283,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Create command") } } @@ -338,7 +343,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Create command") } } @@ -426,7 +432,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Create command") } } @@ -468,7 +475,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Create command") } } @@ -659,7 +667,8 @@ mod tests { | Commands::List | Commands::Purge { .. } | Commands::Validate { .. } - | Commands::Render { .. } => { + | Commands::Render { .. } + | Commands::Docs { .. } => { panic!("Expected Register command") } }