Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/presentation/cli/input/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,22 @@ pub struct GlobalArgs {
#[arg(long, default_value = ".", global = true)]
pub working_dir: PathBuf,

/// Output format for command results (default: text)
/// Output format for command results (default: json)
///
/// Controls the format of user-facing output (stdout channel).
/// - text: Human-readable formatted output with tables and sections (default)
/// - json: Machine-readable JSON for automation, scripts, and AI agents
/// Controls the format of result data written to stdout. Progress messages,
/// warnings, and status updates are always written to stderr regardless of
/// this setting.
/// - json: Machine-readable JSON for automation, scripts, and AI agents (default)
/// - text: Human-readable formatted output with tables and sections
Comment on lines +75 to +81
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says output format “controls the format of user-facing output (stdout channel)”, but the CLI design sends progress/success/warnings to stderr and only the final command result/JSON to stdout. To avoid confusion for users and contributors, consider rewording this to explicitly say it controls the result data written to stdout, while progress/status messages remain on stderr regardless of output format.

Copilot uses AI. Check for mistakes.
///
/// This is independent of logging format (--log-file-format, --log-stderr-format)
/// which controls stderr/file output.
///
/// Examples:
/// - Default: Text format for human consumption
/// - Automation: JSON format for programmatic parsing
/// - Default: JSON format for automation and AI agents
/// - Human output: Pass --output-format text for human-readable display
/// - CI/CD: JSON piped to jq for field extraction
#[arg(long, value_enum, default_value = "text", global = true)]
#[arg(long, value_enum, default_value = "json", global = true)]
pub output_format: OutputFormat,

/// Increase verbosity of user-facing output
Expand Down
14 changes: 7 additions & 7 deletions src/presentation/cli/input/cli/output_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@
/// ```rust
/// use torrust_tracker_deployer_lib::presentation::cli::input::cli::OutputFormat;
///
/// // Default is text format
/// // Default is JSON format
/// let format = OutputFormat::default();
/// assert!(matches!(format, OutputFormat::Text));
/// assert!(matches!(format, OutputFormat::Json));
///
/// // JSON format for automation
/// let json_format = OutputFormat::Json;
/// // Text format for human-readable output
/// let text_format = OutputFormat::Text;
/// ```
#[derive(Clone, Copy, Debug, Default, clap::ValueEnum)]
pub enum OutputFormat {
/// Human-readable text output (default)
/// Human-readable text output
///
/// Produces formatted text with tables, sections, and visual elements
/// optimized for terminal display and human consumption.
Expand All @@ -38,10 +38,9 @@ pub enum OutputFormat {
/// 3. Data directory: ./data/my-env
/// 4. Build directory: ./build/my-env
/// ```
#[default]
Text,

/// JSON output for automation and programmatic parsing
/// JSON output for automation and programmatic parsing (default)
///
/// Produces machine-readable JSON objects that can be parsed by tools
/// like jq, scripts, and AI agents for programmatic extraction of data.
Expand All @@ -56,5 +55,6 @@ pub enum OutputFormat {
/// "created_at": "2026-02-16T14:30:00Z"
/// }
/// ```
#[default]
Json,
}
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/configure/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &ConfigureDetailsData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize configure details",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize configure details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/destroy/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &DestroyDetailsData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize destroy details",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize destroy details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/list/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,16 @@ impl JsonView {
#[must_use]
pub fn render(list: &EnvironmentList) -> String {
serde_json::to_string_pretty(list).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize environment list",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize environment list",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/purge/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &PurgeDetailsData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize purge details",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize purge details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/register/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &RegisterDetailsData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize register details",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize register details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/release/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &ReleaseDetailsData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize release details",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize release details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/render/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &RenderDetailsData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize render details",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize render details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/presentation/cli/views/commands/run/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,18 @@ impl JsonView {
grafana,
};

serde_json::to_string_pretty(&output)
.unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize: {e}"}}"#))
serde_json::to_string_pretty(&output).unwrap_or_else(|e| {
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize run details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}

Expand Down
14 changes: 12 additions & 2 deletions src/presentation/cli/views/commands/show/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,18 @@ impl JsonView {
/// ```
#[must_use]
pub fn render(info: &EnvironmentInfo) -> String {
serde_json::to_string_pretty(info)
.unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize: {e}"}}"#))
serde_json::to_string_pretty(info).unwrap_or_else(|e| {
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize show details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}

Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/test/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &TestResultData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize test results",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize test results",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/presentation/cli/views/commands/validate/views/json_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,16 @@ impl JsonView {
#[must_use]
pub fn render(data: &ValidateDetailsData) -> String {
serde_json::to_string_pretty(data).unwrap_or_else(|e| {
format!(
r#"{{
"error": "Failed to serialize validate details",
"message": "{e}"
}}"#
)
serde_json::to_string_pretty(&serde_json::json!({
"error": "Failed to serialize validate details",
"message": e.to_string(),
}))
.unwrap_or_else(|_| {
r#"{
"error": "Failed to serialize error message"
}"#
.to_string()
})
})
}
}
Expand Down
39 changes: 39 additions & 0 deletions tests/e2e/create_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,42 @@ fn it_should_fail_when_environment_already_exists() {
"Error message should mention environment already exists, got: {stderr}"
);
}

#[test]
fn it_should_produce_json_by_default() {
// Verify dependencies before running tests
verify_required_dependencies().expect("Dependency verification failed");

// Arrange: Create a valid environment config file in a temp workspace
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");
let config = create_test_environment_config("test-create-json-default");
temp_workspace
.write_config_file("environment.json", &config)
.expect("Failed to write config file");
let config_path = temp_workspace.path().join("environment.json");

// Act: Run create command without --output-format
let result = process_runner()
.working_dir(temp_workspace.path())
.log_dir(temp_workspace.path().join("logs"))
.run_create_command(config_path.to_str().unwrap())
.expect("Failed to run create command");

// Assert: Command succeeds
assert!(
result.success(),
"Create command should succeed with a valid config, stderr: {}",
result.stderr()
);

// Assert: stdout is valid JSON
let stdout = result.stdout();
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("Create command default output must be valid JSON");

// Assert: Expected field is present
assert!(
json.get("environment_name").is_some(),
"Expected `environment_name` field in create JSON output, got: {stdout}"
);
}
Loading
Loading