diff --git a/crates/pebble/src/commands.rs b/crates/pebble/src/commands.rs index e56cf933..adc2f21c 100644 --- a/crates/pebble/src/commands.rs +++ b/crates/pebble/src/commands.rs @@ -121,6 +121,25 @@ impl<'a> TaskObject<'a> { } } +/// Emits a JSON-serializable payload to standard output. +/// +/// Serializes the provided generic payload as a JSON string and writes it to `stdout` +/// followed by a newline. This centralizes the standard pattern of converting +/// structured data into machine-readable output for CLI commands. +/// +/// # Arguments +/// +/// * `payload` - A reference to any type that implements `serde::Serialize`. +/// +/// # Errors +/// +/// Returns an error if serialization fails (e.g., if the type contains a map with +/// non-string keys, though this is rare for standard API objects). +pub fn emit_json(payload: &T) -> Result<()> { + println!("{}", serde_json::to_string(payload)?); + Ok(()) +} + /// Resolved runtime configuration and paths for command execution. pub struct RunContext { pub current_dir: PathBuf, @@ -199,10 +218,7 @@ pub fn run_next(ctx: &RunContext, limit: usize) -> Result<()> { .into_iter() .map(|n| TaskObject::from_node(n, &graph, &ctx.tasks_dir)) .collect(); - println!( - "{}", - serde_json::to_string(&serde_json::json!({ "tasks": objects }))? - ); + emit_json(&serde_json::json!({ "tasks": objects }))?; return Ok(()); } @@ -227,18 +243,13 @@ pub fn run_show(ctx: &RunContext, id: &str, path_only: bool) -> Result<()> { if path_only { let rel_path = node.path.strip_prefix(&ctx.tasks_dir).unwrap_or(&node.path); if ctx.json { - println!( - "{}", - serde_json::to_string( - &serde_json::json!({ "path": rel_path.display().to_string() }) - )? - ); + emit_json(&serde_json::json!({ "path": rel_path.display().to_string() }))?; } else { println!("{}", rel_path.display()); } } else if ctx.json { let obj = TaskObject::from_node(node, &graph, &ctx.tasks_dir); - println!("{}", serde_json::to_string(&obj)?); + emit_json(&obj)?; } else { let obj = TaskObject::from_node(node, &graph, &ctx.tasks_dir); println!("Task: {} ({})", obj.title, obj.id); @@ -266,10 +277,7 @@ pub fn run_config_get(ctx: &RunContext, key: &str) -> Result<()> { })?; if ctx.json { - println!( - "{}", - serde_json::to_string(&serde_json::json!({ "key": key, "value": value }))? - ); + emit_json(&serde_json::json!({ "key": key, "value": value }))?; } else { println!("{value}"); } diff --git a/crates/pebble/src/commands/listing.rs b/crates/pebble/src/commands/listing.rs index 42ac708b..070b5927 100644 --- a/crates/pebble/src/commands/listing.rs +++ b/crates/pebble/src/commands/listing.rs @@ -4,7 +4,7 @@ use color_eyre::eyre::{Result, eyre}; use std::cmp::Ordering; use std::collections::HashSet; -use super::{RunContext, TaskObject}; +use super::{RunContext, TaskObject, emit_json}; /// Filters and switches accepted by `pebble list`. pub struct ListOptions { @@ -189,10 +189,7 @@ fn emit_task_list(ctx: &RunContext, graph: &TaskGraph, tasks: Vec<&TaskNode>) -> .into_iter() .map(|n| TaskObject::from_node(n, graph, &ctx.tasks_dir)) .collect(); - println!( - "{}", - serde_json::to_string(&serde_json::json!({ "tasks": objects }))? - ); + emit_json(&serde_json::json!({ "tasks": objects }))?; } else { for task in tasks { println!( diff --git a/crates/pebble/src/commands_add.rs b/crates/pebble/src/commands_add.rs index 301926dc..247f0c3d 100644 --- a/crates/pebble/src/commands_add.rs +++ b/crates/pebble/src/commands_add.rs @@ -1,4 +1,6 @@ -use crate::commands::{RunContext, TaskObject, read_stdin_if_dash, validate_task_references}; +use crate::commands::{ + RunContext, TaskObject, emit_json, read_stdin_if_dash, validate_task_references, +}; use crate::graph::TaskGraph; use crate::models::{Priority, TaskFrontmatter, TaskNode, TaskStatus}; use crate::task_io::current_task_time; @@ -193,7 +195,7 @@ pub fn run_add(ctx: &RunContext, input: RunAddInput) -> Result<()> { let updated_graph = TaskGraph::new(graph.nodes); let mut obj = TaskObject::from_node(&node, &updated_graph, &ctx.tasks_dir); obj.path = display_path; - println!("{}", serde_json::to_string(&obj)?); + emit_json(&obj)?; } else { eprintln!("Created task {} at {}", new_id, display_path); } diff --git a/crates/pebble/src/commands_archive.rs b/crates/pebble/src/commands_archive.rs index 425cb868..d0f4451a 100644 --- a/crates/pebble/src/commands_archive.rs +++ b/crates/pebble/src/commands_archive.rs @@ -1,4 +1,4 @@ -use crate::commands::RunContext; +use crate::commands::{RunContext, emit_json}; use crate::graph::TaskGraph; use color_eyre::eyre::{Result, eyre}; use std::fs; @@ -39,10 +39,7 @@ pub fn run_archive(ctx: &RunContext) -> Result<()> { } if ctx.json { - println!( - "{}", - serde_json::to_string(&serde_json::json!({ "archived": archived }))? - ); + emit_json(&serde_json::json!({ "archived": archived }))?; } Ok(()) diff --git a/crates/pebble/src/commands_diagnostics.rs b/crates/pebble/src/commands_diagnostics.rs index eee1841b..b64519a1 100644 --- a/crates/pebble/src/commands_diagnostics.rs +++ b/crates/pebble/src/commands_diagnostics.rs @@ -1,4 +1,4 @@ -use crate::commands::RunContext; +use crate::commands::{RunContext, emit_json}; use crate::graph::TaskGraph; use crate::models::TaskNode; use color_eyre::eyre::Result; @@ -53,7 +53,7 @@ fn report_diagnostics(ctx: &RunContext, errors: Vec) -> Result< if ctx.json { let out = DiagnosticsOutput { ok, errors }; - println!("{}", serde_json::to_string(&out)?); + emit_json(&out)?; } else if ok { println!("Graph is healthy. No issues found."); } else { diff --git a/crates/pebble/src/commands_fix.rs b/crates/pebble/src/commands_fix.rs index 374be187..641015e1 100644 --- a/crates/pebble/src/commands_fix.rs +++ b/crates/pebble/src/commands_fix.rs @@ -1,4 +1,4 @@ -use crate::commands::RunContext; +use crate::commands::{RunContext, emit_json}; use crate::commands_diagnostics::collect_diagnostics; use crate::graph::TaskGraph; use crate::models::TaskNode; @@ -34,12 +34,7 @@ pub fn run_fix(ctx: &RunContext) -> Result<()> { } if ctx.json { - println!( - "{}", - serde_json::to_string( - &serde_json::json!({ "ok": ok, "fixed_tasks": modified_ids, "errors": errors }) - )? - ); + emit_json(&serde_json::json!({ "ok": ok, "fixed_tasks": modified_ids, "errors": errors }))?; } else if modified_ids.is_empty() { println!("No repairs needed."); } else { diff --git a/crates/pebble/src/commands_write.rs b/crates/pebble/src/commands_write.rs index 157d5ec7..4d834682 100644 --- a/crates/pebble/src/commands_write.rs +++ b/crates/pebble/src/commands_write.rs @@ -1,4 +1,6 @@ -use crate::commands::{RunContext, TaskObject, read_stdin_if_dash, validate_task_references}; +use crate::commands::{ + RunContext, TaskObject, emit_json, read_stdin_if_dash, validate_task_references, +}; use crate::config::{Config, validate_tasks_dir}; use crate::graph::TaskGraph; use crate::models::{NotFoundError, Priority, TaskNode, TaskStatus, UsageError}; @@ -313,7 +315,7 @@ pub fn run_update(ctx: &RunContext, mut input: RunUpdateInput) -> Result<()> { .insert(node.frontmatter.id.clone(), node.clone()); let updated_graph = TaskGraph::new(graph.nodes); let obj = TaskObject::from_node(&node, &updated_graph, &ctx.tasks_dir); - println!("{}", serde_json::to_string(&obj)?); + emit_json(&obj)?; } else { eprintln!("Updated task {}", node.frontmatter.id); } diff --git a/crates/pebble/src/main.rs b/crates/pebble/src/main.rs index e972a83d..230b72ed 100644 --- a/crates/pebble/src/main.rs +++ b/crates/pebble/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; use clap::error::ErrorKind; use color_eyre::eyre::Result; use pebble::cli::{Cli, Commands, ConfigCommands}; +use pebble::commands::emit_json; use pebble::commands::{ ListOptions, RunContext, run_config_get, run_list, run_next, run_search, run_show, }; @@ -17,7 +18,7 @@ use std::path::{Path, PathBuf}; use std::process::ExitCode; fn run_help_json() -> Result<()> { - println!("{}", serde_json::to_string(&help_json_schema())?); + emit_json(&help_json_schema())?; Ok(()) } diff --git a/patch_commands.diff b/patch_commands.diff new file mode 100644 index 00000000..65f4dd24 --- /dev/null +++ b/patch_commands.diff @@ -0,0 +1,26 @@ +<<<<<<< SEARCH +/// Resolved runtime configuration and paths for command execution. +pub struct RunContext { +======= +/// Emits a JSON-serializable payload to standard output. +/// +/// Serializes the provided generic payload as a JSON string and writes it to `stdout` +/// followed by a newline. This centralizes the standard pattern of converting +/// structured data into machine-readable output for CLI commands. +/// +/// # Arguments +/// +/// * `payload` - A reference to any type that implements `serde::Serialize`. +/// +/// # Errors +/// +/// Returns an error if serialization fails (e.g., if the type contains a map with +/// non-string keys, though this is rare for standard API objects). +pub fn emit_json(payload: &T) -> Result<()> { + println!("{}", serde_json::to_string(payload)?); + Ok(()) +} + +/// Resolved runtime configuration and paths for command execution. +pub struct RunContext { +>>>>>>> REPLACE