CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Wire test labeler pipeline into CLI and exit codes

Updates LabelerCmd::run to parse target, build HTTP/DNS clients,
run the pipeline, render the report with color control, and return
appropriate exit codes. Propagates ExitCode through the dispatch chain
(TestCmd -> Command -> cli::run -> main).

Threading no_color flag through from root Cli through dispatch.
ReportHeader is populated from IdentityFacts when available.

Verifies test-labeler.AC6.1 (partial) and AC6.6 (NO_COLOR honored).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

+59 -19
+5 -5
src/cli.rs
··· 2 2 3 3 use clap::Parser; 4 4 use miette::Result; 5 + use std::process::ExitCode; 5 6 use tracing_subscriber::{EnvFilter, fmt}; 6 7 7 8 use crate::commands::Command; ··· 30 31 31 32 /// Parse `std::env::args()`, install global handlers, and dispatch. 32 33 /// 33 - /// Returns `miette::Result<()>` so main can print the report via the installed 34 - /// handler. An `Err` here produces exit code 1; clap parse errors already exit 35 - /// with code 2 before this function is called. 36 - pub async fn run() -> Result<()> { 34 + /// Returns `Result<ExitCode, miette::Report>` to propagate exit codes through 35 + /// the dispatch chain. The main handler renders errors with exit code 1. 36 + pub async fn run() -> Result<ExitCode> { 37 37 let cli = Cli::parse(); 38 38 39 39 install_miette_handler(cli.no_color)?; 40 40 install_tracing(cli.verbose); 41 41 42 - cli.command.run().await 42 + cli.command.run(cli.no_color).await 43 43 } 44 44 45 45 fn install_tracing(verbose: bool) {
+4 -3
src/commands.rs
··· 1 1 //! Top-level subcommand dispatch. 2 2 3 3 use clap::Subcommand; 4 - use miette::Result; 4 + use miette::Report; 5 + use std::process::ExitCode; 5 6 6 7 pub mod test; 7 8 ··· 15 16 } 16 17 17 18 impl Command { 18 - pub async fn run(self) -> Result<()> { 19 + pub async fn run(self, no_color: bool) -> Result<ExitCode, Report> { 19 20 match self { 20 - Command::Test(cmd) => cmd.run().await, 21 + Command::Test(cmd) => cmd.run(no_color).await, 21 22 } 22 23 } 23 24 }
+4 -3
src/commands/test.rs
··· 1 1 //! `atproto-devtool test ...` subcommand tree. 2 2 3 3 use clap::Subcommand; 4 - use miette::Result; 4 + use miette::Report; 5 + use std::process::ExitCode; 5 6 6 7 pub mod labeler; 7 8 ··· 14 15 } 15 16 16 17 impl TestCmd { 17 - pub async fn run(self) -> Result<()> { 18 + pub async fn run(self, no_color: bool) -> Result<ExitCode, Report> { 18 19 match self { 19 - TestCmd::Labeler(cmd) => cmd.run().await, 20 + TestCmd::Labeler(cmd) => cmd.run(no_color).await, 20 21 } 21 22 } 22 23 }
+44 -7
src/commands/test/labeler.rs
··· 4 4 pub mod pipeline; 5 5 pub mod report; 6 6 7 + use std::io; 8 + use std::process::ExitCode; 7 9 use std::time::Duration; 8 10 9 11 use clap::Args; 10 - use miette::Result; 12 + use miette::Report; 13 + 14 + use crate::common::identity::{RealDnsResolver, RealHttpClient}; 15 + use pipeline::{parse_target, run_pipeline, LabelerOptions}; 16 + use report::RenderConfig; 11 17 12 18 /// Run the labeler conformance suite against a handle, DID, or endpoint URL. 13 19 #[derive(Debug, Args)] ··· 29 35 value_parser = parse_subscribe_timeout, 30 36 )] 31 37 pub subscribe_timeout: Duration, 38 + 39 + /// Whether to suppress colored output. 40 + #[arg(long)] 41 + pub no_color: bool, 42 + 43 + /// Whether to emit verbose diagnostics. 44 + #[arg(long)] 45 + pub verbose: bool, 32 46 } 33 47 34 48 impl LabelerCmd { 35 - pub async fn run(self) -> Result<()> { 36 - println!("atproto-devtool test labeler (not yet implemented)"); 37 - println!(" target = {}", self.target); 38 - println!(" did = {:?}", self.did); 39 - println!(" subscribe_timeout = {:?}", self.subscribe_timeout); 40 - Ok(()) 49 + pub async fn run(self, no_color: bool) -> Result<ExitCode, Report> { 50 + // Parse the target. 51 + let target = parse_target(&self.target, self.did.as_deref()) 52 + .map_err(|e| miette::miette!("{e}"))?; 53 + 54 + // Build HTTP and DNS clients. 55 + let http = RealHttpClient::new() 56 + .map_err(|e| miette::miette!("Failed to initialize HTTP client: {}", e))?; 57 + let dns = RealDnsResolver::new(); 58 + 59 + // Run the pipeline. 60 + let opts = LabelerOptions { 61 + http: &http, 62 + dns: &dns, 63 + subscribe_timeout: self.subscribe_timeout, 64 + verbose: self.verbose, 65 + }; 66 + 67 + let report = run_pipeline(target, opts).await; 68 + 69 + // Render the report to stdout. 70 + let mut stdout = io::stdout().lock(); 71 + report 72 + .render(&mut stdout, &RenderConfig { no_color }) 73 + .map_err(|e| miette::miette!("Failed to render report: {}", e))?; 74 + 75 + // Return appropriate exit code. 76 + let exit_code = report.exit_code(); 77 + Ok(ExitCode::from(exit_code as u8)) 41 78 } 42 79 } 43 80
+2 -1
src/main.rs
··· 1 1 //! `atproto-devtool` binary entry point. 2 2 3 3 use miette::Result; 4 + use std::process::ExitCode; 4 5 5 6 #[tokio::main(flavor = "current_thread")] 6 - async fn main() -> Result<()> { 7 + async fn main() -> Result<ExitCode> { 7 8 atproto_devtool::cli::run().await 8 9 }