Skip to main content

xurl/cli/
mod.rs

1//! CLI definition — clap derive with subcommands.
2//!
3//! Mirrors the Go cobra command tree: root (raw mode) + shortcuts +
4//! auth/media/webhook/version subcommands.
5
6pub 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/// Color output choice. Honored by `OutputConfig` together with `NO_COLOR`
19/// and TTY detection.
20#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)]
21#[value(rename_all = "lower")]
22pub enum ColorChoice {
23    /// Enable color when stderr is a TTY and `NO_COLOR` is unset.
24    #[default]
25    Auto,
26    /// Always emit ANSI color escapes (still suppressed by `NO_COLOR`).
27    Always,
28    /// Never emit ANSI color escapes.
29    Never,
30}
31
32/// Root `--help` appendix listing the agentic-flag matrix, env-var
33/// equivalents, exit-code contract, and TTY-aware auto-quiet behavior.
34///
35/// Every env var the binary reads at the root level appears here so agents
36/// can discover the agentic surface from `xr --help` alone (corpus doc:
37/// `cli-env-vars-must-appear-in-help-2026-04-20.md`).
38const 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
92/// `xr post` examples — text + JSON paired, plus a reply variant and a
93/// media-attached form.
94const 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
106/// `xr reply` examples — anchor by post ID, paired text + JSON.
107const 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
119/// `xr quote` examples — paired text + JSON.
120const 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
130/// `xr delete` examples — destructive op; advertise non-interactive shape.
131const 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
141/// `xr read` examples — paired text + JSON, plus a pipe-to-jaq invocation.
142const 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
154/// `xr search` examples — text + JSON, env-var override, JSONL pipeline.
155const 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
167/// `xr whoami` examples — paired text + JSON.
168const 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
178/// `xr user` examples — paired text + JSON.
179const 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
189/// `xr timeline` examples — paired text + JSON, plus JSONL pipeline.
190const 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
200/// `xr mentions` examples — paired text + JSON.
201const 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
211/// `xr like` examples — paired text + JSON.
212const 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
221/// `xr unlike` examples — paired text + JSON.
222const 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
230/// `xr repost` examples — paired text + JSON.
231const 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
239/// `xr unrepost` examples — paired text + JSON.
240const 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
248/// `xr bookmark` examples — paired text + JSON.
249const 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
257/// `xr unbookmark` examples — paired text + JSON.
258const 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
266/// `xr bookmarks` examples — paired text + JSON, JSONL pipeline.
267const 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
277/// `xr likes` examples — paired text + JSON, JSONL pipeline.
278const 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
288/// `xr follow` examples — paired text + JSON.
289const 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
299/// `xr unfollow` examples — paired text + JSON.
300const 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
308/// `xr following` examples — paired text + JSON, `--of` for another user.
309const 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
319/// `xr followers` examples — paired text + JSON, `--of` for another user.
320const 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
330/// `xr mute` examples — paired text + JSON.
331const 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
339/// `xr unmute` examples — paired text + JSON.
340const 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
348/// `xr usage` examples — paired text + JSON.
349const 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
359/// `xr dm` examples — paired text + JSON.
360const 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
370/// `xr dms` examples — paired text + JSON, JSONL pipeline.
371const 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
381/// `xr auth` parent help — points to subcommands.
382const 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
394/// `xr media` parent help — points to subcommands.
395const 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
405/// `xr schema` examples — paired text + JSON, list, all.
406const 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
418/// `xr completions` examples.
419const 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
429/// `xr version` examples.
430const 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
438/// `xr validate` — JSON-shape validation against bundled response schemas.
439const 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
451/// `xr examples` advertises itself — paired text + JSON for the top-level
452/// flag round-trip.
453const 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
463/// `xr auth oauth2` — browser + headless flows.
464const 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
478/// `xr auth oauth1` — non-interactive OAuth1 setup.
479const 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
490/// `xr auth app` — bearer-token configuration.
491const 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
501/// `xr auth status` — paired text + JSON.
502const 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
512/// `xr auth clear` — destructive op; advertise non-interactive shape.
513const 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
525/// `xr auth apps` parent help — points to subcommands.
526const 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
538/// `xr auth default` — paired text + JSON.
539const 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
551/// `xr auth apps add` examples.
552const 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
563/// `xr auth apps update` examples.
564const 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
576/// `xr auth apps remove` — destructive op; advertise non-interactive shape.
577const 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
587/// `xr auth apps list` — paired text + JSON.
588const 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
598/// `xr auth apps redirect-uri` parent help.
599const 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
609/// `xr auth apps redirect-uri get` examples.
610const 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
620/// `xr auth apps redirect-uri set` examples.
621const 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
631/// `xr media upload` examples.
632const 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
644/// `xr media status` examples.
645const 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/// Auth-enabled curl-like interface for the X API.
656#[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    /// HTTP method (GET by default)
704    #[arg(short = 'X', long = "method", global = false)]
705    pub method: Option<String>,
706
707    /// Request headers
708    #[arg(short = 'H', long = "header")]
709    pub headers: Vec<String>,
710
711    /// Request body data
712    #[arg(short = 'd', long = "data")]
713    pub data: Option<String>,
714
715    /// Authentication type (oauth1, oauth2, app)
716    #[arg(long = "auth")]
717    pub auth_type: Option<String>,
718
719    /// Username for `OAuth2` authentication
720    #[arg(short = 'u', long = "username")]
721    pub username: Option<String>,
722
723    /// Print verbose information
724    #[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    /// Add trace header to request
738    #[arg(short = 't', long = "trace")]
739    pub trace: bool,
740
741    /// Force streaming mode
742    #[arg(short = 's', long = "stream")]
743    pub stream: bool,
744
745    /// File to upload (for multipart requests)
746    #[arg(short = 'F', long = "file")]
747    pub file: Option<String>,
748
749    /// Use a specific registered app (overrides default)
750    #[arg(long = "app", global = true, env = "XURL_APP")]
751    pub app: Option<String>,
752
753    /// Output format. text (default), json, jsonl, ndjson (alias of jsonl),
754    /// yaml (`.yml`), csv, tsv. Formats not in the value enum (e.g. toml,
755    /// xml) are not supported — xurl emits a JSON envelope with reason
756    /// `invalid-args` if requested.
757    #[arg(
758        long,
759        global = true,
760        default_value = "text",
761        value_enum,
762        env = "XURL_OUTPUT"
763    )]
764    pub output: OutputFormat,
765
766    /// Shorthand for `--output json` (P2 alias).
767    #[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    /// Shorthand for `--output jsonl` (P2 alias).
779    #[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    /// Emit unstyled, compact output. Strips ANSI in text mode; compact (no
791    /// pretty-printing) JSON in json/jsonl modes.
792    #[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    /// Documented no-op. `xr` writes directly to stdout and never invokes
805    /// `$PAGER`; this flag is advertised so agents can pass `--no-pager`
806    /// unconditionally without xr rejecting it.
807    #[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    /// Suppress all non-essential output (errors still go to stderr)
817    #[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    /// Disable interactive prompts; fail with error instead
831    #[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    /// Request timeout in seconds
844    #[arg(long, global = true, default_value = "30", env = "XURL_TIMEOUT")]
845    pub timeout: u64,
846
847    /// Colorize output: auto (TTY-aware), always, or never
848    #[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    /// Validate inputs and skip the API call (U7).
858    ///
859    /// Honored by every write op; emits a canonical dry-run envelope on
860    /// stdout under `--output json` / `--output jsonl`, or a "Would …" line
861    /// under `--output text`. Read ops ignore it.
862    #[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    /// Global result-set limit, clamped to 1..=100 (U7).
875    ///
876    /// Applies to every list-style command. The per-command `-n/--max-results`
877    /// flag takes precedence when both are set.
878    #[arg(long = "limit", global = true, env = "XURL_LIMIT")]
879    pub limit: Option<i32>,
880
881    /// Pagination cursor / `pagination_token` for list endpoints.
882    ///
883    /// The X API uses cursor-based pagination: each list response carries a
884    /// `meta.next_token` field, and the next page is fetched by re-running
885    /// the same command with `--cursor <token>` (or `XURL_CURSOR=<token>`).
886    /// Threads through to the `pagination_token` query parameter on every
887    /// `search`, `timeline`, `mentions`, `bookmarks`, `likes`, `following`,
888    /// `followers`, and `dms` invocation.
889    #[arg(
890        long = "cursor",
891        global = true,
892        env = "XURL_CURSOR",
893        value_name = "TOKEN"
894    )]
895    pub cursor: Option<String>,
896
897    /// Documented alias for `--cursor`.
898    ///
899    /// X's API does not offer offset-style pagination (`--page 2` is not
900    /// addressable). Passing `--page` returns a canonical
901    /// `unsupported-pagination` envelope on stderr suggesting `--cursor`
902    /// instead. Exposed so agents trained on offset-pagination conventions
903    /// get a structured error rather than a silent no-op.
904    #[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    /// Documented alias for `--cursor` (`--after <token>`).
914    ///
915    /// Threads through to the same `pagination_token` query parameter as
916    /// `--cursor`. Exposed so agents that picked up "after-style" pagination
917    /// from other CLIs (`gh`, `kubectl`) get a working flag.
918    #[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    /// Subcommand to run
929    #[command(subcommand)]
930    pub command: Option<Commands>,
931
932    /// URL for raw mode (positional, only when no subcommand)
933    pub url: Option<String>,
934}
935
936/// All subcommands.
937#[derive(Subcommand, Debug)]
938pub enum Commands {
939    // ── Posting ──────────────────────────────────────────────────────
940    /// Post to X
941    #[command(after_help = POST_HELP)]
942    Post {
943        /// The text to post
944        text: String,
945        /// Media ID(s) to attach (repeatable)
946        #[arg(long = "media-id")]
947        media_ids: Vec<String>,
948        /// Shortcut flags shared with every other shortcut command.
949        #[command(flatten)]
950        common: CommonFlags,
951    },
952    /// Reply to a post
953    #[command(after_help = REPLY_HELP)]
954    Reply {
955        /// Post ID or URL to reply to
956        post_id: String,
957        /// The reply text
958        text: String,
959        /// Media ID(s) to attach (repeatable)
960        #[arg(long = "media-id")]
961        media_ids: Vec<String>,
962        /// Shortcut flags shared with every other shortcut command.
963        #[command(flatten)]
964        common: CommonFlags,
965    },
966    /// Quote a post
967    #[command(after_help = QUOTE_HELP)]
968    Quote {
969        /// Post ID or URL to quote
970        post_id: String,
971        /// The quote text
972        text: String,
973        /// Shortcut flags shared with every other shortcut command.
974        #[command(flatten)]
975        common: CommonFlags,
976    },
977    /// Delete a post
978    #[command(after_help = DELETE_HELP)]
979    Delete {
980        /// Post ID or URL to delete
981        post_id: String,
982        /// Skip the confirmation prompt; required under `--no-interactive`
983        #[arg(long)]
984        force: bool,
985        /// Shortcut flags shared with every other shortcut command.
986        #[command(flatten)]
987        common: CommonFlags,
988    },
989
990    // ── Reading ──────────────────────────────────────────────────────
991    /// Read a post
992    #[command(after_help = READ_HELP)]
993    Read {
994        /// Post ID or URL to read
995        post_id: String,
996        /// Shortcut flags shared with every other shortcut command.
997        #[command(flatten)]
998        common: CommonFlags,
999    },
1000    /// Search recent posts
1001    #[command(after_help = SEARCH_HELP)]
1002    Search {
1003        /// Search query
1004        query: String,
1005        /// Number of results (1-100). Overrides global `--limit` when set.
1006        #[arg(short = 'n', long = "max-results")]
1007        max_results: Option<i32>,
1008        /// Shortcut flags shared with every other shortcut command.
1009        #[command(flatten)]
1010        common: CommonFlags,
1011    },
1012
1013    // ── User Info ────────────────────────────────────────────────────
1014    /// Show the authenticated user's profile
1015    #[command(after_help = WHOAMI_HELP)]
1016    Whoami {
1017        /// Shortcut flags shared with every other shortcut command.
1018        #[command(flatten)]
1019        common: CommonFlags,
1020    },
1021    /// Look up a user by username
1022    #[command(after_help = USER_HELP)]
1023    User {
1024        /// Username to look up
1025        #[arg(value_name = "USERNAME")]
1026        target_username: String,
1027        /// Shortcut flags shared with every other shortcut command.
1028        #[command(flatten)]
1029        common: CommonFlags,
1030    },
1031
1032    // ── Timeline & Mentions ──────────────────────────────────────────
1033    /// Show your home timeline
1034    #[command(after_help = TIMELINE_HELP)]
1035    Timeline {
1036        /// Number of results (1-100). Overrides global `--limit` when set.
1037        #[arg(short = 'n', long = "max-results")]
1038        max_results: Option<i32>,
1039        /// Shortcut flags shared with every other shortcut command.
1040        #[command(flatten)]
1041        common: CommonFlags,
1042    },
1043    /// Show your recent mentions
1044    #[command(after_help = MENTIONS_HELP)]
1045    Mentions {
1046        /// Number of results (5-100). Overrides global `--limit` when set.
1047        #[arg(short = 'n', long = "max-results")]
1048        max_results: Option<i32>,
1049        /// Shortcut flags shared with every other shortcut command.
1050        #[command(flatten)]
1051        common: CommonFlags,
1052    },
1053
1054    // ── Engagement ───────────────────────────────────────────────────
1055    /// Like a post
1056    #[command(after_help = LIKE_HELP)]
1057    Like {
1058        /// Post ID or URL
1059        post_id: String,
1060        /// Shortcut flags shared with every other shortcut command.
1061        #[command(flatten)]
1062        common: CommonFlags,
1063    },
1064    /// Unlike a post
1065    #[command(after_help = UNLIKE_HELP)]
1066    Unlike {
1067        /// Post ID or URL
1068        post_id: String,
1069        /// Shortcut flags shared with every other shortcut command.
1070        #[command(flatten)]
1071        common: CommonFlags,
1072    },
1073    /// Repost a post
1074    #[command(after_help = REPOST_HELP)]
1075    Repost {
1076        /// Post ID or URL
1077        post_id: String,
1078        /// Shortcut flags shared with every other shortcut command.
1079        #[command(flatten)]
1080        common: CommonFlags,
1081    },
1082    /// Undo a repost
1083    #[command(after_help = UNREPOST_HELP)]
1084    Unrepost {
1085        /// Post ID or URL
1086        post_id: String,
1087        /// Shortcut flags shared with every other shortcut command.
1088        #[command(flatten)]
1089        common: CommonFlags,
1090    },
1091    /// Bookmark a post
1092    #[command(after_help = BOOKMARK_HELP)]
1093    Bookmark {
1094        /// Post ID or URL
1095        post_id: String,
1096        /// Shortcut flags shared with every other shortcut command.
1097        #[command(flatten)]
1098        common: CommonFlags,
1099    },
1100    /// Remove a bookmark
1101    #[command(after_help = UNBOOKMARK_HELP)]
1102    Unbookmark {
1103        /// Post ID or URL
1104        post_id: String,
1105        /// Shortcut flags shared with every other shortcut command.
1106        #[command(flatten)]
1107        common: CommonFlags,
1108    },
1109    /// List your bookmarks
1110    #[command(after_help = BOOKMARKS_HELP)]
1111    Bookmarks {
1112        /// Number of results (1-100). Overrides global `--limit` when set.
1113        #[arg(short = 'n', long = "max-results")]
1114        max_results: Option<i32>,
1115        /// Shortcut flags shared with every other shortcut command.
1116        #[command(flatten)]
1117        common: CommonFlags,
1118    },
1119    /// List your liked posts
1120    #[command(after_help = LIKES_HELP)]
1121    Likes {
1122        /// Number of results (1-100). Overrides global `--limit` when set.
1123        #[arg(short = 'n', long = "max-results")]
1124        max_results: Option<i32>,
1125        /// Shortcut flags shared with every other shortcut command.
1126        #[command(flatten)]
1127        common: CommonFlags,
1128    },
1129
1130    // ── Social Graph ─────────────────────────────────────────────────
1131    /// Follow a user
1132    #[command(after_help = FOLLOW_HELP)]
1133    Follow {
1134        /// Username to follow
1135        #[arg(value_name = "USERNAME")]
1136        target_username: String,
1137        /// Shortcut flags shared with every other shortcut command.
1138        #[command(flatten)]
1139        common: CommonFlags,
1140    },
1141    /// Unfollow a user
1142    #[command(after_help = UNFOLLOW_HELP)]
1143    Unfollow {
1144        /// Username to unfollow
1145        #[arg(value_name = "USERNAME")]
1146        target_username: String,
1147        /// Shortcut flags shared with every other shortcut command.
1148        #[command(flatten)]
1149        common: CommonFlags,
1150    },
1151    /// List users you follow
1152    #[command(after_help = FOLLOWING_HELP)]
1153    Following {
1154        /// Number of results (1-1000). Overrides global `--limit` when set.
1155        #[arg(short = 'n', long = "max-results")]
1156        max_results: Option<i32>,
1157        /// Username to list following for (default: you)
1158        #[arg(long = "of")]
1159        of: Option<String>,
1160        /// Shortcut flags shared with every other shortcut command.
1161        #[command(flatten)]
1162        common: CommonFlags,
1163    },
1164    /// List your followers
1165    #[command(after_help = FOLLOWERS_HELP)]
1166    Followers {
1167        /// Number of results (1-1000). Overrides global `--limit` when set.
1168        #[arg(short = 'n', long = "max-results")]
1169        max_results: Option<i32>,
1170        /// Username to list followers for (default: you)
1171        #[arg(long = "of")]
1172        of: Option<String>,
1173        /// Shortcut flags shared with every other shortcut command.
1174        #[command(flatten)]
1175        common: CommonFlags,
1176    },
1177    /// Mute a user
1178    #[command(after_help = MUTE_HELP)]
1179    Mute {
1180        /// Username to mute
1181        #[arg(value_name = "USERNAME")]
1182        target_username: String,
1183        /// Shortcut flags shared with every other shortcut command.
1184        #[command(flatten)]
1185        common: CommonFlags,
1186    },
1187    /// Unmute a user
1188    #[command(after_help = UNMUTE_HELP)]
1189    Unmute {
1190        /// Username to unmute
1191        #[arg(value_name = "USERNAME")]
1192        target_username: String,
1193        /// Shortcut flags shared with every other shortcut command.
1194        #[command(flatten)]
1195        common: CommonFlags,
1196    },
1197
1198    // ── Usage ─────────────────────────────────────────────────────────
1199    /// Show API usage (tweet caps, daily breakdown)
1200    #[command(after_help = USAGE_HELP)]
1201    Usage {
1202        /// Shortcut flags shared with every other shortcut command.
1203        #[command(flatten)]
1204        common: CommonFlags,
1205    },
1206
1207    // ── Direct Messages ──────────────────────────────────────────────
1208    /// Send a direct message
1209    #[command(after_help = DM_HELP)]
1210    Dm {
1211        /// Username to DM
1212        #[arg(value_name = "USERNAME")]
1213        target_username: String,
1214        /// Message text
1215        text: String,
1216        /// Shortcut flags shared with every other shortcut command.
1217        #[command(flatten)]
1218        common: CommonFlags,
1219    },
1220    /// List recent direct messages
1221    #[command(after_help = DMS_HELP)]
1222    Dms {
1223        /// Number of results (1-100). Overrides global `--limit` when set.
1224        #[arg(short = 'n', long = "max-results")]
1225        max_results: Option<i32>,
1226        /// Shortcut flags shared with every other shortcut command.
1227        #[command(flatten)]
1228        common: CommonFlags,
1229    },
1230
1231    // ── Auth ─────────────────────────────────────────────────────────
1232    /// Authentication management
1233    #[command(after_help = AUTH_HELP)]
1234    Auth {
1235        /// `auth` subcommand to dispatch (`oauth2`, `oauth1`, `app`, …).
1236        #[command(subcommand)]
1237        command: AuthCommands,
1238    },
1239
1240    // ── Media ────────────────────────────────────────────────────────
1241    /// Media upload operations
1242    #[command(after_help = MEDIA_HELP)]
1243    Media {
1244        /// `media` subcommand to dispatch (`upload` or `status`).
1245        #[command(subcommand)]
1246        command: MediaCommands,
1247    },
1248
1249    // ── Skill bundle ─────────────────────────────────────────────────
1250    /// Install or manage the xurl-rs skill bundle
1251    ///
1252    /// Namespace for bundle operations. `xr skill install <host>` shallow-clones
1253    /// the xurl-rs repository into a host's canonical skills directory so the
1254    /// bundled `AGENTS.md` becomes discoverable to local agents.
1255    #[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        /// `skill` subcommand to dispatch (`install` or `update`).
1263        #[command(subcommand)]
1264        cmd: SkillCmd,
1265    },
1266
1267    // ── Meta ─────────────────────────────────────────────────────────
1268    /// Show JSON Schema for a command's response type
1269    #[command(after_help = SCHEMA_HELP)]
1270    Schema {
1271        /// Command name to get the schema for (e.g. "post", "whoami", "envelope")
1272        command: Option<String>,
1273        /// List all commands and their response types
1274        #[arg(long)]
1275        list: bool,
1276        /// Output all schemas as a single JSON document
1277        #[arg(long)]
1278        all: bool,
1279        /// Output the canonical agent-native output envelope schema
1280        #[arg(long)]
1281        envelope: bool,
1282    },
1283
1284    /// Generate shell completion script
1285    #[command(after_help = COMPLETIONS_HELP)]
1286    Completions {
1287        /// Shell to generate completions for
1288        #[arg(value_enum)]
1289        shell: clap_complete::Shell,
1290    },
1291    /// Show xurl version information
1292    #[command(after_help = VERSION_HELP)]
1293    Version,
1294
1295    /// Print a curated gallery of invocation examples grouped by use case
1296    #[command(after_help = EXAMPLES_HELP)]
1297    Examples,
1298
1299    /// Validate a JSON document against a bundled response schema.
1300    ///
1301    /// Reads JSON from stdin (when no file argument is given or `-` is
1302    /// passed) or from the supplied file, deserializes it into the
1303    /// requested typed response, and emits an `ok` / `validation-failed`
1304    /// envelope. Use `--schema` to pin a specific shape (e.g. `tweet`,
1305    /// `user`); without it the command auto-detects from the top-level
1306    /// shape.
1307    #[command(after_help = VALIDATE_HELP)]
1308    Validate {
1309        /// File path to read JSON from. Pass `-` or omit to read from stdin.
1310        #[arg(value_name = "FILE")]
1311        file: Option<String>,
1312
1313        /// Schema name to validate against (`tweet`, `tweets`, `user`,
1314        /// `users`, `dm`, `dms`, `usage`, `envelope`). Omit for auto-detection.
1315        #[arg(long = "schema", value_name = "NAME")]
1316        schema: Option<String>,
1317    },
1318}
1319
1320/// `skill` subcommand variants.
1321#[derive(Subcommand, Debug)]
1322pub enum SkillCmd {
1323    /// Install the skill bundle into a host's canonical skills directory.
1324    ///
1325    /// Shallow-clones the xurl-rs repository so the bundled `AGENTS.md` is
1326    /// discoverable to local agents. The destination is taken from the
1327    /// build-generated host map (`src/skill_install/skill.json`).
1328    #[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        /// Target host (e.g. claude_code, codex, cursor). Required unless `--all`.
1336        host: Option<SkillHost>,
1337
1338        /// Install into every known host in one invocation.
1339        #[arg(long, conflicts_with = "host")]
1340        all: bool,
1341
1342        /// Print the resolved git command without spawning.
1343        #[arg(long)]
1344        dry_run: bool,
1345    },
1346
1347    /// Refresh an existing skill-bundle install in place.
1348    ///
1349    /// Removes the current destination and re-runs the install pipeline so
1350    /// the bundle picks up upstream changes. Hardening surface is identical
1351    /// to `install`. The envelope's `action` is `"skill-update"` so agents
1352    /// can distinguish from a first-time install.
1353    #[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        /// Target host (e.g. claude_code, codex, cursor). Required unless `--all`.
1360        host: Option<SkillHost>,
1361
1362        /// Update every known host in one invocation.
1363        #[arg(long, conflicts_with = "host")]
1364        all: bool,
1365
1366        /// Print the resolved plan without removing or cloning.
1367        #[arg(long)]
1368        dry_run: bool,
1369    },
1370}
1371
1372impl Cli {
1373    /// Resolves the effective output format after applying `--json` /
1374    /// `--jsonl` aliases.
1375    ///
1376    /// `--jsonl` wins over `--json` if both were set (they conflict via
1377    /// clap, so at most one survives parsing); either alias overrides
1378    /// `--output`.
1379    #[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/// Common flags shared by shortcut commands.
1392///
1393/// `--verbose` is intentionally absent here; it lives on the root [`Cli`]
1394/// as a global flag with `XURL_VERBOSE` env backing, so subcommands inherit
1395/// it without local duplication.
1396#[derive(clap::Args, Debug, Clone)]
1397pub struct CommonFlags {
1398    /// Authentication type (oauth1, oauth2, app)
1399    #[arg(long = "auth")]
1400    pub auth_type: Option<String>,
1401
1402    /// `OAuth2` username to act as
1403    #[arg(short = 'u', long = "username")]
1404    pub username: Option<String>,
1405
1406    /// Add X-B3-Flags trace header
1407    #[arg(short = 't', long = "trace")]
1408    pub trace: bool,
1409}
1410
1411impl CommonFlags {
1412    /// Converts to `CallOptions` for shortcut methods.
1413    ///
1414    /// `verbose` and `timeout_secs` are sourced from the root [`Cli`] global
1415    /// flags rather than per-subcommand, so the caller threads them through
1416    /// here.
1417    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    /// Like [`to_call_options`] but with an explicit cursor / pagination
1422    /// token. The runner threads the global `--cursor` (or `--after` /
1423    /// `XURL_CURSOR` / `XURL_AFTER`) here so list shortcuts can append it to
1424    /// their URL.
1425    ///
1426    /// [`to_call_options`]: Self::to_call_options
1427    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/// Auth subcommands.
1446#[derive(Subcommand, Debug)]
1447pub enum AuthCommands {
1448    /// Configure `OAuth2` authentication
1449    #[command(after_help = AUTH_OAUTH2_HELP)]
1450    Oauth2 {
1451        /// Enable manual two-step flow for headless machines (SSH, containers)
1452        ///
1453        /// Auto-engages when stdout is not a TTY (piped runs, CI) so headless
1454        /// callers receive the auth URL instead of a silent `open::that` spawn
1455        /// that nothing will see. The `XURL_NO_BROWSER` env var sets this by
1456        /// default on machines that should never attempt to open a browser.
1457        /// Honours `1` / `true` / `yes` / `on` as truthy and `0` / `false` /
1458        /// `no` / `off` / empty as falsey (the same `FalseyValueParser` shape
1459        /// used by every other env-backed boolean flag).
1460        #[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        /// Step number: 1 (generate auth URL) or 2 (complete exchange)
1471        #[arg(long, requires = "no_browser", value_parser = clap::value_parser!(u8).range(1..=2))]
1472        step: Option<u8>,
1473        /// Redirect URL from browser (step 2). Use '-' to read from stdin (recommended on shared machines)
1474        #[arg(long = "auth-url", requires = "step")]
1475        auth_url: Option<String>,
1476        /// Username to label the saved token (bypasses `/2/users/me` lookup when supplied)
1477        #[arg(value_name = "USERNAME")]
1478        username: Option<String>,
1479    },
1480    /// Configure `OAuth1` authentication
1481    #[command(after_help = AUTH_OAUTH1_HELP)]
1482    Oauth1 {
1483        /// Consumer key
1484        #[arg(long = "consumer-key")]
1485        consumer_key: String,
1486        /// Consumer secret
1487        #[arg(long = "consumer-secret")]
1488        consumer_secret: String,
1489        /// Access token
1490        #[arg(long = "access-token")]
1491        access_token: String,
1492        /// Token secret
1493        #[arg(long = "token-secret")]
1494        token_secret: String,
1495    },
1496    /// Configure app-auth (bearer token)
1497    #[command(after_help = AUTH_APP_HELP)]
1498    App {
1499        /// Bearer token
1500        #[arg(long = "bearer-token")]
1501        bearer_token: String,
1502    },
1503    /// Show authentication status
1504    #[command(after_help = AUTH_STATUS_HELP)]
1505    Status,
1506    /// Clear authentication tokens
1507    #[command(after_help = AUTH_CLEAR_HELP)]
1508    Clear {
1509        /// Clear all authentication
1510        #[arg(long)]
1511        all: bool,
1512        /// Clear `OAuth1` tokens
1513        #[arg(long)]
1514        oauth1: bool,
1515        /// Clear `OAuth2` token for username
1516        #[arg(long = "oauth2-username")]
1517        oauth2_username: Option<String>,
1518        /// Clear bearer token
1519        #[arg(long)]
1520        bearer: bool,
1521        /// Skip the confirmation prompt; required under `--no-interactive`
1522        #[arg(long)]
1523        force: bool,
1524    },
1525    /// Manage registered X API apps
1526    #[command(after_help = AUTH_APPS_HELP)]
1527    Apps {
1528        /// `apps` subcommand to dispatch (`add`, `list`, `update`, …).
1529        #[command(subcommand)]
1530        command: AppCommands,
1531    },
1532    /// Set default app and/or user
1533    #[command(after_help = AUTH_DEFAULT_HELP)]
1534    Default {
1535        /// App name (optional)
1536        app_name: Option<String>,
1537        /// Username (optional)
1538        username: Option<String>,
1539    },
1540}
1541
1542/// App management subcommands.
1543#[derive(Subcommand, Debug)]
1544pub enum AppCommands {
1545    /// Register a new X API app
1546    #[command(after_help = APPS_ADD_HELP)]
1547    Add {
1548        /// App name
1549        name: String,
1550        /// `OAuth2` client ID
1551        #[arg(long = "client-id")]
1552        client_id: String,
1553        /// `OAuth2` client secret
1554        #[arg(long = "client-secret")]
1555        client_secret: String,
1556        /// `OAuth2` redirect URI (https or http on loopback)
1557        #[arg(long = "redirect-uri")]
1558        redirect_uri: Option<String>,
1559    },
1560    /// Update credentials for an existing app
1561    #[command(after_help = APPS_UPDATE_HELP)]
1562    Update {
1563        /// App name
1564        name: String,
1565        /// `OAuth2` client ID
1566        #[arg(long = "client-id")]
1567        client_id: Option<String>,
1568        /// `OAuth2` client secret
1569        #[arg(long = "client-secret")]
1570        client_secret: Option<String>,
1571        /// `OAuth2` redirect URI (https or http on loopback); empty string clears
1572        #[arg(long = "redirect-uri")]
1573        redirect_uri: Option<String>,
1574    },
1575    /// Remove a registered app
1576    #[command(after_help = APPS_REMOVE_HELP)]
1577    Remove {
1578        /// App name
1579        name: String,
1580        /// Skip the confirmation prompt; required under `--no-interactive`
1581        #[arg(long)]
1582        force: bool,
1583    },
1584    /// List registered apps
1585    #[command(after_help = APPS_LIST_HELP)]
1586    List,
1587    /// Inspect or set the stored `OAuth2` redirect URI for an app
1588    #[command(after_help = APPS_REDIRECT_URI_HELP)]
1589    RedirectUri {
1590        /// `redirect-uri` subcommand to dispatch (`get` or `set`).
1591        #[command(subcommand)]
1592        command: RedirectUriCommands,
1593    },
1594}
1595
1596/// `auth apps redirect-uri` subcommands.
1597#[derive(Subcommand, Debug)]
1598pub enum RedirectUriCommands {
1599    /// Show the effective redirect URI, its source, and the stored value
1600    #[command(after_help = REDIRECT_URI_GET_HELP)]
1601    Get {
1602        /// App name (defaults to the configured default app)
1603        #[arg(value_name = "NAME")]
1604        name: Option<String>,
1605    },
1606    /// Set the stored redirect URI for an app (empty string clears)
1607    #[command(after_help = REDIRECT_URI_SET_HELP)]
1608    Set {
1609        /// App name
1610        #[arg(value_name = "NAME")]
1611        name: String,
1612        /// Redirect URI (https or http on loopback)
1613        #[arg(value_name = "URI")]
1614        uri: String,
1615    },
1616}
1617
1618/// Media subcommands.
1619#[derive(Subcommand, Debug)]
1620pub enum MediaCommands {
1621    /// Upload media file
1622    #[command(after_help = MEDIA_UPLOAD_HELP)]
1623    Upload {
1624        /// File path
1625        file: String,
1626        /// Media type (e.g., video/mp4)
1627        #[arg(long = "media-type", default_value = "video/mp4")]
1628        media_type: String,
1629        /// Media category (e.g., `amplify_video`)
1630        #[arg(long = "category", default_value = "amplify_video")]
1631        category: String,
1632        /// Wait for media processing to complete
1633        #[arg(long = "wait", default_value = "true")]
1634        wait: bool,
1635        /// Authentication type
1636        #[arg(long = "auth")]
1637        auth_type: Option<String>,
1638        /// Username
1639        #[arg(short = 'u', long = "username")]
1640        username: Option<String>,
1641        /// Trace header
1642        #[arg(short = 't', long = "trace")]
1643        trace: bool,
1644        /// Request headers
1645        #[arg(short = 'H', long = "header")]
1646        headers: Vec<String>,
1647    },
1648    /// Check media upload status
1649    #[command(after_help = MEDIA_STATUS_HELP)]
1650    Status {
1651        /// Media ID
1652        media_id: String,
1653        /// Authentication type
1654        #[arg(long = "auth")]
1655        auth_type: Option<String>,
1656        /// Username
1657        #[arg(short = 'u', long = "username")]
1658        username: Option<String>,
1659        /// Wait for processing
1660        #[arg(short = 'w', long = "wait")]
1661        wait: bool,
1662        /// Trace header
1663        #[arg(short = 't', long = "trace")]
1664        trace: bool,
1665        /// Request headers
1666        #[arg(short = 'H', long = "header")]
1667        headers: Vec<String>,
1668    },
1669}