1pub mod commands;
7pub mod exit_codes;
8pub mod runner;
9
10pub use runner::{run, run_argv, run_with_store_path};
11
12use clap::builder::FalseyValueParser;
13use clap::{Parser, Subcommand, ValueEnum};
14
15pub use crate::output::OutputFormat;
16pub use crate::skill_install::SkillHost;
17
18#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)]
21#[value(rename_all = "lower")]
22pub enum ColorChoice {
23 #[default]
25 Auto,
26 Always,
28 Never,
30}
31
32const ROOT_HELP: &str = "\
39Examples:
40 Authenticate (browser):
41 xr auth oauth2
42 Authenticate (headless / SSH / container):
43 xr auth oauth2 --no-browser --step 1
44 Post a status update (text vs JSON):
45 xr post \"Hello world\"
46 xr post \"Hello world\" --output json
47 Search recent posts with env-var precedence:
48 XURL_OUTPUT=json xr search \"rustlang\" -n 25
49 Browse the full curated gallery:
50 xr examples
51
52ENVIRONMENT VARIABLES:
53 XURL_OUTPUT Output format: text, json, jsonl, ndjson, yaml, csv, tsv (same as --output)
54 XURL_CURSOR Pagination cursor / page token (same as --cursor)
55 XURL_QUIET Suppress non-essential output (same as --quiet)
56 XURL_NO_INTERACTIVE Fail instead of prompting (same as --no-interactive)
57 XURL_TIMEOUT Network timeout in seconds (same as --timeout)
58 XURL_COLOR Color control: auto, always, never (same as --color)
59 XURL_VERBOSE Verbose request/response logging (same as -v/--verbose)
60 XURL_APP Override default app (same as --app)
61 XURL_JSON Shorthand for XURL_OUTPUT=json (same as --json)
62 XURL_JSONL Shorthand for XURL_OUTPUT=jsonl (same as --jsonl)
63 XURL_NO_BROWSER Skip browser-open on `auth oauth2` (same as --no-browser)
64 REDIRECT_URI OAuth2 redirect URI override for the active app
65
66Flags override env vars when both are set. NO_COLOR=1 always wins over
67--color/XURL_COLOR (https://no-color.org). --no-pager is a documented
68no-op so agents can pass it unconditionally; xr never invokes $PAGER.
69
70INPUT FROM STDIN:
71 Subcommands that accept JSON input (currently `xr validate`) read from
72 stdin when no file argument is given or when `-` is passed as the path.
73 This matches the standard CLI convention for piping data:
74 cat tweet.json | xr validate --schema tweet --output json
75 cat tweet.json | xr validate - --schema tweet --output json
76
77EXIT CODES:
78 0 success
79 1 general error
80 2 invalid arguments or authentication required
81 3 rate-limited (HTTP 429)
82 4 not found (HTTP 404)
83 5 network error
84
85TTY behavior:
86 When stdout is not a TTY, color output is auto-stripped (unless
87 --color always is set) and human-only banners are suppressed, so piping
88 to jaq or redirecting to a file produces clean machine-readable output
89 without any extra flags.
90";
91
92const POST_HELP: &str = "\
95Examples:
96 Post a status update (text):
97 xr post \"Hello world\"
98 Post the same update (machine-readable JSON envelope):
99 xr post \"Hello world\" --output json
100 Post with media (run `xr media upload` first to get an ID):
101 xr post \"Look at this\" --media-id 1234567890 --output json
102 Post quietly (suppress human-readable banners):
103 xr post \"Ship it\" --quiet --output json
104";
105
106const REPLY_HELP: &str = "\
108Examples:
109 Reply to a post by ID:
110 xr reply 1234567890 \"Congrats!\"
111 Reply with JSON output for scripting:
112 xr reply 1234567890 \"Congrats!\" --output json
113 Reply to a post URL (xr accepts either):
114 xr reply https://x.com/jack/status/1234567890 \"Nice thread.\"
115 Reply with a media attachment:
116 xr reply 1234567890 \"Here's a photo\" --media-id 222 --output json
117";
118
119const QUOTE_HELP: &str = "\
121Examples:
122 Quote-post by ID:
123 xr quote 1234567890 \"Worth a read.\"
124 Quote-post with JSON envelope:
125 xr quote 1234567890 \"Worth a read.\" --output json
126 Quote-post from a URL:
127 xr quote https://x.com/jack/status/1234567890 \"Thread.\"
128";
129
130const DELETE_HELP: &str = "\
132Examples:
133 Delete a post by ID (text):
134 xr delete 1234567890
135 Delete with JSON envelope:
136 xr delete 1234567890 --output json
137 Delete in a non-interactive context (CI, agent):
138 xr delete 1234567890 --no-interactive --output json
139";
140
141const READ_HELP: &str = "\
143Examples:
144 Read a post (text):
145 xr read 1234567890
146 Read a post (JSON):
147 xr read 1234567890 --output json
148 Read a post URL:
149 xr read https://x.com/jack/status/1234567890 --output json
150 Extract a single field via jaq:
151 xr read 1234567890 --output json | jaq '.data.text'
152";
153
154const SEARCH_HELP: &str = "\
156Examples:
157 Search recent posts (text):
158 xr search \"rustlang\"
159 Search with 25 results, JSON envelope:
160 xr search \"rustlang\" -n 25 --output json
161 Demonstrate env-var precedence (XURL_OUTPUT == --output):
162 XURL_OUTPUT=json xr search \"rustlang\"
163 Stream-friendly JSONL piped to jaq:
164 xr search \"rustlang\" --output jsonl | jaq '.id'
165";
166
167const WHOAMI_HELP: &str = "\
169Examples:
170 Show your profile (text):
171 xr whoami
172 Same, as a JSON envelope:
173 xr whoami --output json
174 Act as a specific authenticated user (multi-account):
175 xr whoami --username alice --output json
176";
177
178const USER_HELP: &str = "\
180Examples:
181 Look up a user by handle (text):
182 xr user jack
183 Same, as JSON:
184 xr user jack --output json
185 Use the @ prefix (xr accepts either):
186 xr user @jack --output json
187";
188
189const TIMELINE_HELP: &str = "\
191Examples:
192 Home timeline (last 10, text):
193 xr timeline
194 Home timeline (50 results, JSON envelope):
195 xr timeline -n 50 --output json
196 Stream-friendly JSONL piped to jaq:
197 xr timeline -n 100 --output jsonl | jaq '.id'
198";
199
200const MENTIONS_HELP: &str = "\
202Examples:
203 Your mentions (text):
204 xr mentions
205 Last 25, JSON envelope:
206 xr mentions -n 25 --output json
207 JSONL pipeline:
208 xr mentions -n 100 --output jsonl | jaq '.id'
209";
210
211const LIKE_HELP: &str = "\
213Examples:
214 Like a post (text):
215 xr like 1234567890
216 Like a post (JSON envelope):
217 xr like 1234567890 --output json
218 Idempotent: re-liking is a server-side no-op.
219";
220
221const UNLIKE_HELP: &str = "\
223Examples:
224 Unlike a post (text):
225 xr unlike 1234567890
226 Unlike (JSON envelope):
227 xr unlike 1234567890 --output json
228";
229
230const REPOST_HELP: &str = "\
232Examples:
233 Repost a post (text):
234 xr repost 1234567890
235 Repost (JSON envelope):
236 xr repost 1234567890 --output json
237";
238
239const UNREPOST_HELP: &str = "\
241Examples:
242 Undo a repost (text):
243 xr unrepost 1234567890
244 Undo a repost (JSON envelope):
245 xr unrepost 1234567890 --output json
246";
247
248const BOOKMARK_HELP: &str = "\
250Examples:
251 Bookmark a post (text):
252 xr bookmark 1234567890
253 Bookmark (JSON envelope):
254 xr bookmark 1234567890 --output json
255";
256
257const UNBOOKMARK_HELP: &str = "\
259Examples:
260 Remove a bookmark (text):
261 xr unbookmark 1234567890
262 Remove a bookmark (JSON envelope):
263 xr unbookmark 1234567890 --output json
264";
265
266const BOOKMARKS_HELP: &str = "\
268Examples:
269 List your bookmarks (text):
270 xr bookmarks
271 100 results, JSON envelope:
272 xr bookmarks -n 100 --output json
273 JSONL piped to jaq:
274 xr bookmarks -n 100 --output jsonl | jaq '.id'
275";
276
277const LIKES_HELP: &str = "\
279Examples:
280 List your liked posts (text):
281 xr likes
282 100 results, JSON envelope:
283 xr likes -n 100 --output json
284 JSONL piped to jaq:
285 xr likes -n 100 --output jsonl | jaq '.id'
286";
287
288const FOLLOW_HELP: &str = "\
290Examples:
291 Follow a user (text):
292 xr follow @jack
293 Follow (JSON envelope):
294 xr follow @jack --output json
295 Without the @ prefix:
296 xr follow jack --output json
297";
298
299const UNFOLLOW_HELP: &str = "\
301Examples:
302 Unfollow a user (text):
303 xr unfollow @jack
304 Unfollow (JSON envelope):
305 xr unfollow @jack --output json
306";
307
308const FOLLOWING_HELP: &str = "\
310Examples:
311 Users you follow (text):
312 xr following
313 100 results, JSON envelope:
314 xr following -n 100 --output json
315 Who someone else follows:
316 xr following --of @jack --output json
317";
318
319const FOLLOWERS_HELP: &str = "\
321Examples:
322 Your followers (text):
323 xr followers
324 100 results, JSON envelope:
325 xr followers -n 100 --output json
326 Someone else's followers:
327 xr followers --of @jack --output json
328";
329
330const MUTE_HELP: &str = "\
332Examples:
333 Mute a user (text):
334 xr mute @noisy
335 Mute (JSON envelope):
336 xr mute @noisy --output json
337";
338
339const UNMUTE_HELP: &str = "\
341Examples:
342 Unmute a user (text):
343 xr unmute @noisy
344 Unmute (JSON envelope):
345 xr unmute @noisy --output json
346";
347
348const USAGE_HELP: &str = "\
350Examples:
351 Show API usage (text):
352 xr usage
353 Show API usage (JSON envelope, machine-parseable cap data):
354 xr usage --output json
355 Quiet + JSON for clean agent consumption:
356 xr usage --quiet --output json
357";
358
359const DM_HELP: &str = "\
361Examples:
362 Send a DM (text):
363 xr dm @recipient \"Hello\"
364 Send a DM (JSON envelope):
365 xr dm @recipient \"Hello\" --output json
366 Without the @ prefix:
367 xr dm recipient \"Hi\" --output json
368";
369
370const DMS_HELP: &str = "\
372Examples:
373 List recent DM events (text):
374 xr dms
375 50 results, JSON envelope:
376 xr dms -n 50 --output json
377 JSONL piped to jaq:
378 xr dms -n 100 --output jsonl | jaq '.id'
379";
380
381const AUTH_HELP: &str = "\
383Examples:
384 Browser-based OAuth2 (default):
385 xr auth oauth2
386 Headless OAuth2 (servers, containers):
387 xr auth oauth2 --no-browser --step 1
388 Bearer token for read-only / search:
389 xr auth app --bearer-token \"$TOKEN\"
390 Show current auth state, machine-readable:
391 xr auth status --output json
392";
393
394const MEDIA_HELP: &str = "\
396Examples:
397 Upload an image:
398 xr media upload ./photo.png --media-type image/png --category tweet_image
399 Upload a video and wait for processing:
400 xr media upload ./clip.mp4 --wait --output json
401 Check upload status:
402 xr media status 1234567890 --output json
403";
404
405const SCHEMA_HELP: &str = "\
407Examples:
408 List all command schemas (text):
409 xr schema --list
410 List all command schemas (JSON):
411 xr schema --list --output json
412 Dump a single command's schema:
413 xr schema post --output json
414 Dump every schema as one JSON document:
415 xr schema --all --output json
416";
417
418const COMPLETIONS_HELP: &str = "\
420Examples:
421 Bash:
422 xr completions bash > ~/.bash_completion.d/xr
423 Zsh:
424 xr completions zsh > ~/.zfunc/_xr
425 Fish:
426 xr completions fish > ~/.config/fish/completions/xr.fish
427";
428
429const VERSION_HELP: &str = "\
431Examples:
432 Print the binary version (text):
433 xr version
434 Same, JSON-friendly via the top-level flag:
435 xr --version
436";
437
438const VALIDATE_HELP: &str = "\
440Examples:
441 Read JSON from stdin (no file argument):
442 cat tweet.json | xr validate --output json
443 Same, written as `xr validate -` for explicit stdin:
444 cat tweet.json | xr validate - --schema tweet --output json
445 Validate a file against a specific schema:
446 xr validate tweets.json --schema tweets --output json
447 Validate the canonical xurl error envelope:
448 echo '{\"status\":\"error\",\"reason\":\"x\",\"exit_code\":1,\"message\":\"y\"}' | xr validate --schema envelope --output json
449";
450
451const EXAMPLES_HELP: &str = "\
454Examples:
455 Print the full curated gallery:
456 xr examples
457 Discover env-var precedence and exit codes:
458 xr --help
459 Browse a single command's curated examples:
460 xr post --help
461";
462
463const AUTH_OAUTH2_HELP: &str = "\
465Examples:
466 Interactive browser flow (default):
467 xr auth oauth2
468 Headless step 1 (generate auth URL on a server / container):
469 xr auth oauth2 --no-browser --step 1
470 Headless step 2 (paste the redirect URL after authorizing):
471 xr auth oauth2 --no-browser --step 2 --auth-url 'https://localhost/callback?code=...&state=...'
472 Headless step 2 reading the URL from stdin (recommended on shared boxes):
473 echo 'https://localhost/callback?code=...&state=...' | xr auth oauth2 --no-browser --step 2 --auth-url - --output json
474 Label the saved token with a specific username (skips /2/users/me):
475 xr auth oauth2 alice --output json
476";
477
478const AUTH_OAUTH1_HELP: &str = "\
480Examples:
481 Configure OAuth1 from app + user credentials:
482 xr auth oauth1 \\
483 --consumer-key CK --consumer-secret CS \\
484 --access-token AT --token-secret TS
485 Same, with JSON envelope for scripted setup:
486 xr auth oauth1 --consumer-key CK --consumer-secret CS \\
487 --access-token AT --token-secret TS --output json
488";
489
490const AUTH_APP_HELP: &str = "\
492Examples:
493 Set the bearer token from an env var:
494 xr auth app --bearer-token \"$XURL_BEARER_TOKEN\"
495 Same, JSON envelope:
496 xr auth app --bearer-token \"$XURL_BEARER_TOKEN\" --output json
497 Test the configured bearer:
498 xr auth status --output json
499";
500
501const AUTH_STATUS_HELP: &str = "\
503Examples:
504 Show current auth state (text):
505 xr auth status
506 Same, machine-readable:
507 xr auth status --output json
508 Quiet + JSON for agent consumption:
509 xr auth status --quiet --output json
510";
511
512const AUTH_CLEAR_HELP: &str = "\
514Examples:
515 Clear all tokens (text):
516 xr auth clear --all
517 Clear only the bearer token (JSON envelope):
518 xr auth clear --bearer --output json
519 Clear a single OAuth2 user:
520 xr auth clear --oauth2-username alice --output json
521 Non-interactive, fail without an explicit selector:
522 xr auth clear --all --no-interactive --output json
523";
524
525const AUTH_APPS_HELP: &str = "\
527Examples:
528 Register a new app:
529 xr auth apps add my-app --client-id ID --client-secret SECRET
530 List registered apps (JSON):
531 xr auth apps list --output json
532 Update credentials for an existing app:
533 xr auth apps update my-app --client-secret NEW
534 Inspect or set the stored OAuth2 redirect URI:
535 xr auth apps redirect-uri get my-app --output json
536";
537
538const AUTH_DEFAULT_HELP: &str = "\
540Examples:
541 Interactive picker (TTY):
542 xr auth default
543 Set default by name:
544 xr auth default my-app
545 Set default app + username together:
546 xr auth default my-app alice
547 Non-interactive fail-fast if no name is supplied:
548 xr auth default --no-interactive --output json
549";
550
551const APPS_ADD_HELP: &str = "\
553Examples:
554 Register a new app (text):
555 xr auth apps add my-app --client-id ID --client-secret SECRET
556 Register with a custom redirect URI:
557 xr auth apps add my-app --client-id ID --client-secret SECRET \\
558 --redirect-uri https://localhost:8443/callback
559 Register, JSON envelope for scripted setup:
560 xr auth apps add my-app --client-id ID --client-secret SECRET --output json
561";
562
563const APPS_UPDATE_HELP: &str = "\
565Examples:
566 Rotate the client secret:
567 xr auth apps update my-app --client-secret NEW
568 Update the redirect URI:
569 xr auth apps update my-app --redirect-uri https://localhost:8443/callback
570 Clear the stored redirect URI (pass empty string):
571 xr auth apps update my-app --redirect-uri \"\"
572 Same, JSON envelope:
573 xr auth apps update my-app --client-secret NEW --output json
574";
575
576const APPS_REMOVE_HELP: &str = "\
578Examples:
579 Remove a registered app (text):
580 xr auth apps remove my-app
581 Remove (JSON envelope):
582 xr auth apps remove my-app --output json
583 Non-interactive removal in CI:
584 xr auth apps remove my-app --no-interactive --output json
585";
586
587const APPS_LIST_HELP: &str = "\
589Examples:
590 List registered apps (text):
591 xr auth apps list
592 List (JSON envelope):
593 xr auth apps list --output json
594 Quiet + JSON for clean agent consumption:
595 xr auth apps list --quiet --output json
596";
597
598const APPS_REDIRECT_URI_HELP: &str = "\
600Examples:
601 Show the effective redirect URI and its source:
602 xr auth apps redirect-uri get my-app --output json
603 Set the stored redirect URI:
604 xr auth apps redirect-uri set my-app https://localhost:8443/callback
605 Clear the stored redirect URI (empty value):
606 xr auth apps redirect-uri set my-app \"\"
607";
608
609const REDIRECT_URI_GET_HELP: &str = "\
611Examples:
612 Show the effective URI for the default app (text):
613 xr auth apps redirect-uri get
614 Show for a specific app (JSON envelope):
615 xr auth apps redirect-uri get my-app --output json
616 Compare env-var override vs stored value:
617 REDIRECT_URI=https://example/callback xr auth apps redirect-uri get my-app --output json
618";
619
620const REDIRECT_URI_SET_HELP: &str = "\
622Examples:
623 Set a custom redirect URI:
624 xr auth apps redirect-uri set my-app https://localhost:8443/callback
625 Same, JSON envelope:
626 xr auth apps redirect-uri set my-app https://localhost:8443/callback --output json
627 Clear the stored URI (empty string):
628 xr auth apps redirect-uri set my-app \"\"
629";
630
631const MEDIA_UPLOAD_HELP: &str = "\
633Examples:
634 Upload an image (text):
635 xr media upload ./photo.png --media-type image/png --category tweet_image
636 Upload a video and wait for processing (JSON envelope):
637 xr media upload ./clip.mp4 --wait --output json
638 Upload using a specific auth method:
639 xr media upload ./photo.png --auth oauth2 --output json
640 Skip waiting (returns immediately after FINALIZE):
641 xr media upload ./clip.mp4 --wait false --output json
642";
643
644const MEDIA_STATUS_HELP: &str = "\
646Examples:
647 Check upload status (text):
648 xr media status 1234567890
649 Check status (JSON envelope):
650 xr media status 1234567890 --output json
651 Poll until processing completes:
652 xr media status 1234567890 --wait --output json
653";
654
655#[derive(Parser, Debug)]
657#[command(
658 name = "xr",
659 about = "Auth enabled curl-like interface for the X API",
660 long_about = r#"A command-line tool for making authenticated requests to the X API.
661
662Shortcut commands (agent-friendly):
663 xr post "Hello world!" Post to X
664 xr reply 1234567890 "Nice!" Reply to a post
665 xr read 1234567890 Read a post
666 xr search "golang" -n 20 Search posts
667 xr whoami Show your profile
668 xr like 1234567890 Like a post
669 xr repost 1234567890 Repost
670 xr follow @user Follow a user
671 xr dm @user "Hey!" Send a DM
672 xr timeline Home timeline
673 xr mentions Your mentions
674
675Raw API access (curl-style):
676 basic requests xr /2/users/me
677 xr -X POST /2/tweets -d '{"text":"Hello world!"}'
678 xr -H "Content-Type: application/json" /2/tweets
679 authentication xr --auth oauth2 /2/users/me
680 xr --auth oauth1 /2/users/me
681 xr --auth app /2/users/me
682 media and streaming xr media upload path/to/video.mp4
683 xr /2/tweets/search/stream --auth app
684 xr -s /2/users/me
685
686Multi-app management:
687 xr auth apps add my-app --client-id ... --client-secret ...
688 xr auth apps list
689 xr auth default # interactive picker
690 xr auth default my-app # set by name
691 xr --app my-app /2/users/me # per-request override
692
693Shell completions:
694 xr completions bash > ~/.bash_completion.d/xr
695 xr completions zsh > ~/.zfunc/_xr
696 xr completions fish > ~/.config/fish/completions/xr.fish
697
698Run 'xr --help' to see all available commands."#,
699 after_help = ROOT_HELP,
700 version
701)]
702pub struct Cli {
703 #[arg(short = 'X', long = "method", global = false)]
705 pub method: Option<String>,
706
707 #[arg(short = 'H', long = "header")]
709 pub headers: Vec<String>,
710
711 #[arg(short = 'd', long = "data")]
713 pub data: Option<String>,
714
715 #[arg(long = "auth")]
717 pub auth_type: Option<String>,
718
719 #[arg(short = 'u', long = "username")]
721 pub username: Option<String>,
722
723 #[arg(
725 short = 'v',
726 long = "verbose",
727 global = true,
728 env = "XURL_VERBOSE",
729 value_parser = FalseyValueParser::new(),
730 num_args = 0..=1,
731 default_value_t = false,
732 default_missing_value = "true",
733 require_equals = false,
734 )]
735 pub verbose: bool,
736
737 #[arg(short = 't', long = "trace")]
739 pub trace: bool,
740
741 #[arg(short = 's', long = "stream")]
743 pub stream: bool,
744
745 #[arg(short = 'F', long = "file")]
747 pub file: Option<String>,
748
749 #[arg(long = "app", global = true, env = "XURL_APP")]
751 pub app: Option<String>,
752
753 #[arg(
758 long,
759 global = true,
760 default_value = "text",
761 value_enum,
762 env = "XURL_OUTPUT"
763 )]
764 pub output: OutputFormat,
765
766 #[arg(
768 long,
769 global = true,
770 conflicts_with = "output",
771 conflicts_with = "jsonl",
772 env = "XURL_JSON",
773 value_parser = clap::builder::FalseyValueParser::new(),
774 action = clap::ArgAction::SetTrue,
775 )]
776 pub json: bool,
777
778 #[arg(
780 long,
781 global = true,
782 conflicts_with = "output",
783 conflicts_with = "json",
784 env = "XURL_JSONL",
785 value_parser = clap::builder::FalseyValueParser::new(),
786 action = clap::ArgAction::SetTrue,
787 )]
788 pub jsonl: bool,
789
790 #[arg(
793 long,
794 global = true,
795 env = "XURL_RAW",
796 value_parser = FalseyValueParser::new(),
797 num_args = 0..=1,
798 default_value_t = false,
799 default_missing_value = "true",
800 require_equals = false,
801 )]
802 pub raw: bool,
803
804 #[arg(
808 long,
809 global = true,
810 env = "XURL_NO_PAGER",
811 value_parser = FalseyValueParser::new(),
812 action = clap::ArgAction::SetTrue,
813 )]
814 pub no_pager: bool,
815
816 #[arg(
818 long,
819 short = 'q',
820 global = true,
821 env = "XURL_QUIET",
822 value_parser = FalseyValueParser::new(),
823 num_args = 0..=1,
824 default_value_t = false,
825 default_missing_value = "true",
826 require_equals = false,
827 )]
828 pub quiet: bool,
829
830 #[arg(
832 long,
833 global = true,
834 env = "XURL_NO_INTERACTIVE",
835 value_parser = FalseyValueParser::new(),
836 num_args = 0..=1,
837 default_value_t = false,
838 default_missing_value = "true",
839 require_equals = false,
840 )]
841 pub no_interactive: bool,
842
843 #[arg(long, global = true, default_value = "30", env = "XURL_TIMEOUT")]
845 pub timeout: u64,
846
847 #[arg(
849 long,
850 global = true,
851 value_enum,
852 default_value_t = ColorChoice::Auto,
853 env = "XURL_COLOR"
854 )]
855 pub color: ColorChoice,
856
857 #[arg(
863 long = "dry-run",
864 global = true,
865 env = "XURL_DRY_RUN",
866 value_parser = FalseyValueParser::new(),
867 num_args = 0..=1,
868 default_value_t = false,
869 default_missing_value = "true",
870 require_equals = false,
871 )]
872 pub dry_run: bool,
873
874 #[arg(long = "limit", global = true, env = "XURL_LIMIT")]
879 pub limit: Option<i32>,
880
881 #[arg(
890 long = "cursor",
891 global = true,
892 env = "XURL_CURSOR",
893 value_name = "TOKEN"
894 )]
895 pub cursor: Option<String>,
896
897 #[arg(
905 long = "page",
906 global = true,
907 env = "XURL_PAGE",
908 value_name = "N",
909 conflicts_with = "cursor"
910 )]
911 pub page: Option<String>,
912
913 #[arg(
919 long = "after",
920 global = true,
921 env = "XURL_AFTER",
922 value_name = "TOKEN",
923 conflicts_with = "cursor",
924 conflicts_with = "page"
925 )]
926 pub after: Option<String>,
927
928 #[command(subcommand)]
930 pub command: Option<Commands>,
931
932 pub url: Option<String>,
934}
935
936#[derive(Subcommand, Debug)]
938pub enum Commands {
939 #[command(after_help = POST_HELP)]
942 Post {
943 text: String,
945 #[arg(long = "media-id")]
947 media_ids: Vec<String>,
948 #[command(flatten)]
950 common: CommonFlags,
951 },
952 #[command(after_help = REPLY_HELP)]
954 Reply {
955 post_id: String,
957 text: String,
959 #[arg(long = "media-id")]
961 media_ids: Vec<String>,
962 #[command(flatten)]
964 common: CommonFlags,
965 },
966 #[command(after_help = QUOTE_HELP)]
968 Quote {
969 post_id: String,
971 text: String,
973 #[command(flatten)]
975 common: CommonFlags,
976 },
977 #[command(after_help = DELETE_HELP)]
979 Delete {
980 post_id: String,
982 #[arg(long)]
984 force: bool,
985 #[command(flatten)]
987 common: CommonFlags,
988 },
989
990 #[command(after_help = READ_HELP)]
993 Read {
994 post_id: String,
996 #[command(flatten)]
998 common: CommonFlags,
999 },
1000 #[command(after_help = SEARCH_HELP)]
1002 Search {
1003 query: String,
1005 #[arg(short = 'n', long = "max-results")]
1007 max_results: Option<i32>,
1008 #[command(flatten)]
1010 common: CommonFlags,
1011 },
1012
1013 #[command(after_help = WHOAMI_HELP)]
1016 Whoami {
1017 #[command(flatten)]
1019 common: CommonFlags,
1020 },
1021 #[command(after_help = USER_HELP)]
1023 User {
1024 #[arg(value_name = "USERNAME")]
1026 target_username: String,
1027 #[command(flatten)]
1029 common: CommonFlags,
1030 },
1031
1032 #[command(after_help = TIMELINE_HELP)]
1035 Timeline {
1036 #[arg(short = 'n', long = "max-results")]
1038 max_results: Option<i32>,
1039 #[command(flatten)]
1041 common: CommonFlags,
1042 },
1043 #[command(after_help = MENTIONS_HELP)]
1045 Mentions {
1046 #[arg(short = 'n', long = "max-results")]
1048 max_results: Option<i32>,
1049 #[command(flatten)]
1051 common: CommonFlags,
1052 },
1053
1054 #[command(after_help = LIKE_HELP)]
1057 Like {
1058 post_id: String,
1060 #[command(flatten)]
1062 common: CommonFlags,
1063 },
1064 #[command(after_help = UNLIKE_HELP)]
1066 Unlike {
1067 post_id: String,
1069 #[command(flatten)]
1071 common: CommonFlags,
1072 },
1073 #[command(after_help = REPOST_HELP)]
1075 Repost {
1076 post_id: String,
1078 #[command(flatten)]
1080 common: CommonFlags,
1081 },
1082 #[command(after_help = UNREPOST_HELP)]
1084 Unrepost {
1085 post_id: String,
1087 #[command(flatten)]
1089 common: CommonFlags,
1090 },
1091 #[command(after_help = BOOKMARK_HELP)]
1093 Bookmark {
1094 post_id: String,
1096 #[command(flatten)]
1098 common: CommonFlags,
1099 },
1100 #[command(after_help = UNBOOKMARK_HELP)]
1102 Unbookmark {
1103 post_id: String,
1105 #[command(flatten)]
1107 common: CommonFlags,
1108 },
1109 #[command(after_help = BOOKMARKS_HELP)]
1111 Bookmarks {
1112 #[arg(short = 'n', long = "max-results")]
1114 max_results: Option<i32>,
1115 #[command(flatten)]
1117 common: CommonFlags,
1118 },
1119 #[command(after_help = LIKES_HELP)]
1121 Likes {
1122 #[arg(short = 'n', long = "max-results")]
1124 max_results: Option<i32>,
1125 #[command(flatten)]
1127 common: CommonFlags,
1128 },
1129
1130 #[command(after_help = FOLLOW_HELP)]
1133 Follow {
1134 #[arg(value_name = "USERNAME")]
1136 target_username: String,
1137 #[command(flatten)]
1139 common: CommonFlags,
1140 },
1141 #[command(after_help = UNFOLLOW_HELP)]
1143 Unfollow {
1144 #[arg(value_name = "USERNAME")]
1146 target_username: String,
1147 #[command(flatten)]
1149 common: CommonFlags,
1150 },
1151 #[command(after_help = FOLLOWING_HELP)]
1153 Following {
1154 #[arg(short = 'n', long = "max-results")]
1156 max_results: Option<i32>,
1157 #[arg(long = "of")]
1159 of: Option<String>,
1160 #[command(flatten)]
1162 common: CommonFlags,
1163 },
1164 #[command(after_help = FOLLOWERS_HELP)]
1166 Followers {
1167 #[arg(short = 'n', long = "max-results")]
1169 max_results: Option<i32>,
1170 #[arg(long = "of")]
1172 of: Option<String>,
1173 #[command(flatten)]
1175 common: CommonFlags,
1176 },
1177 #[command(after_help = MUTE_HELP)]
1179 Mute {
1180 #[arg(value_name = "USERNAME")]
1182 target_username: String,
1183 #[command(flatten)]
1185 common: CommonFlags,
1186 },
1187 #[command(after_help = UNMUTE_HELP)]
1189 Unmute {
1190 #[arg(value_name = "USERNAME")]
1192 target_username: String,
1193 #[command(flatten)]
1195 common: CommonFlags,
1196 },
1197
1198 #[command(after_help = USAGE_HELP)]
1201 Usage {
1202 #[command(flatten)]
1204 common: CommonFlags,
1205 },
1206
1207 #[command(after_help = DM_HELP)]
1210 Dm {
1211 #[arg(value_name = "USERNAME")]
1213 target_username: String,
1214 text: String,
1216 #[command(flatten)]
1218 common: CommonFlags,
1219 },
1220 #[command(after_help = DMS_HELP)]
1222 Dms {
1223 #[arg(short = 'n', long = "max-results")]
1225 max_results: Option<i32>,
1226 #[command(flatten)]
1228 common: CommonFlags,
1229 },
1230
1231 #[command(after_help = AUTH_HELP)]
1234 Auth {
1235 #[command(subcommand)]
1237 command: AuthCommands,
1238 },
1239
1240 #[command(after_help = MEDIA_HELP)]
1243 Media {
1244 #[command(subcommand)]
1246 command: MediaCommands,
1247 },
1248
1249 #[command(after_help = "Examples:
1256 xr skill install claude_code # install bundle to Claude Code
1257 xr skill install claude_code --dry-run # print the resolved git command without spawning
1258 xr skill install --all # install across every known host
1259 xr skill install codex --output json # JSON envelope for agent consumption
1260 xr skill install --all --dry-run --output json # multi-host dry-run envelope")]
1261 Skill {
1262 #[command(subcommand)]
1264 cmd: SkillCmd,
1265 },
1266
1267 #[command(after_help = SCHEMA_HELP)]
1270 Schema {
1271 command: Option<String>,
1273 #[arg(long)]
1275 list: bool,
1276 #[arg(long)]
1278 all: bool,
1279 #[arg(long)]
1281 envelope: bool,
1282 },
1283
1284 #[command(after_help = COMPLETIONS_HELP)]
1286 Completions {
1287 #[arg(value_enum)]
1289 shell: clap_complete::Shell,
1290 },
1291 #[command(after_help = VERSION_HELP)]
1293 Version,
1294
1295 #[command(after_help = EXAMPLES_HELP)]
1297 Examples,
1298
1299 #[command(after_help = VALIDATE_HELP)]
1308 Validate {
1309 #[arg(value_name = "FILE")]
1311 file: Option<String>,
1312
1313 #[arg(long = "schema", value_name = "NAME")]
1316 schema: Option<String>,
1317 },
1318}
1319
1320#[derive(Subcommand, Debug)]
1322pub enum SkillCmd {
1323 #[command(after_help = "Examples:
1329 xr skill install claude_code # install bundle to Claude Code
1330 xr skill install claude_code --dry-run # print the resolved git command without spawning
1331 xr skill install --all # install across every known host
1332 xr skill install codex --output json # JSON envelope for agent consumption
1333 xr skill install --all --dry-run --output json # multi-host dry-run envelope")]
1334 Install {
1335 host: Option<SkillHost>,
1337
1338 #[arg(long, conflicts_with = "host")]
1340 all: bool,
1341
1342 #[arg(long)]
1344 dry_run: bool,
1345 },
1346
1347 #[command(after_help = "Examples:
1354 xr skill update claude_code # refresh Claude Code's xurl-rs bundle
1355 xr skill update claude_code --dry-run # show the resolved plan without touching disk
1356 xr skill update --all # refresh every known host
1357 xr skill update codex --output json # JSON envelope for agent consumption")]
1358 Update {
1359 host: Option<SkillHost>,
1361
1362 #[arg(long, conflicts_with = "host")]
1364 all: bool,
1365
1366 #[arg(long)]
1368 dry_run: bool,
1369 },
1370}
1371
1372impl Cli {
1373 #[must_use]
1380 pub fn effective_output(&self) -> OutputFormat {
1381 if self.jsonl {
1382 OutputFormat::Jsonl
1383 } else if self.json {
1384 OutputFormat::Json
1385 } else {
1386 self.output.clone()
1387 }
1388 }
1389}
1390
1391#[derive(clap::Args, Debug, Clone)]
1397pub struct CommonFlags {
1398 #[arg(long = "auth")]
1400 pub auth_type: Option<String>,
1401
1402 #[arg(short = 'u', long = "username")]
1404 pub username: Option<String>,
1405
1406 #[arg(short = 't', long = "trace")]
1408 pub trace: bool,
1409}
1410
1411impl CommonFlags {
1412 pub fn to_call_options(&self, verbose: bool, timeout_secs: u64) -> crate::api::CallOptions {
1418 self.to_call_options_with_cursor(verbose, timeout_secs, None)
1419 }
1420
1421 pub fn to_call_options_with_cursor(
1428 &self,
1429 verbose: bool,
1430 timeout_secs: u64,
1431 cursor: Option<&str>,
1432 ) -> crate::api::CallOptions {
1433 crate::api::CallOptions {
1434 auth_type: self.auth_type.clone().unwrap_or_default(),
1435 username: self.username.clone().unwrap_or_default(),
1436 no_auth: false,
1437 verbose,
1438 trace: self.trace,
1439 timeout_secs,
1440 pagination_token: cursor.unwrap_or_default().to_string(),
1441 }
1442 }
1443}
1444
1445#[derive(Subcommand, Debug)]
1447pub enum AuthCommands {
1448 #[command(after_help = AUTH_OAUTH2_HELP)]
1450 Oauth2 {
1451 #[arg(
1461 long,
1462 env = "XURL_NO_BROWSER",
1463 value_parser = FalseyValueParser::new(),
1464 num_args = 0..=1,
1465 default_value_t = false,
1466 default_missing_value = "true",
1467 require_equals = false,
1468 )]
1469 no_browser: bool,
1470 #[arg(long, requires = "no_browser", value_parser = clap::value_parser!(u8).range(1..=2))]
1472 step: Option<u8>,
1473 #[arg(long = "auth-url", requires = "step")]
1475 auth_url: Option<String>,
1476 #[arg(value_name = "USERNAME")]
1478 username: Option<String>,
1479 },
1480 #[command(after_help = AUTH_OAUTH1_HELP)]
1482 Oauth1 {
1483 #[arg(long = "consumer-key")]
1485 consumer_key: String,
1486 #[arg(long = "consumer-secret")]
1488 consumer_secret: String,
1489 #[arg(long = "access-token")]
1491 access_token: String,
1492 #[arg(long = "token-secret")]
1494 token_secret: String,
1495 },
1496 #[command(after_help = AUTH_APP_HELP)]
1498 App {
1499 #[arg(long = "bearer-token")]
1501 bearer_token: String,
1502 },
1503 #[command(after_help = AUTH_STATUS_HELP)]
1505 Status,
1506 #[command(after_help = AUTH_CLEAR_HELP)]
1508 Clear {
1509 #[arg(long)]
1511 all: bool,
1512 #[arg(long)]
1514 oauth1: bool,
1515 #[arg(long = "oauth2-username")]
1517 oauth2_username: Option<String>,
1518 #[arg(long)]
1520 bearer: bool,
1521 #[arg(long)]
1523 force: bool,
1524 },
1525 #[command(after_help = AUTH_APPS_HELP)]
1527 Apps {
1528 #[command(subcommand)]
1530 command: AppCommands,
1531 },
1532 #[command(after_help = AUTH_DEFAULT_HELP)]
1534 Default {
1535 app_name: Option<String>,
1537 username: Option<String>,
1539 },
1540}
1541
1542#[derive(Subcommand, Debug)]
1544pub enum AppCommands {
1545 #[command(after_help = APPS_ADD_HELP)]
1547 Add {
1548 name: String,
1550 #[arg(long = "client-id")]
1552 client_id: String,
1553 #[arg(long = "client-secret")]
1555 client_secret: String,
1556 #[arg(long = "redirect-uri")]
1558 redirect_uri: Option<String>,
1559 },
1560 #[command(after_help = APPS_UPDATE_HELP)]
1562 Update {
1563 name: String,
1565 #[arg(long = "client-id")]
1567 client_id: Option<String>,
1568 #[arg(long = "client-secret")]
1570 client_secret: Option<String>,
1571 #[arg(long = "redirect-uri")]
1573 redirect_uri: Option<String>,
1574 },
1575 #[command(after_help = APPS_REMOVE_HELP)]
1577 Remove {
1578 name: String,
1580 #[arg(long)]
1582 force: bool,
1583 },
1584 #[command(after_help = APPS_LIST_HELP)]
1586 List,
1587 #[command(after_help = APPS_REDIRECT_URI_HELP)]
1589 RedirectUri {
1590 #[command(subcommand)]
1592 command: RedirectUriCommands,
1593 },
1594}
1595
1596#[derive(Subcommand, Debug)]
1598pub enum RedirectUriCommands {
1599 #[command(after_help = REDIRECT_URI_GET_HELP)]
1601 Get {
1602 #[arg(value_name = "NAME")]
1604 name: Option<String>,
1605 },
1606 #[command(after_help = REDIRECT_URI_SET_HELP)]
1608 Set {
1609 #[arg(value_name = "NAME")]
1611 name: String,
1612 #[arg(value_name = "URI")]
1614 uri: String,
1615 },
1616}
1617
1618#[derive(Subcommand, Debug)]
1620pub enum MediaCommands {
1621 #[command(after_help = MEDIA_UPLOAD_HELP)]
1623 Upload {
1624 file: String,
1626 #[arg(long = "media-type", default_value = "video/mp4")]
1628 media_type: String,
1629 #[arg(long = "category", default_value = "amplify_video")]
1631 category: String,
1632 #[arg(long = "wait", default_value = "true")]
1634 wait: bool,
1635 #[arg(long = "auth")]
1637 auth_type: Option<String>,
1638 #[arg(short = 'u', long = "username")]
1640 username: Option<String>,
1641 #[arg(short = 't', long = "trace")]
1643 trace: bool,
1644 #[arg(short = 'H', long = "header")]
1646 headers: Vec<String>,
1647 },
1648 #[command(after_help = MEDIA_STATUS_HELP)]
1650 Status {
1651 media_id: String,
1653 #[arg(long = "auth")]
1655 auth_type: Option<String>,
1656 #[arg(short = 'u', long = "username")]
1658 username: Option<String>,
1659 #[arg(short = 'w', long = "wait")]
1661 wait: bool,
1662 #[arg(short = 't', long = "trace")]
1664 trace: bool,
1665 #[arg(short = 'H', long = "header")]
1667 headers: Vec<String>,
1668 },
1669}