pub mod argv;
pub mod clap_errors;
pub mod commands;
pub mod dispatch;
pub mod runner;
pub use runner::{run, run_argv, run_with_paths};
use crate::output::{ColorMode, OutputFormat};
use crate::skill_install::SkillHost;
use clap::{Args, Parser};
pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
#[derive(Parser, Debug)]
#[command(
name = "bird",
about = "X API CLI",
version,
after_help = include_str!("../../examples/top-level.txt")
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
#[arg(long, short = 'u', global = true)]
pub username: Option<String>,
#[arg(long, short = 'o', global = true, value_enum, env = "BIRD_OUTPUT")]
pub output: Option<OutputFormat>,
#[arg(
long,
global = true,
conflicts_with_all = ["output", "jsonl"],
env = "BIRD_JSON",
value_parser = clap::builder::FalseyValueParser::new(),
)]
pub json: bool,
#[arg(
long,
global = true,
conflicts_with = "output",
env = "BIRD_JSONL",
value_parser = clap::builder::FalseyValueParser::new(),
)]
pub jsonl: bool,
#[arg(
long,
global = true,
value_enum,
default_value = "auto",
env = "BIRD_COLOR"
)]
pub color: ColorMode,
#[arg(long, global = true, hide = true)]
pub plain: bool,
#[arg(long, global = true, hide = true)]
pub no_color: bool,
#[arg(
long,
short = 'q',
global = true,
env = "BIRD_QUIET",
value_parser = clap::builder::FalseyValueParser::new(),
)]
pub quiet: bool,
#[arg(
long,
short = 'v',
global = true,
action = clap::ArgAction::Count,
env = "BIRD_VERBOSE",
)]
pub verbose: u8,
#[arg(long, global = true, env = "BIRD_TIMEOUT", default_value_t = DEFAULT_TIMEOUT_SECS)]
pub timeout: u64,
#[arg(long, global = true, env = "BIRD_NO_INTERACTIVE")]
pub no_interactive: bool,
#[arg(long, global = true)]
pub raw: bool,
#[arg(long, global = true)]
pub examples: bool,
#[arg(long, global = true)]
pub refresh: bool,
#[arg(long, global = true)]
pub no_cache: bool,
#[arg(long, global = true)]
pub cache_only: bool,
#[arg(long, global = true, value_name = "N")]
pub limit: Option<u32>,
#[arg(long, global = true, value_name = "TOKEN", alias = "page")]
pub cursor: Option<String>,
}
#[derive(Args, Debug, Clone, Copy, Default)]
pub struct WriteGuard {
#[arg(long, short = 'f', alias = "yes", global = false)]
pub force: bool,
#[arg(long, global = false)]
pub dry_run: bool,
}
#[derive(Args, Debug, Clone, Copy, Default)]
pub struct OutputFlags {
#[arg(long)]
pub pretty: bool,
}
#[derive(clap::Subcommand, Debug)]
pub enum Command {
#[command(after_help = include_str!("../../examples/login.txt"))]
Login {
#[command(flatten)]
headless: crate::login::HeadlessAuthArgs,
},
#[command(after_help = include_str!("../../examples/me.txt"))]
Me {
#[command(flatten)]
common: OutputFlags,
},
#[command(after_help = include_str!("../../examples/get.txt"))]
Get {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[command(flatten)]
common: OutputFlags,
},
#[command(after_help = include_str!("../../examples/post.txt"))]
Post {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[arg(long, value_name = "JSON")]
body: Option<String>,
#[command(flatten)]
common: OutputFlags,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/put.txt"))]
Put {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[arg(long, value_name = "JSON")]
body: Option<String>,
#[command(flatten)]
common: OutputFlags,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/bookmarks.txt"))]
Bookmarks {
#[command(flatten)]
common: OutputFlags,
},
#[command(after_help = include_str!("../../examples/profile.txt"))]
Profile {
username: String,
#[command(flatten)]
common: OutputFlags,
},
#[command(after_help = include_str!("../../examples/search.txt"))]
Search {
query: String,
#[command(flatten)]
common: OutputFlags,
#[arg(long, default_value = "recent")]
sort: String,
#[arg(long)]
min_likes: Option<u64>,
#[arg(long)]
max_results: Option<u32>,
#[arg(long)]
pages: Option<u32>,
},
#[command(after_help = include_str!("../../examples/thread.txt"))]
Thread {
tweet_id: String,
#[command(flatten)]
common: OutputFlags,
#[arg(long, default_value = "10")]
max_pages: u32,
},
#[command(after_help = include_str!("../../examples/delete.txt"))]
Delete {
path: String,
#[arg(long, short = 'p', value_name = "KEY=VALUE", num_args = 1..)]
param: Vec<String>,
#[arg(long, value_name = "KEY=VALUE", num_args = 1..)]
query: Vec<String>,
#[command(flatten)]
common: OutputFlags,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/watchlist.txt"))]
Watchlist {
#[command(subcommand)]
action: WatchlistCommand,
#[command(flatten)]
common: OutputFlags,
},
#[command(after_help = include_str!("../../examples/usage.txt"))]
Usage {
#[arg(long)]
since: Option<String>,
#[arg(long)]
local: bool,
#[command(flatten)]
common: OutputFlags,
},
#[command(after_help = include_str!("../../examples/tweet.txt"))]
Tweet {
text: String,
#[arg(long)]
media_id: Option<String>,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/reply.txt"))]
Reply {
tweet_id: String,
text: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/like.txt"))]
Like {
tweet_id: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/unlike.txt"))]
Unlike {
tweet_id: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/repost.txt"))]
Repost {
tweet_id: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/unrepost.txt"))]
Unrepost {
tweet_id: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/follow.txt"))]
Follow {
username: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/unfollow.txt"))]
Unfollow {
username: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/dm.txt"))]
Dm {
username: String,
text: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/block.txt"))]
Block {
username: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/unblock.txt"))]
Unblock {
username: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/mute.txt"))]
Mute {
username: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/unmute.txt"))]
Unmute {
username: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/doctor.txt"))]
Doctor {
command: Option<String>,
#[command(flatten)]
common: OutputFlags,
},
#[command(after_help = include_str!("../../examples/cache.txt"))]
Cache {
#[command(subcommand)]
action: CacheAction,
},
#[command(after_help = include_str!("../../examples/completions.txt"))]
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
#[command(after_help = "Examples:
bird skill install claude_code
bird skill install claude_code --dry-run
bird skill install --all
bird skill update claude_code")]
Skill {
#[command(subcommand)]
action: SkillAction,
},
#[command(after_help = "Examples:
bird schema
bird schema --list
bird schema bookmarks
bird schema bookmarks --output json")]
Schema {
name: Option<String>,
#[arg(long)]
list: bool,
},
}
#[derive(clap::Subcommand, Debug, Clone, Copy)]
pub enum SkillAction {
Install {
#[arg(value_enum, conflicts_with = "all")]
host: Option<SkillHost>,
#[arg(long)]
all: bool,
#[arg(long)]
dry_run: bool,
},
#[command(alias = "upgrade")]
Update {
#[arg(value_enum, conflicts_with = "all")]
host: Option<SkillHost>,
#[arg(long)]
all: bool,
#[arg(long)]
dry_run: bool,
},
}
#[derive(clap::Subcommand, Debug)]
pub enum CacheAction {
#[command(after_help = include_str!("../../examples/cache-clear.txt"))]
Clear {
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/cache-stats.txt"))]
Stats {
#[command(flatten)]
common: OutputFlags,
},
}
#[derive(clap::Subcommand, Debug)]
pub enum WatchlistCommand {
#[command(
alias = "check",
after_help = include_str!("../../examples/watchlist-check.txt"),
)]
Fetch,
#[command(after_help = include_str!("../../examples/watchlist-add.txt"))]
Add {
username: String,
},
#[command(after_help = include_str!("../../examples/watchlist-remove.txt"))]
Remove {
username: String,
#[command(flatten)]
guard: WriteGuard,
},
#[command(after_help = include_str!("../../examples/watchlist-list.txt"))]
List,
}
const _: fn() = || {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<Cli>();
_assert_send_sync::<Command>();
};
impl Cli {
pub fn effective_color(&self) -> ColorMode {
if self.plain || self.no_color {
ColorMode::Never
} else {
self.color
}
}
pub fn effective_output(&self) -> Option<OutputFormat> {
if self.json {
Some(OutputFormat::Json)
} else if self.jsonl {
Some(OutputFormat::Jsonl)
} else {
self.output
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn me_with_pretty_sets_common_pretty_true() {
let cli = Cli::try_parse_from(["bird", "me", "--pretty"]).expect("bird me --pretty parses");
match cli.command {
Command::Me { common } => assert!(common.pretty),
other => panic!("expected Me variant, got {:?}", other),
}
}
#[test]
fn cache_stats_pretty_flattens_into_common() {
let cli = Cli::try_parse_from(["bird", "cache", "stats", "--pretty"])
.expect("bird cache stats --pretty parses");
match cli.command {
Command::Cache {
action: CacheAction::Stats { common },
} => assert!(common.pretty),
other => panic!("expected Cache::Stats variant, got {:?}", other),
}
}
#[test]
fn write_only_subcommands_reject_pretty_flag() {
assert!(
Cli::try_parse_from(["bird", "like", "123", "--pretty"]).is_err(),
"like is write-only; --pretty must not parse"
);
assert!(
Cli::try_parse_from(["bird", "follow", "alice", "--pretty"]).is_err(),
"follow is write-only; --pretty must not parse"
);
assert!(
Cli::try_parse_from(["bird", "tweet", "hello", "--pretty"]).is_err(),
"tweet is write-only; --pretty must not parse"
);
}
}