CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

Add CLI root and test labeler subcommand stub

Implements Tasks 3 and 4: root clap Parser in src/cli.rs dispatching to
Command enum in src/commands.rs, with Test variant nesting TestCmd in
src/commands/test.rs, which holds Labeler(LabelerCmd) in
src/commands/test/labeler.rs. LabelerCmd accepts target, --did, and
--subscribe-timeout arguments with humantime-based duration parsing and
1-second floor validation. Stub run() prints parsed arguments and returns
OK without implementation. Verification: cargo check and cargo build pass.

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

+150
+54
src/cli.rs
··· 1 + //! Root clap parser and dispatch entry point. 2 + 3 + use clap::Parser; 4 + use miette::Result; 5 + use tracing_subscriber::{EnvFilter, fmt}; 6 + 7 + use crate::commands::Command; 8 + use crate::common::diagnostics::install_miette_handler; 9 + 10 + /// Top-level `atproto-devtool` CLI. 11 + #[derive(Debug, Parser)] 12 + #[command( 13 + name = "atproto-devtool", 14 + version, 15 + about = "Diagnostics and conformance tooling for atproto services.", 16 + long_about = None, 17 + )] 18 + pub struct Cli { 19 + /// Enable verbose (DEBUG-level) logging to stderr. 20 + #[arg(long, global = true)] 21 + verbose: bool, 22 + 23 + /// Disable ANSI color in rendered diagnostics. 24 + #[arg(long, global = true)] 25 + no_color: bool, 26 + 27 + #[command(subcommand)] 28 + command: Command, 29 + } 30 + 31 + /// Parse `std::env::args()`, install global handlers, and dispatch. 32 + /// 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<()> { 37 + let cli = Cli::parse(); 38 + 39 + install_miette_handler(cli.no_color)?; 40 + install_tracing(cli.verbose); 41 + 42 + cli.command.run().await 43 + } 44 + 45 + fn install_tracing(verbose: bool) { 46 + let default_filter = if verbose { "debug" } else { "warn" }; 47 + let filter = EnvFilter::try_from_default_env() 48 + .unwrap_or_else(|_| EnvFilter::new(default_filter)); 49 + 50 + let _ = fmt() 51 + .with_env_filter(filter) 52 + .with_writer(std::io::stderr) 53 + .try_init(); 54 + }
+23
src/commands.rs
··· 1 + //! Top-level subcommand dispatch. 2 + 3 + use clap::Subcommand; 4 + use miette::Result; 5 + 6 + pub mod test; 7 + 8 + use self::test::TestCmd; 9 + 10 + #[derive(Debug, Subcommand)] 11 + pub enum Command { 12 + /// Conformance and diagnostic checks against atproto services. 13 + #[command(subcommand)] 14 + Test(TestCmd), 15 + } 16 + 17 + impl Command { 18 + pub async fn run(self) -> Result<()> { 19 + match self { 20 + Command::Test(cmd) => cmd.run().await, 21 + } 22 + } 23 + }
+22
src/commands/test.rs
··· 1 + //! `atproto-devtool test ...` subcommand tree. 2 + 3 + use clap::Subcommand; 4 + use miette::Result; 5 + 6 + pub mod labeler; 7 + 8 + use self::labeler::LabelerCmd; 9 + 10 + #[derive(Debug, Subcommand)] 11 + pub enum TestCmd { 12 + /// Run the labeler conformance suite against an atproto labeler. 13 + Labeler(LabelerCmd), 14 + } 15 + 16 + impl TestCmd { 17 + pub async fn run(self) -> Result<()> { 18 + match self { 19 + TestCmd::Labeler(cmd) => cmd.run().await, 20 + } 21 + } 22 + }
+51
src/commands/test/labeler.rs
··· 1 + //! `atproto-devtool test labeler <target>` command. 2 + 3 + use std::time::Duration; 4 + 5 + use clap::Args; 6 + use miette::Result; 7 + 8 + /// Run the labeler conformance suite against a handle, DID, or endpoint URL. 9 + #[derive(Debug, Args)] 10 + pub struct LabelerCmd { 11 + /// Handle (`alice.example`), DID (`did:plc:...` / `did:web:...`), or labeler endpoint URL. 12 + pub target: String, 13 + 14 + /// Explicit DID override. Required (and combined with the target URL) when 15 + /// `target` is a raw endpoint URL and you want identity/crypto checks to run. 16 + #[arg(long)] 17 + pub did: Option<String>, 18 + 19 + /// Per-connection time budget for the subscription-layer checks. 20 + /// 21 + /// Minimum 1 second; values below 1 second are rejected at parse time. 22 + #[arg( 23 + long, 24 + default_value = "5s", 25 + value_parser = parse_subscribe_timeout, 26 + )] 27 + pub subscribe_timeout: Duration, 28 + } 29 + 30 + impl LabelerCmd { 31 + pub async fn run(self) -> Result<()> { 32 + println!("atproto-devtool test labeler (not yet implemented)"); 33 + println!(" target = {}", self.target); 34 + println!(" did = {:?}", self.did); 35 + println!(" subscribe_timeout = {:?}", self.subscribe_timeout); 36 + Ok(()) 37 + } 38 + } 39 + 40 + pub(crate) fn parse_subscribe_timeout(raw: &str) -> Result<Duration, String> { 41 + let parsed = humantime::parse_duration(raw) 42 + .map_err(|e| format!("invalid --subscribe-timeout value `{raw}`: {e}"))?; 43 + 44 + if parsed < Duration::from_secs(1) { 45 + return Err(format!( 46 + "--subscribe-timeout must be at least 1 second (got {parsed:?})" 47 + )); 48 + } 49 + 50 + Ok(parsed) 51 + }