pub mod commands;
pub mod exit_codes;
pub mod runner;
pub use runner::{run, run_argv, run_with_store_path};
use clap::builder::FalseyValueParser;
use clap::{Parser, Subcommand, ValueEnum};
pub use crate::output::OutputFormat;
pub use crate::skill_install::SkillHost;
#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)]
#[value(rename_all = "lower")]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
const ROOT_HELP: &str = "\
Examples:
Authenticate (browser):
xr auth oauth2
Authenticate (headless / SSH / container):
xr auth oauth2 --no-browser --step 1
Post a status update (text vs JSON):
xr post \"Hello world\"
xr post \"Hello world\" --output json
Search recent posts with env-var precedence:
XURL_OUTPUT=json xr search \"rustlang\" -n 25
Browse the full curated gallery:
xr examples
ENVIRONMENT VARIABLES:
XURL_OUTPUT Output format: text, json, jsonl, ndjson, yaml, csv, tsv (same as --output)
XURL_CURSOR Pagination cursor / page token (same as --cursor)
XURL_QUIET Suppress non-essential output (same as --quiet)
XURL_NO_INTERACTIVE Fail instead of prompting (same as --no-interactive)
XURL_TIMEOUT Network timeout in seconds (same as --timeout)
XURL_COLOR Color control: auto, always, never (same as --color)
XURL_VERBOSE Verbose request/response logging (same as -v/--verbose)
XURL_APP Override default app (same as --app)
XURL_JSON Shorthand for XURL_OUTPUT=json (same as --json)
XURL_JSONL Shorthand for XURL_OUTPUT=jsonl (same as --jsonl)
XURL_NO_BROWSER Skip browser-open on `auth oauth2` (same as --no-browser)
REDIRECT_URI OAuth2 redirect URI override for the active app
Flags override env vars when both are set. NO_COLOR=1 always wins over
--color/XURL_COLOR (https://no-color.org). --no-pager is a documented
no-op so agents can pass it unconditionally; xr never invokes $PAGER.
INPUT FROM STDIN:
Subcommands that accept JSON input (currently `xr validate`) read from
stdin when no file argument is given or when `-` is passed as the path.
This matches the standard CLI convention for piping data:
cat tweet.json | xr validate --schema tweet --output json
cat tweet.json | xr validate - --schema tweet --output json
EXIT CODES:
0 success
1 general error
2 invalid arguments or authentication required
3 rate-limited (HTTP 429)
4 not found (HTTP 404)
5 network error
TTY behavior:
When stdout is not a TTY, color output is auto-stripped (unless
--color always is set) and human-only banners are suppressed, so piping
to jaq or redirecting to a file produces clean machine-readable output
without any extra flags.
";
const POST_HELP: &str = "\
Examples:
Post a status update (text):
xr post \"Hello world\"
Post the same update (machine-readable JSON envelope):
xr post \"Hello world\" --output json
Post with media (run `xr media upload` first to get an ID):
xr post \"Look at this\" --media-id 1234567890 --output json
Post quietly (suppress human-readable banners):
xr post \"Ship it\" --quiet --output json
";
const REPLY_HELP: &str = "\
Examples:
Reply to a post by ID:
xr reply 1234567890 \"Congrats!\"
Reply with JSON output for scripting:
xr reply 1234567890 \"Congrats!\" --output json
Reply to a post URL (xr accepts either):
xr reply https://x.com/jack/status/1234567890 \"Nice thread.\"
Reply with a media attachment:
xr reply 1234567890 \"Here's a photo\" --media-id 222 --output json
";
const QUOTE_HELP: &str = "\
Examples:
Quote-post by ID:
xr quote 1234567890 \"Worth a read.\"
Quote-post with JSON envelope:
xr quote 1234567890 \"Worth a read.\" --output json
Quote-post from a URL:
xr quote https://x.com/jack/status/1234567890 \"Thread.\"
";
const DELETE_HELP: &str = "\
Examples:
Delete a post by ID (text):
xr delete 1234567890
Delete with JSON envelope:
xr delete 1234567890 --output json
Delete in a non-interactive context (CI, agent):
xr delete 1234567890 --no-interactive --output json
";
const READ_HELP: &str = "\
Examples:
Read a post (text):
xr read 1234567890
Read a post (JSON):
xr read 1234567890 --output json
Read a post URL:
xr read https://x.com/jack/status/1234567890 --output json
Extract a single field via jaq:
xr read 1234567890 --output json | jaq '.data.text'
";
const SEARCH_HELP: &str = "\
Examples:
Search recent posts (text):
xr search \"rustlang\"
Search with 25 results, JSON envelope:
xr search \"rustlang\" -n 25 --output json
Demonstrate env-var precedence (XURL_OUTPUT == --output):
XURL_OUTPUT=json xr search \"rustlang\"
Stream-friendly JSONL piped to jaq:
xr search \"rustlang\" --output jsonl | jaq '.id'
";
const WHOAMI_HELP: &str = "\
Examples:
Show your profile (text):
xr whoami
Same, as a JSON envelope:
xr whoami --output json
Act as a specific authenticated user (multi-account):
xr whoami --username alice --output json
";
const USER_HELP: &str = "\
Examples:
Look up a user by handle (text):
xr user jack
Same, as JSON:
xr user jack --output json
Use the @ prefix (xr accepts either):
xr user @jack --output json
";
const TIMELINE_HELP: &str = "\
Examples:
Home timeline (last 10, text):
xr timeline
Home timeline (50 results, JSON envelope):
xr timeline -n 50 --output json
Stream-friendly JSONL piped to jaq:
xr timeline -n 100 --output jsonl | jaq '.id'
";
const MENTIONS_HELP: &str = "\
Examples:
Your mentions (text):
xr mentions
Last 25, JSON envelope:
xr mentions -n 25 --output json
JSONL pipeline:
xr mentions -n 100 --output jsonl | jaq '.id'
";
const LIKE_HELP: &str = "\
Examples:
Like a post (text):
xr like 1234567890
Like a post (JSON envelope):
xr like 1234567890 --output json
Idempotent: re-liking is a server-side no-op.
";
const UNLIKE_HELP: &str = "\
Examples:
Unlike a post (text):
xr unlike 1234567890
Unlike (JSON envelope):
xr unlike 1234567890 --output json
";
const REPOST_HELP: &str = "\
Examples:
Repost a post (text):
xr repost 1234567890
Repost (JSON envelope):
xr repost 1234567890 --output json
";
const UNREPOST_HELP: &str = "\
Examples:
Undo a repost (text):
xr unrepost 1234567890
Undo a repost (JSON envelope):
xr unrepost 1234567890 --output json
";
const BOOKMARK_HELP: &str = "\
Examples:
Bookmark a post (text):
xr bookmark 1234567890
Bookmark (JSON envelope):
xr bookmark 1234567890 --output json
";
const UNBOOKMARK_HELP: &str = "\
Examples:
Remove a bookmark (text):
xr unbookmark 1234567890
Remove a bookmark (JSON envelope):
xr unbookmark 1234567890 --output json
";
const BOOKMARKS_HELP: &str = "\
Examples:
List your bookmarks (text):
xr bookmarks
100 results, JSON envelope:
xr bookmarks -n 100 --output json
JSONL piped to jaq:
xr bookmarks -n 100 --output jsonl | jaq '.id'
";
const LIKES_HELP: &str = "\
Examples:
List your liked posts (text):
xr likes
100 results, JSON envelope:
xr likes -n 100 --output json
JSONL piped to jaq:
xr likes -n 100 --output jsonl | jaq '.id'
";
const FOLLOW_HELP: &str = "\
Examples:
Follow a user (text):
xr follow @jack
Follow (JSON envelope):
xr follow @jack --output json
Without the @ prefix:
xr follow jack --output json
";
const UNFOLLOW_HELP: &str = "\
Examples:
Unfollow a user (text):
xr unfollow @jack
Unfollow (JSON envelope):
xr unfollow @jack --output json
";
const FOLLOWING_HELP: &str = "\
Examples:
Users you follow (text):
xr following
100 results, JSON envelope:
xr following -n 100 --output json
Who someone else follows:
xr following --of @jack --output json
";
const FOLLOWERS_HELP: &str = "\
Examples:
Your followers (text):
xr followers
100 results, JSON envelope:
xr followers -n 100 --output json
Someone else's followers:
xr followers --of @jack --output json
";
const MUTE_HELP: &str = "\
Examples:
Mute a user (text):
xr mute @noisy
Mute (JSON envelope):
xr mute @noisy --output json
";
const UNMUTE_HELP: &str = "\
Examples:
Unmute a user (text):
xr unmute @noisy
Unmute (JSON envelope):
xr unmute @noisy --output json
";
const USAGE_HELP: &str = "\
Examples:
Show API usage (text):
xr usage
Show API usage (JSON envelope, machine-parseable cap data):
xr usage --output json
Quiet + JSON for clean agent consumption:
xr usage --quiet --output json
";
const DM_HELP: &str = "\
Examples:
Send a DM (text):
xr dm @recipient \"Hello\"
Send a DM (JSON envelope):
xr dm @recipient \"Hello\" --output json
Without the @ prefix:
xr dm recipient \"Hi\" --output json
";
const DMS_HELP: &str = "\
Examples:
List recent DM events (text):
xr dms
50 results, JSON envelope:
xr dms -n 50 --output json
JSONL piped to jaq:
xr dms -n 100 --output jsonl | jaq '.id'
";
const AUTH_HELP: &str = "\
Examples:
Browser-based OAuth2 (default):
xr auth oauth2
Headless OAuth2 (servers, containers):
xr auth oauth2 --no-browser --step 1
Bearer token for read-only / search:
xr auth app --bearer-token \"$TOKEN\"
Show current auth state, machine-readable:
xr auth status --output json
";
const MEDIA_HELP: &str = "\
Examples:
Upload an image:
xr media upload ./photo.png --media-type image/png --category tweet_image
Upload a video and wait for processing:
xr media upload ./clip.mp4 --wait --output json
Check upload status:
xr media status 1234567890 --output json
";
const SCHEMA_HELP: &str = "\
Examples:
List all command schemas (text):
xr schema --list
List all command schemas (JSON):
xr schema --list --output json
Dump a single command's schema:
xr schema post --output json
Dump every schema as one JSON document:
xr schema --all --output json
";
const COMPLETIONS_HELP: &str = "\
Examples:
Bash:
xr completions bash > ~/.bash_completion.d/xr
Zsh:
xr completions zsh > ~/.zfunc/_xr
Fish:
xr completions fish > ~/.config/fish/completions/xr.fish
";
const VERSION_HELP: &str = "\
Examples:
Print the binary version (text):
xr version
Same, JSON-friendly via the top-level flag:
xr --version
";
const VALIDATE_HELP: &str = "\
Examples:
Read JSON from stdin (no file argument):
cat tweet.json | xr validate --output json
Same, written as `xr validate -` for explicit stdin:
cat tweet.json | xr validate - --schema tweet --output json
Validate a file against a specific schema:
xr validate tweets.json --schema tweets --output json
Validate the canonical xurl error envelope:
echo '{\"status\":\"error\",\"reason\":\"x\",\"exit_code\":1,\"message\":\"y\"}' | xr validate --schema envelope --output json
";
const EXAMPLES_HELP: &str = "\
Examples:
Print the full curated gallery:
xr examples
Discover env-var precedence and exit codes:
xr --help
Browse a single command's curated examples:
xr post --help
";
const AUTH_OAUTH2_HELP: &str = "\
Examples:
Interactive browser flow (default):
xr auth oauth2
Headless step 1 (generate auth URL on a server / container):
xr auth oauth2 --no-browser --step 1
Headless step 2 (paste the redirect URL after authorizing):
xr auth oauth2 --no-browser --step 2 --auth-url 'https://localhost/callback?code=...&state=...'
Headless step 2 reading the URL from stdin (recommended on shared boxes):
echo 'https://localhost/callback?code=...&state=...' | xr auth oauth2 --no-browser --step 2 --auth-url - --output json
Label the saved token with a specific username (skips /2/users/me):
xr auth oauth2 alice --output json
";
const AUTH_OAUTH1_HELP: &str = "\
Examples:
Configure OAuth1 from app + user credentials:
xr auth oauth1 \\
--consumer-key CK --consumer-secret CS \\
--access-token AT --token-secret TS
Same, with JSON envelope for scripted setup:
xr auth oauth1 --consumer-key CK --consumer-secret CS \\
--access-token AT --token-secret TS --output json
";
const AUTH_APP_HELP: &str = "\
Examples:
Set the bearer token from an env var:
xr auth app --bearer-token \"$XURL_BEARER_TOKEN\"
Same, JSON envelope:
xr auth app --bearer-token \"$XURL_BEARER_TOKEN\" --output json
Test the configured bearer:
xr auth status --output json
";
const AUTH_STATUS_HELP: &str = "\
Examples:
Show current auth state (text):
xr auth status
Same, machine-readable:
xr auth status --output json
Quiet + JSON for agent consumption:
xr auth status --quiet --output json
";
const AUTH_CLEAR_HELP: &str = "\
Examples:
Clear all tokens (text):
xr auth clear --all
Clear only the bearer token (JSON envelope):
xr auth clear --bearer --output json
Clear a single OAuth2 user:
xr auth clear --oauth2-username alice --output json
Non-interactive, fail without an explicit selector:
xr auth clear --all --no-interactive --output json
";
const AUTH_APPS_HELP: &str = "\
Examples:
Register a new app:
xr auth apps add my-app --client-id ID --client-secret SECRET
List registered apps (JSON):
xr auth apps list --output json
Update credentials for an existing app:
xr auth apps update my-app --client-secret NEW
Inspect or set the stored OAuth2 redirect URI:
xr auth apps redirect-uri get my-app --output json
";
const AUTH_DEFAULT_HELP: &str = "\
Examples:
Interactive picker (TTY):
xr auth default
Set default by name:
xr auth default my-app
Set default app + username together:
xr auth default my-app alice
Non-interactive fail-fast if no name is supplied:
xr auth default --no-interactive --output json
";
const APPS_ADD_HELP: &str = "\
Examples:
Register a new app (text):
xr auth apps add my-app --client-id ID --client-secret SECRET
Register with a custom redirect URI:
xr auth apps add my-app --client-id ID --client-secret SECRET \\
--redirect-uri https://localhost:8443/callback
Register, JSON envelope for scripted setup:
xr auth apps add my-app --client-id ID --client-secret SECRET --output json
";
const APPS_UPDATE_HELP: &str = "\
Examples:
Rotate the client secret:
xr auth apps update my-app --client-secret NEW
Update the redirect URI:
xr auth apps update my-app --redirect-uri https://localhost:8443/callback
Clear the stored redirect URI (pass empty string):
xr auth apps update my-app --redirect-uri \"\"
Same, JSON envelope:
xr auth apps update my-app --client-secret NEW --output json
";
const APPS_REMOVE_HELP: &str = "\
Examples:
Remove a registered app (text):
xr auth apps remove my-app
Remove (JSON envelope):
xr auth apps remove my-app --output json
Non-interactive removal in CI:
xr auth apps remove my-app --no-interactive --output json
";
const APPS_LIST_HELP: &str = "\
Examples:
List registered apps (text):
xr auth apps list
List (JSON envelope):
xr auth apps list --output json
Quiet + JSON for clean agent consumption:
xr auth apps list --quiet --output json
";
const APPS_REDIRECT_URI_HELP: &str = "\
Examples:
Show the effective redirect URI and its source:
xr auth apps redirect-uri get my-app --output json
Set the stored redirect URI:
xr auth apps redirect-uri set my-app https://localhost:8443/callback
Clear the stored redirect URI (empty value):
xr auth apps redirect-uri set my-app \"\"
";
const REDIRECT_URI_GET_HELP: &str = "\
Examples:
Show the effective URI for the default app (text):
xr auth apps redirect-uri get
Show for a specific app (JSON envelope):
xr auth apps redirect-uri get my-app --output json
Compare env-var override vs stored value:
REDIRECT_URI=https://example/callback xr auth apps redirect-uri get my-app --output json
";
const REDIRECT_URI_SET_HELP: &str = "\
Examples:
Set a custom redirect URI:
xr auth apps redirect-uri set my-app https://localhost:8443/callback
Same, JSON envelope:
xr auth apps redirect-uri set my-app https://localhost:8443/callback --output json
Clear the stored URI (empty string):
xr auth apps redirect-uri set my-app \"\"
";
const MEDIA_UPLOAD_HELP: &str = "\
Examples:
Upload an image (text):
xr media upload ./photo.png --media-type image/png --category tweet_image
Upload a video and wait for processing (JSON envelope):
xr media upload ./clip.mp4 --wait --output json
Upload using a specific auth method:
xr media upload ./photo.png --auth oauth2 --output json
Skip waiting (returns immediately after FINALIZE):
xr media upload ./clip.mp4 --wait false --output json
";
const MEDIA_STATUS_HELP: &str = "\
Examples:
Check upload status (text):
xr media status 1234567890
Check status (JSON envelope):
xr media status 1234567890 --output json
Poll until processing completes:
xr media status 1234567890 --wait --output json
";
#[derive(Parser, Debug)]
#[command(
name = "xr",
about = "Auth enabled curl-like interface for the X API",
long_about = r#"A command-line tool for making authenticated requests to the X API.
Shortcut commands (agent-friendly):
xr post "Hello world!" Post to X
xr reply 1234567890 "Nice!" Reply to a post
xr read 1234567890 Read a post
xr search "golang" -n 20 Search posts
xr whoami Show your profile
xr like 1234567890 Like a post
xr repost 1234567890 Repost
xr follow @user Follow a user
xr dm @user "Hey!" Send a DM
xr timeline Home timeline
xr mentions Your mentions
Raw API access (curl-style):
basic requests xr /2/users/me
xr -X POST /2/tweets -d '{"text":"Hello world!"}'
xr -H "Content-Type: application/json" /2/tweets
authentication xr --auth oauth2 /2/users/me
xr --auth oauth1 /2/users/me
xr --auth app /2/users/me
media and streaming xr media upload path/to/video.mp4
xr /2/tweets/search/stream --auth app
xr -s /2/users/me
Multi-app management:
xr auth apps add my-app --client-id ... --client-secret ...
xr auth apps list
xr auth default # interactive picker
xr auth default my-app # set by name
xr --app my-app /2/users/me # per-request override
Shell completions:
xr completions bash > ~/.bash_completion.d/xr
xr completions zsh > ~/.zfunc/_xr
xr completions fish > ~/.config/fish/completions/xr.fish
Run 'xr --help' to see all available commands."#,
after_help = ROOT_HELP,
version
)]
pub struct Cli {
#[arg(short = 'X', long = "method", global = false)]
pub method: Option<String>,
#[arg(short = 'H', long = "header")]
pub headers: Vec<String>,
#[arg(short = 'd', long = "data")]
pub data: Option<String>,
#[arg(long = "auth")]
pub auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
pub username: Option<String>,
#[arg(
short = 'v',
long = "verbose",
global = true,
env = "XURL_VERBOSE",
value_parser = FalseyValueParser::new(),
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
require_equals = false,
)]
pub verbose: bool,
#[arg(short = 't', long = "trace")]
pub trace: bool,
#[arg(short = 's', long = "stream")]
pub stream: bool,
#[arg(short = 'F', long = "file")]
pub file: Option<String>,
#[arg(long = "app", global = true, env = "XURL_APP")]
pub app: Option<String>,
#[arg(
long,
global = true,
default_value = "text",
value_enum,
env = "XURL_OUTPUT"
)]
pub output: OutputFormat,
#[arg(
long,
global = true,
conflicts_with = "output",
conflicts_with = "jsonl",
env = "XURL_JSON",
value_parser = clap::builder::FalseyValueParser::new(),
action = clap::ArgAction::SetTrue,
)]
pub json: bool,
#[arg(
long,
global = true,
conflicts_with = "output",
conflicts_with = "json",
env = "XURL_JSONL",
value_parser = clap::builder::FalseyValueParser::new(),
action = clap::ArgAction::SetTrue,
)]
pub jsonl: bool,
#[arg(
long,
global = true,
env = "XURL_RAW",
value_parser = FalseyValueParser::new(),
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
require_equals = false,
)]
pub raw: bool,
#[arg(
long,
global = true,
env = "XURL_NO_PAGER",
value_parser = FalseyValueParser::new(),
action = clap::ArgAction::SetTrue,
)]
pub no_pager: bool,
#[arg(
long,
short = 'q',
global = true,
env = "XURL_QUIET",
value_parser = FalseyValueParser::new(),
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
require_equals = false,
)]
pub quiet: bool,
#[arg(
long,
global = true,
env = "XURL_NO_INTERACTIVE",
value_parser = FalseyValueParser::new(),
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
require_equals = false,
)]
pub no_interactive: bool,
#[arg(long, global = true, default_value = "30", env = "XURL_TIMEOUT")]
pub timeout: u64,
#[arg(
long,
global = true,
value_enum,
default_value_t = ColorChoice::Auto,
env = "XURL_COLOR"
)]
pub color: ColorChoice,
#[arg(
long = "dry-run",
global = true,
env = "XURL_DRY_RUN",
value_parser = FalseyValueParser::new(),
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
require_equals = false,
)]
pub dry_run: bool,
#[arg(long = "limit", global = true, env = "XURL_LIMIT")]
pub limit: Option<i32>,
#[arg(
long = "cursor",
global = true,
env = "XURL_CURSOR",
value_name = "TOKEN"
)]
pub cursor: Option<String>,
#[arg(
long = "page",
global = true,
env = "XURL_PAGE",
value_name = "N",
conflicts_with = "cursor"
)]
pub page: Option<String>,
#[arg(
long = "after",
global = true,
env = "XURL_AFTER",
value_name = "TOKEN",
conflicts_with = "cursor",
conflicts_with = "page"
)]
pub after: Option<String>,
#[command(subcommand)]
pub command: Option<Commands>,
pub url: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(after_help = POST_HELP)]
Post {
text: String,
#[arg(long = "media-id")]
media_ids: Vec<String>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = REPLY_HELP)]
Reply {
post_id: String,
text: String,
#[arg(long = "media-id")]
media_ids: Vec<String>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = QUOTE_HELP)]
Quote {
post_id: String,
text: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = DELETE_HELP)]
Delete {
post_id: String,
#[arg(long)]
force: bool,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = READ_HELP)]
Read {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = SEARCH_HELP)]
Search {
query: String,
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = WHOAMI_HELP)]
Whoami {
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = USER_HELP)]
User {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = TIMELINE_HELP)]
Timeline {
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = MENTIONS_HELP)]
Mentions {
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = LIKE_HELP)]
Like {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = UNLIKE_HELP)]
Unlike {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = REPOST_HELP)]
Repost {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = UNREPOST_HELP)]
Unrepost {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = BOOKMARK_HELP)]
Bookmark {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = UNBOOKMARK_HELP)]
Unbookmark {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = BOOKMARKS_HELP)]
Bookmarks {
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = LIKES_HELP)]
Likes {
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = FOLLOW_HELP)]
Follow {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = UNFOLLOW_HELP)]
Unfollow {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = FOLLOWING_HELP)]
Following {
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[arg(long = "of")]
of: Option<String>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = FOLLOWERS_HELP)]
Followers {
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[arg(long = "of")]
of: Option<String>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = MUTE_HELP)]
Mute {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = UNMUTE_HELP)]
Unmute {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = USAGE_HELP)]
Usage {
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = DM_HELP)]
Dm {
#[arg(value_name = "USERNAME")]
target_username: String,
text: String,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = DMS_HELP)]
Dms {
#[arg(short = 'n', long = "max-results")]
max_results: Option<i32>,
#[command(flatten)]
common: CommonFlags,
},
#[command(after_help = AUTH_HELP)]
Auth {
#[command(subcommand)]
command: AuthCommands,
},
#[command(after_help = MEDIA_HELP)]
Media {
#[command(subcommand)]
command: MediaCommands,
},
#[command(after_help = "Examples:
xr skill install claude_code # install bundle to Claude Code
xr skill install claude_code --dry-run # print the resolved git command without spawning
xr skill install --all # install across every known host
xr skill install codex --output json # JSON envelope for agent consumption
xr skill install --all --dry-run --output json # multi-host dry-run envelope")]
Skill {
#[command(subcommand)]
cmd: SkillCmd,
},
#[command(after_help = SCHEMA_HELP)]
Schema {
command: Option<String>,
#[arg(long)]
list: bool,
#[arg(long)]
all: bool,
#[arg(long)]
envelope: bool,
},
#[command(after_help = COMPLETIONS_HELP)]
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
#[command(after_help = VERSION_HELP)]
Version,
#[command(after_help = EXAMPLES_HELP)]
Examples,
#[command(after_help = VALIDATE_HELP)]
Validate {
#[arg(value_name = "FILE")]
file: Option<String>,
#[arg(long = "schema", value_name = "NAME")]
schema: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum SkillCmd {
#[command(after_help = "Examples:
xr skill install claude_code # install bundle to Claude Code
xr skill install claude_code --dry-run # print the resolved git command without spawning
xr skill install --all # install across every known host
xr skill install codex --output json # JSON envelope for agent consumption
xr skill install --all --dry-run --output json # multi-host dry-run envelope")]
Install {
host: Option<SkillHost>,
#[arg(long, conflicts_with = "host")]
all: bool,
#[arg(long)]
dry_run: bool,
},
#[command(after_help = "Examples:
xr skill update claude_code # refresh Claude Code's xurl-rs bundle
xr skill update claude_code --dry-run # show the resolved plan without touching disk
xr skill update --all # refresh every known host
xr skill update codex --output json # JSON envelope for agent consumption")]
Update {
host: Option<SkillHost>,
#[arg(long, conflicts_with = "host")]
all: bool,
#[arg(long)]
dry_run: bool,
},
}
impl Cli {
#[must_use]
pub fn effective_output(&self) -> OutputFormat {
if self.jsonl {
OutputFormat::Jsonl
} else if self.json {
OutputFormat::Json
} else {
self.output.clone()
}
}
}
#[derive(clap::Args, Debug, Clone)]
pub struct CommonFlags {
#[arg(long = "auth")]
pub auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
pub username: Option<String>,
#[arg(short = 't', long = "trace")]
pub trace: bool,
}
impl CommonFlags {
pub fn to_call_options(&self, verbose: bool, timeout_secs: u64) -> crate::api::CallOptions {
self.to_call_options_with_cursor(verbose, timeout_secs, None)
}
pub fn to_call_options_with_cursor(
&self,
verbose: bool,
timeout_secs: u64,
cursor: Option<&str>,
) -> crate::api::CallOptions {
crate::api::CallOptions {
auth_type: self.auth_type.clone().unwrap_or_default(),
username: self.username.clone().unwrap_or_default(),
no_auth: false,
verbose,
trace: self.trace,
timeout_secs,
pagination_token: cursor.unwrap_or_default().to_string(),
}
}
}
#[derive(Subcommand, Debug)]
pub enum AuthCommands {
#[command(after_help = AUTH_OAUTH2_HELP)]
Oauth2 {
#[arg(
long,
env = "XURL_NO_BROWSER",
value_parser = FalseyValueParser::new(),
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
require_equals = false,
)]
no_browser: bool,
#[arg(long, requires = "no_browser", value_parser = clap::value_parser!(u8).range(1..=2))]
step: Option<u8>,
#[arg(long = "auth-url", requires = "step")]
auth_url: Option<String>,
#[arg(value_name = "USERNAME")]
username: Option<String>,
},
#[command(after_help = AUTH_OAUTH1_HELP)]
Oauth1 {
#[arg(long = "consumer-key")]
consumer_key: String,
#[arg(long = "consumer-secret")]
consumer_secret: String,
#[arg(long = "access-token")]
access_token: String,
#[arg(long = "token-secret")]
token_secret: String,
},
#[command(after_help = AUTH_APP_HELP)]
App {
#[arg(long = "bearer-token")]
bearer_token: String,
},
#[command(after_help = AUTH_STATUS_HELP)]
Status,
#[command(after_help = AUTH_CLEAR_HELP)]
Clear {
#[arg(long)]
all: bool,
#[arg(long)]
oauth1: bool,
#[arg(long = "oauth2-username")]
oauth2_username: Option<String>,
#[arg(long)]
bearer: bool,
#[arg(long)]
force: bool,
},
#[command(after_help = AUTH_APPS_HELP)]
Apps {
#[command(subcommand)]
command: AppCommands,
},
#[command(after_help = AUTH_DEFAULT_HELP)]
Default {
app_name: Option<String>,
username: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum AppCommands {
#[command(after_help = APPS_ADD_HELP)]
Add {
name: String,
#[arg(long = "client-id")]
client_id: String,
#[arg(long = "client-secret")]
client_secret: String,
#[arg(long = "redirect-uri")]
redirect_uri: Option<String>,
},
#[command(after_help = APPS_UPDATE_HELP)]
Update {
name: String,
#[arg(long = "client-id")]
client_id: Option<String>,
#[arg(long = "client-secret")]
client_secret: Option<String>,
#[arg(long = "redirect-uri")]
redirect_uri: Option<String>,
},
#[command(after_help = APPS_REMOVE_HELP)]
Remove {
name: String,
#[arg(long)]
force: bool,
},
#[command(after_help = APPS_LIST_HELP)]
List,
#[command(after_help = APPS_REDIRECT_URI_HELP)]
RedirectUri {
#[command(subcommand)]
command: RedirectUriCommands,
},
}
#[derive(Subcommand, Debug)]
pub enum RedirectUriCommands {
#[command(after_help = REDIRECT_URI_GET_HELP)]
Get {
#[arg(value_name = "NAME")]
name: Option<String>,
},
#[command(after_help = REDIRECT_URI_SET_HELP)]
Set {
#[arg(value_name = "NAME")]
name: String,
#[arg(value_name = "URI")]
uri: String,
},
}
#[derive(Subcommand, Debug)]
pub enum MediaCommands {
#[command(after_help = MEDIA_UPLOAD_HELP)]
Upload {
file: String,
#[arg(long = "media-type", default_value = "video/mp4")]
media_type: String,
#[arg(long = "category", default_value = "amplify_video")]
category: String,
#[arg(long = "wait", default_value = "true")]
wait: bool,
#[arg(long = "auth")]
auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
username: Option<String>,
#[arg(short = 't', long = "trace")]
trace: bool,
#[arg(short = 'H', long = "header")]
headers: Vec<String>,
},
#[command(after_help = MEDIA_STATUS_HELP)]
Status {
media_id: String,
#[arg(long = "auth")]
auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
username: Option<String>,
#[arg(short = 'w', long = "wait")]
wait: bool,
#[arg(short = 't', long = "trace")]
trace: bool,
#[arg(short = 'H', long = "header")]
headers: Vec<String>,
},
}