dsc/cli.rs
1use clap::{ArgAction, Parser, Subcommand, ValueEnum};
2use clap_complete::Shell;
3use std::path::PathBuf;
4
5#[derive(Parser)]
6#[command(name = "dsc")]
7#[command(about = "Discourse CLI", long_about = None)]
8pub struct Cli {
9 /// Path to the config file. If omitted, `dsc` consults `$DSC_CONFIG`,
10 /// then searches `./dsc.toml`, `$DSC_CONFIG_HOME/dsc.toml`
11 /// (default `~/.config/dsc/dsc.toml`), then system locations.
12 /// Errors if the given file does not exist (no silent fallthrough).
13 /// See `dsc config` for the active selection.
14 #[arg(long, short = 'c')]
15 pub config: Option<PathBuf>,
16 /// Describe destructive actions without sending them. Read-only commands
17 /// ignore the flag.
18 #[arg(long, short = 'n', global = true)]
19 pub dry_run: bool,
20 #[command(subcommand)]
21 pub command: Commands,
22}
23
24#[derive(Subcommand)]
25pub enum Commands {
26 /// List configured Discourses.
27 #[command(visible_alias = "ls")]
28 List {
29 /// Output format for the listing.
30 #[arg(long, short = 'f', value_enum, default_value = "text")]
31 format: OutputFormat,
32 /// Filter by tags (comma/semicolon separated, match-any).
33 #[arg(long, value_name = "tag1,tag2")]
34 tags: Option<String>,
35 /// Open each listed Discourse base URL in a browser tab/window.
36 #[arg(long, short = 'o')]
37 open: bool,
38 /// Include empty results and verbose listing details where supported.
39 #[arg(long, short = 'v')]
40 verbose: bool,
41 #[command(subcommand)]
42 command: Option<ListCommand>,
43 },
44 /// Add one or more Discourses to the config.
45 #[command(visible_alias = "a")]
46 Add {
47 /// Comma-separated discourse names to add.
48 names: String,
49 /// Prompt for additional optional fields while adding.
50 #[arg(long, short = 'i')]
51 interactive: bool,
52 },
53 /// Import Discourses from a file or stdin.
54 #[command(visible_alias = "imp")]
55 Import {
56 /// Path to import input (text/CSV). Reads stdin when omitted.
57 path: Option<PathBuf>,
58 },
59 /// Run remote OS + Discourse update workflow for one or all Discourses.
60 #[command(visible_alias = "up")]
61 Update {
62 /// Discourse name, or 'all' to update every configured Discourse.
63 name: String,
64 /// Parallel update mode for `dsc update all`.
65 #[arg(long, short = 'p')]
66 parallel: bool,
67 /// Maximum workers when parallel mode is enabled (default: 3).
68 #[arg(long, short = 'm')]
69 max: Option<usize>,
70 /// Disable changelog posting (posting prompt is on by default).
71 #[arg(long = "no-changelog", action = ArgAction::SetFalse, default_value_t = true)]
72 post_changelog: bool,
73 /// Auto-confirm changelog posting prompt (non-interactive mode).
74 #[arg(long, short = 'y')]
75 yes: bool,
76 },
77 /// Manage custom emoji.
78 #[command(visible_alias = "em")]
79 Emoji {
80 #[command(subcommand)]
81 command: EmojiCommand,
82 },
83 /// Pull/push/sync topics as local Markdown.
84 #[command(visible_alias = "t")]
85 Topic {
86 #[command(subcommand)]
87 command: TopicCommand,
88 },
89 /// List/copy/pull/push categories.
90 #[command(visible_alias = "cat")]
91 Category {
92 #[command(subcommand)]
93 command: CategoryCommand,
94 },
95 /// List/inspect/copy groups.
96 #[command(visible_alias = "grp")]
97 Group {
98 #[command(subcommand)]
99 command: GroupCommand,
100 },
101 /// Operations that act from a user's perspective.
102 #[command(visible_alias = "usr")]
103 User {
104 #[command(subcommand)]
105 command: UserCommand,
106 },
107 /// Send invites — single or bulk from a file.
108 #[command(visible_alias = "inv")]
109 Invite {
110 #[command(subcommand)]
111 command: InviteCommand,
112 },
113 /// Manage API keys (admin scope).
114 #[command(visible_alias = "ak")]
115 ApiKey {
116 #[command(subcommand)]
117 command: ApiKeyCommand,
118 },
119 /// Send and list private messages.
120 #[command(visible_alias = "msg")]
121 Pm {
122 #[command(subcommand)]
123 command: PmCommand,
124 },
125 /// Create/list/restore backups.
126 #[command(visible_alias = "bk")]
127 Backup {
128 #[command(subcommand)]
129 command: BackupCommand,
130 },
131 /// List/pull/push color palettes.
132 #[command(visible_alias = "pal")]
133 Palette {
134 #[command(subcommand)]
135 command: PaletteCommand,
136 },
137 /// List/install/remove plugins.
138 #[command(visible_alias = "plg")]
139 Plugin {
140 #[command(subcommand)]
141 command: PluginCommand,
142 },
143 /// List/install/remove/pull/push/duplicate themes.
144 #[command(visible_alias = "th")]
145 Theme {
146 #[command(subcommand)]
147 command: ThemeCommand,
148 },
149 /// Update site settings.
150 #[command(visible_alias = "set")]
151 Setting {
152 #[command(subcommand)]
153 command: SettingCommand,
154 },
155 /// Export everything a forum holds about one person into a reviewable
156 /// Subject Access Request (SAR / GDPR Art. 15) bundle. Single forum.
157 Sar {
158 /// Discourse name.
159 discourse: String,
160 /// Subject: a username or an email address.
161 user: String,
162 /// Output directory (default `sar-<username>-<date>/`).
163 #[arg(long, short = 'o')]
164 output: Option<PathBuf>,
165 /// Also collect the subject's private messages. Off by default: PMs
166 /// contain third-party personal data and need a disclose/redact
167 /// judgement. Written with a REVIEW REQUIRED banner when included.
168 #[arg(long)]
169 messages: bool,
170 },
171 /// Manage the tag taxonomy: list/pull/push tags and tag groups.
172 #[command(visible_alias = "tg")]
173 Tag {
174 #[command(subcommand)]
175 command: TagCommand,
176 },
177 /// Post-level operations: edit / delete / move.
178 #[command(visible_alias = "po")]
179 Post {
180 #[command(subcommand)]
181 command: PostCommand,
182 },
183 /// Open a Discourse in the default browser.
184 #[command(visible_alias = "o")]
185 Open {
186 /// Discourse name.
187 discourse: String,
188 },
189 /// Harden a fresh Ubuntu server reachable via `ssh root@host`.
190 ///
191 /// **Stage 1 (current):** creates a non-root sudo user, installs the
192 /// given pubkey to their authorized_keys, and verifies the new-user
193 /// SSH login works. Does NOT yet tighten sshd_config, install Docker
194 /// / fail2ban / etc — those come in follow-up releases.
195 ///
196 /// Defaults can be overridden in the `[harden]` block of dsc.toml;
197 /// the flags below override that block on a per-run basis.
198 #[command(visible_alias = "hd")]
199 Harden {
200 /// Target hostname or IP (reachable via SSH).
201 host: String,
202 /// Username to SSH in as initially. Defaults to `root`, which is
203 /// what a fresh cloud-provisioned box typically has.
204 #[arg(long, default_value = "root")]
205 ssh_user: String,
206 /// Username for the new sudo-enabled non-root account. Overrides
207 /// `[harden].new_user` from dsc.toml. Built-in default: `discourse`.
208 #[arg(long)]
209 new_user: Option<String>,
210 /// SSH port to move the daemon to in stage 2. Overrides
211 /// `[harden].ssh_port`. Built-in default: 2227. Parsed now so the
212 /// CLI is stable; not yet applied in stage 1.
213 #[arg(long)]
214 ssh_port: Option<u16>,
215 /// Path to an SSH public key file whose contents will be added to
216 /// the new user's authorized_keys. A typical value is
217 /// `~/.ssh/<hostname>.pub` — the per-server keypair pattern in
218 /// the Bawmedical hardening playbook.
219 #[arg(long)]
220 pubkey_file: PathBuf,
221 },
222 /// Community-health analytics — growth, activity, and health metrics
223 /// for a Discourse, with optional period-over-period comparison.
224 ///
225 /// See `spec/analytics.md` for the full spec. v1 ships every metric
226 /// that maps onto a single `/admin/reports/{id}.json` endpoint;
227 /// derivation-heavy ones (e.g. lost regulars, top-10 share) print
228 /// `— (n/i)` until follow-up implementation lands.
229 #[command(visible_alias = "stats")]
230 Analytics {
231 /// Discourse name.
232 discourse: String,
233 /// Window to report on. Same syntax as `dsc user activity --since`
234 /// (e.g. `7d`, `24h`, `1m`, ISO-8601). Ignored when `--snapshot`
235 /// is set. Default: 30d.
236 #[arg(long, short = 's', default_value = "30d")]
237 since: String,
238 /// Also fetch the immediately preceding window of equal length and
239 /// show a delta column. Mutually exclusive with `--snapshot`.
240 #[arg(long, short = 'c', conflicts_with = "snapshot")]
241 compare: bool,
242 /// Multi-window snapshot mode. Reports each metric across several
243 /// preset windows (`--periods`) so you see growth/health trends
244 /// at a glance. Replaces `--since` + `--compare`.
245 #[arg(long)]
246 snapshot: bool,
247 /// Comma-separated periods for `--snapshot`. Default: `24h,7d,30d,1y`.
248 #[arg(long, requires = "snapshot")]
249 periods: Option<String>,
250 /// Restrict output to one section.
251 #[arg(long, value_enum, default_value = "all")]
252 section: SectionArg,
253 /// Output format. `table` is DuckDB-style box-drawing; falls
254 /// through to `text` automatically when stdout isn't a TTY.
255 #[arg(long, short = 'f', value_enum, default_value = "text")]
256 format: AnalyticsFormat,
257 },
258 /// Search topics on a Discourse.
259 #[command(visible_alias = "s")]
260 Search {
261 /// Discourse name.
262 discourse: String,
263 /// Search query (passed through verbatim, including any
264 /// Discourse filter syntax like `category:foo` or `@user`).
265 query: String,
266 /// Output format.
267 #[arg(long, short = 'f', value_enum, default_value = "text")]
268 format: ListFormat,
269 },
270 /// Upload a file. Prints the resulting upload:// short URL by default.
271 #[command(visible_alias = "u")]
272 Upload {
273 /// Discourse name.
274 discourse: String,
275 /// Path to the file to upload.
276 file: PathBuf,
277 /// Discourse upload context. Default `composer` is correct for
278 /// embedding in posts; other values include `avatar`,
279 /// `profile_background`, `card_background`, `custom_emoji`.
280 #[arg(long, short = 't', default_value = "composer")]
281 upload_type: String,
282 /// Output format. Text mode prints just the short URL.
283 #[arg(long, short = 'f', value_enum, default_value = "text")]
284 format: ListFormat,
285 },
286 /// Inspect and validate configuration.
287 #[command(visible_alias = "cfg")]
288 Config {
289 #[command(subcommand)]
290 command: Option<ConfigCommand>,
291 },
292 /// Generate shell completion scripts.
293 #[command(visible_alias = "comp")]
294 Completions {
295 /// Target shell.
296 #[arg(value_enum)]
297 shell: CompletionShell,
298 /// Output directory. Prints to stdout when omitted.
299 #[arg(long, short = 'd')]
300 dir: Option<PathBuf>,
301 },
302 /// Generate man pages for `dsc` and every subcommand.
303 ///
304 /// Writes one ROFF-formatted file per (sub)command (e.g. `dsc.1`,
305 /// `dsc-tag-pull.1`) into the given directory. Distro packagers
306 /// install these into section 1 of the man path. Run `gzip -9` on
307 /// the output if your packaging convention expects compressed pages.
308 #[command(visible_alias = "manpages")]
309 Man {
310 /// Output directory. Required - this command always writes to disk.
311 #[arg(long, short = 'd')]
312 dir: PathBuf,
313 },
314 /// Print the dsc version.
315 #[command(visible_alias = "ver")]
316 Version,
317}
318
319#[derive(Subcommand)]
320pub enum ConfigCommand {
321 /// Probe each configured Discourse: API auth and (optionally) SSH reachability.
322 #[command(visible_alias = "ck")]
323 Check {
324 /// Output format.
325 #[arg(long, short = 'f', value_enum, default_value = "text")]
326 format: ListFormat,
327 /// Skip the SSH reachability probe.
328 #[arg(long)]
329 skip_ssh: bool,
330 },
331}
332
333#[derive(Subcommand)]
334pub enum ListCommand {
335 /// Sort discourse entries by name and rewrite config in-place.
336 /// Also inserts placeholder values for unset template keys.
337 #[command(visible_alias = "ty")]
338 Tidy,
339}
340
341#[derive(Subcommand)]
342pub enum EmojiCommand {
343 /// Pull all custom emoji from a Discourse into a local directory.
344 #[command(visible_alias = "pl")]
345 Pull {
346 /// Discourse name.
347 discourse: String,
348 /// Local directory to save emoji images into.
349 output_dir: PathBuf,
350 },
351 /// Push (upload) one emoji file, or bulk-upload from a directory (alias: add).
352 #[command(visible_alias = "ps", alias = "add")]
353 Push {
354 /// Discourse name.
355 discourse: String,
356 /// Local file or directory path.
357 emoji_path: PathBuf,
358 /// Optional emoji name (file uploads only).
359 emoji_name: Option<String>,
360 },
361
362 /// List custom emojis on a Discourse.
363 #[command(visible_alias = "ls")]
364 List {
365 /// Discourse name.
366 discourse: String,
367 /// Output format.
368 #[arg(long, short = 'f', value_enum, default_value = "text")]
369 format: ListFormat,
370 /// Include additional fields where supported.
371 #[arg(long, short = 'v')]
372 verbose: bool,
373 /// Render inline images when terminal protocol support is available.
374 #[arg(long, short = 'i')]
375 inline: bool,
376 },
377}
378
379#[derive(Subcommand)]
380pub enum TopicCommand {
381 /// Pull a topic to a local Markdown file.
382 #[command(visible_alias = "pl")]
383 Pull {
384 /// Discourse name.
385 discourse: String,
386 /// Topic ID.
387 topic_id: u64,
388 /// Destination file or directory (auto-derived when omitted).
389 local_path: Option<PathBuf>,
390 /// Pull the entire thread (every post) as a single Markdown file
391 /// with YAML frontmatter and per-post headings. Default behaviour
392 /// (no `--full`) writes only the OP, which is what `topic push`
393 /// expects.
394 #[arg(long, short = 'F')]
395 full: bool,
396 },
397 /// Push a local Markdown file to a topic.
398 #[command(visible_alias = "ps")]
399 Push {
400 /// Discourse name.
401 discourse: String,
402 /// Topic ID.
403 topic_id: u64,
404 /// Local Markdown file path.
405 local_path: PathBuf,
406 /// Update the post without bumping the topic in the activity feed.
407 /// Use for silent maintenance edits (sends post[no_bump]=true).
408 #[arg(long)]
409 no_bump: bool,
410 /// Update the post without recording an edit-history revision
411 /// (sends post[skip_revision]=true). Suppresses the online audit
412 /// trail - use sparingly.
413 #[arg(long)]
414 skip_revision: bool,
415 },
416 /// Sync a topic and local Markdown file using newest timestamp.
417 #[command(visible_alias = "sy")]
418 Sync {
419 /// Discourse name.
420 discourse: String,
421 /// Topic ID.
422 topic_id: u64,
423 /// Local Markdown file path.
424 local_path: PathBuf,
425 /// Skip sync confirmation prompt.
426 #[arg(long, short = 'y')]
427 yes: bool,
428 },
429 /// Reply to a topic with content from a file or stdin.
430 #[command(visible_alias = "r")]
431 Reply {
432 /// Discourse name.
433 discourse: String,
434 /// Topic ID.
435 topic_id: u64,
436 /// Input file path. Reads stdin when omitted or `-`.
437 local_path: Option<PathBuf>,
438 /// Output format.
439 #[arg(long, short = 'f', value_enum, default_value = "text")]
440 format: ListFormat,
441 },
442 /// Create a new topic in a category, body from a file or stdin.
443 #[command(visible_alias = "n")]
444 New {
445 /// Discourse name.
446 discourse: String,
447 /// Target category ID.
448 category_id: u64,
449 /// Topic title.
450 #[arg(long, short = 't')]
451 title: String,
452 /// Input file path. Reads stdin when omitted or `-`.
453 local_path: Option<PathBuf>,
454 /// Output format.
455 #[arg(long, short = 'f', value_enum, default_value = "text")]
456 format: ListFormat,
457 },
458 /// Add a tag to a topic.
459 Tag {
460 /// Discourse name.
461 discourse: String,
462 /// Topic ID.
463 topic_id: u64,
464 /// Tag to add.
465 tag: String,
466 },
467 /// Remove a tag from a topic.
468 Untag {
469 /// Discourse name.
470 discourse: String,
471 /// Topic ID.
472 topic_id: u64,
473 /// Tag to remove.
474 tag: String,
475 },
476 /// Rename a topic's title (changes its URL slug). Honours `--dry-run`.
477 Title {
478 /// Discourse name.
479 discourse: String,
480 /// Topic ID.
481 topic_id: u64,
482 /// New title.
483 title: String,
484 },
485 /// Set a topic's full tag list, replacing existing tags. Pass no tags to
486 /// clear all tags. Honours `--dry-run`.
487 Tags {
488 /// Discourse name.
489 discourse: String,
490 /// Topic ID.
491 topic_id: u64,
492 /// Tags to set (space-separated; omit to clear all tags).
493 tags: Vec<String>,
494 },
495}
496
497#[derive(Subcommand)]
498pub enum CategoryCommand {
499 /// List categories.
500 #[command(visible_alias = "ls")]
501 List {
502 /// Discourse name.
503 discourse: String,
504 /// Output format.
505 #[arg(long, short = 'f', value_enum, default_value = "text")]
506 format: ListFormat,
507 /// Include additional fields where supported.
508 #[arg(long, short = 'v')]
509 verbose: bool,
510 /// Show category hierarchy tree.
511 #[arg(long)]
512 tree: bool,
513 },
514 /// Copy a category to another Discourse.
515 #[command(visible_alias = "cp")]
516 Copy {
517 /// Source discourse name.
518 discourse: String,
519 /// Target discourse name (defaults to source when omitted).
520 #[arg(long, short = 't')]
521 target: Option<String>,
522 /// Category ID or slug.
523 category: String,
524 },
525 /// Pull all topics from a category into local Markdown files.
526 #[command(visible_alias = "pl")]
527 Pull {
528 /// Discourse name.
529 discourse: String,
530 /// Category ID or slug.
531 category: String,
532 /// Destination directory (auto-derived when omitted).
533 local_path: Option<PathBuf>,
534 },
535 /// Push local Markdown files into a category.
536 #[command(visible_alias = "ps")]
537 Push {
538 /// Discourse name.
539 discourse: String,
540 /// Category ID or slug.
541 category: String,
542 /// Local directory containing Markdown files.
543 local_path: PathBuf,
544 /// Only update existing topics; error instead of creating a new topic
545 /// when a local file has no remote match.
546 #[arg(long)]
547 updates_only: bool,
548 /// Update posts without bumping their topics in the activity feed.
549 /// Use for silent bulk maintenance edits (sends post[no_bump]=true).
550 #[arg(long)]
551 no_bump: bool,
552 /// Update posts without recording edit-history revisions
553 /// (sends post[skip_revision]=true). Suppresses the online audit
554 /// trail - use sparingly.
555 #[arg(long)]
556 skip_revision: bool,
557 },
558}
559
560#[derive(Subcommand)]
561pub enum GroupCommand {
562 /// List groups.
563 #[command(visible_alias = "ls")]
564 List {
565 /// Discourse name.
566 discourse: String,
567 /// Output format.
568 #[arg(long, short = 'f', value_enum, default_value = "text")]
569 format: ListFormat,
570 /// Include additional fields where supported.
571 #[arg(long, short = 'v')]
572 verbose: bool,
573 },
574 /// Show group details.
575 #[command(visible_alias = "i")]
576 Info {
577 /// Discourse name.
578 discourse: String,
579 /// Group ID.
580 group: u64,
581 /// Output format.
582 #[arg(long, short = 'f', value_enum, default_value = "json")]
583 format: StructuredFormat,
584 },
585 /// List members of a group.
586 #[command(visible_alias = "m")]
587 Members {
588 /// Discourse name.
589 discourse: String,
590 /// Group ID.
591 group: u64,
592 /// Output format.
593 #[arg(long, short = 'f', value_enum, default_value = "text")]
594 format: ListFormat,
595 },
596 /// Copy a group to another Discourse.
597 #[command(visible_alias = "cp")]
598 Copy {
599 /// Source discourse name.
600 discourse: String,
601 /// Target discourse name (defaults to source when omitted).
602 #[arg(long, short = 't')]
603 target: Option<String>,
604 /// Group ID.
605 group: u64,
606 },
607 /// Bulk add members to a group from a file (or stdin) of email addresses.
608 #[command(visible_alias = "a")]
609 Add {
610 /// Discourse name.
611 discourse: String,
612 /// Group ID.
613 group: u64,
614 /// Path to a file of email addresses (one per line; blank
615 /// lines and `#` comments are ignored). Reads stdin when
616 /// omitted or `-`.
617 local_path: Option<PathBuf>,
618 /// Send Discourse notifications to added users.
619 #[arg(long)]
620 notify: bool,
621 },
622}
623
624#[derive(Subcommand)]
625pub enum BackupCommand {
626 /// Create a new backup.
627 #[command(visible_alias = "cr")]
628 Create {
629 /// Discourse name.
630 discourse: String,
631 },
632 /// List backups.
633 #[command(visible_alias = "ls")]
634 List {
635 /// Discourse name.
636 discourse: String,
637 /// Output format.
638 #[arg(long, short = 'f', value_enum, default_value = "text")]
639 format: OutputFormat,
640 /// Include additional fields where supported.
641 #[arg(long, short = 'v')]
642 verbose: bool,
643 },
644 /// Pull (download) a backup to a local file.
645 #[command(visible_alias = "pl")]
646 Pull {
647 /// Discourse name.
648 discourse: String,
649 /// Backup filename on the server (from `dsc backup list`).
650 backup_filename: String,
651 /// Local output path. Defaults to the backup filename in the current directory.
652 local_path: Option<PathBuf>,
653 },
654 /// Push (restore) a backup on the server (alias: restore).
655 #[command(visible_alias = "ps", alias = "restore")]
656 Push {
657 /// Discourse name.
658 discourse: String,
659 /// Backup filename/path on the target system.
660 backup_path: String,
661 },
662}
663
664#[derive(Subcommand)]
665pub enum PaletteCommand {
666 /// List color palettes.
667 #[command(visible_alias = "ls")]
668 List {
669 /// Discourse name.
670 discourse: String,
671 /// Output format.
672 #[arg(long, short = 'f', value_enum, default_value = "text")]
673 format: ListFormat,
674 /// Include additional fields where supported.
675 #[arg(long, short = 'v')]
676 verbose: bool,
677 },
678 /// Pull a palette to local JSON.
679 #[command(visible_alias = "pl")]
680 Pull {
681 /// Discourse name.
682 discourse: String,
683 /// Palette ID.
684 palette_id: u64,
685 /// Destination file path (auto-derived when omitted).
686 local_path: Option<PathBuf>,
687 },
688 /// Push local JSON to create or update a palette.
689 #[command(visible_alias = "ps")]
690 Push {
691 /// Discourse name.
692 discourse: String,
693 /// Local JSON file path.
694 local_path: PathBuf,
695 /// Palette ID to update (creates a new palette when omitted).
696 palette_id: Option<u64>,
697 },
698}
699
700#[derive(Subcommand)]
701pub enum PluginCommand {
702 /// List installed plugins.
703 #[command(visible_alias = "ls")]
704 List {
705 /// Discourse name.
706 discourse: String,
707 /// Output format.
708 #[arg(long, short = 'f', value_enum, default_value = "text")]
709 format: ListFormat,
710 /// Include additional fields where supported.
711 #[arg(long, short = 'v')]
712 verbose: bool,
713 },
714 /// Install a plugin from URL.
715 #[command(visible_alias = "i")]
716 Install {
717 /// Discourse name.
718 discourse: String,
719 /// Plugin repository URL.
720 url: String,
721 },
722 /// Remove a plugin by name.
723 #[command(visible_alias = "rm")]
724 Remove {
725 /// Discourse name.
726 discourse: String,
727 /// Plugin name.
728 name: String,
729 },
730}
731
732#[derive(Subcommand)]
733pub enum ThemeCommand {
734 /// List installed themes.
735 #[command(visible_alias = "ls")]
736 List {
737 /// Discourse name.
738 discourse: String,
739 /// Output format.
740 #[arg(long, short = 'f', value_enum, default_value = "text")]
741 format: ListFormat,
742 /// Include additional fields where supported.
743 #[arg(long, short = 'v')]
744 verbose: bool,
745 },
746 /// Install a theme from URL.
747 #[command(visible_alias = "i")]
748 Install {
749 /// Discourse name.
750 discourse: String,
751 /// Theme repository URL.
752 url: String,
753 },
754 /// Remove a theme by name.
755 #[command(visible_alias = "rm")]
756 Remove {
757 /// Discourse name.
758 discourse: String,
759 /// Theme name.
760 name: String,
761 },
762 /// Pull a theme to a local JSON file.
763 #[command(visible_alias = "pl")]
764 Pull {
765 /// Discourse name.
766 discourse: String,
767 /// Theme ID (from `dsc theme list`).
768 theme_id: u64,
769 /// Destination file path (auto-derived from theme name when omitted).
770 local_path: Option<PathBuf>,
771 },
772 /// Push a local JSON file to create or update a theme.
773 #[command(visible_alias = "ps")]
774 Push {
775 /// Discourse name.
776 discourse: String,
777 /// Local JSON file path.
778 local_path: PathBuf,
779 /// Theme ID to update (creates a new theme when omitted).
780 theme_id: Option<u64>,
781 },
782 /// Duplicate a theme and print the new theme ID.
783 #[command(visible_alias = "dup")]
784 Duplicate {
785 /// Discourse name.
786 discourse: String,
787 /// Theme ID to duplicate (from `dsc theme list`).
788 theme_id: u64,
789 /// Output format.
790 #[arg(long, short = 'f', value_enum, default_value = "text")]
791 format: ListFormat,
792 },
793 /// Show a richer view of one theme/component than `list`.
794 Show {
795 /// Discourse name.
796 discourse: String,
797 /// Theme ID (from `dsc theme list`).
798 theme_id: u64,
799 /// Output format.
800 #[arg(long, short = 'f', value_enum, default_value = "text")]
801 format: ListFormat,
802 },
803 /// Read and write a theme/component's settings (not site settings).
804 Setting {
805 #[command(subcommand)]
806 command: ThemeSettingCommand,
807 },
808 /// Enable a theme or component.
809 Enable {
810 /// Discourse name.
811 discourse: String,
812 /// Theme ID (from `dsc theme list`).
813 theme_id: u64,
814 },
815 /// Disable a theme or component.
816 Disable {
817 /// Discourse name.
818 discourse: String,
819 /// Theme ID (from `dsc theme list`).
820 theme_id: u64,
821 },
822 /// Attach a component to a parent theme (makes it active on that theme).
823 Attach {
824 /// Discourse name.
825 discourse: String,
826 /// Parent theme ID.
827 parent_id: u64,
828 /// Component (child theme) ID to attach.
829 component_id: u64,
830 },
831 /// Detach a component from a parent theme.
832 Detach {
833 /// Discourse name.
834 discourse: String,
835 /// Parent theme ID.
836 parent_id: u64,
837 /// Component (child theme) ID to detach.
838 component_id: u64,
839 },
840 /// Manage colour palettes (colour schemes). The canonical home for what
841 /// was `dsc palette`.
842 Palette {
843 #[command(subcommand)]
844 command: PaletteCommand,
845 },
846}
847
848#[derive(Subcommand)]
849pub enum ThemeSettingCommand {
850 /// List a theme/component's settings.
851 #[command(visible_alias = "ls")]
852 List {
853 /// Discourse name.
854 discourse: String,
855 /// Theme ID (from `dsc theme list`).
856 theme_id: u64,
857 /// Output format.
858 #[arg(long, short = 'f', value_enum, default_value = "text")]
859 format: ListFormat,
860 },
861 /// Print a single setting's current value.
862 Get {
863 /// Discourse name.
864 discourse: String,
865 /// Theme ID.
866 theme_id: u64,
867 /// Setting key (the `setting` name from `theme setting list`).
868 key: String,
869 /// Output format.
870 #[arg(long, short = 'f', value_enum, default_value = "text")]
871 format: ListFormat,
872 },
873 /// Set a single setting. Value is sent verbatim (pass JSON text for
874 /// json-schema list settings). Honours global `--dry-run`.
875 Set {
876 /// Discourse name.
877 discourse: String,
878 /// Theme ID.
879 theme_id: u64,
880 /// Setting key.
881 key: String,
882 /// New value (verbatim).
883 value: String,
884 },
885}
886
887#[derive(Subcommand)]
888pub enum PmCommand {
889 /// Send a private message.
890 #[command(visible_alias = "s")]
891 Send {
892 /// Discourse name.
893 discourse: String,
894 /// Recipient(s) — comma-separated usernames or group names.
895 recipients: String,
896 /// PM title / subject.
897 #[arg(long, short = 't')]
898 title: String,
899 /// Input file path. Reads stdin when omitted or `-`.
900 local_path: Option<PathBuf>,
901 },
902 /// List PMs for a user.
903 #[command(visible_alias = "ls")]
904 List {
905 /// Discourse name.
906 discourse: String,
907 /// Username whose PMs to list.
908 username: String,
909 /// Direction / view: inbox | sent | archive | unread | new.
910 #[arg(long, short = 'd', default_value = "inbox")]
911 direction: String,
912 /// Output format.
913 #[arg(long, short = 'f', value_enum, default_value = "text")]
914 format: ListFormat,
915 },
916}
917
918#[derive(Subcommand)]
919pub enum ApiKeyCommand {
920 /// List API keys.
921 #[command(visible_alias = "ls")]
922 List {
923 /// Discourse name.
924 discourse: String,
925 /// Output format.
926 #[arg(long, short = 'f', value_enum, default_value = "text")]
927 format: ListFormat,
928 },
929 /// Create a new API key. The secret is only shown at creation time —
930 /// capture it from the output.
931 #[command(visible_alias = "cr")]
932 Create {
933 /// Discourse name.
934 discourse: String,
935 /// Description / label for the key (shown in admin UI).
936 description: String,
937 /// Username the key acts as. Omit for a global all-users key.
938 #[arg(long, short = 'u')]
939 username: Option<String>,
940 /// Output format.
941 #[arg(long, short = 'f', value_enum, default_value = "text")]
942 format: ListFormat,
943 },
944 /// Revoke an API key by ID.
945 #[command(visible_alias = "rm")]
946 Revoke {
947 /// Discourse name.
948 discourse: String,
949 /// API key ID (from `dsc api-key list`).
950 key_id: u64,
951 },
952}
953
954#[derive(Subcommand)]
955pub enum InviteCommand {
956 /// Invite a single email address.
957 #[command(visible_alias = "s")]
958 Send {
959 /// Discourse name.
960 discourse: String,
961 /// Email address to invite.
962 email: String,
963 /// Add invitee to one or more groups on accept (repeatable).
964 #[arg(long, short = 'g')]
965 group: Vec<u64>,
966 /// Land the invitee on a specific topic on accept.
967 #[arg(long, short = 't')]
968 topic: Option<u64>,
969 /// Custom invitation message.
970 #[arg(long, short = 'm')]
971 message: Option<String>,
972 },
973 /// Bulk-invite from a file (or stdin) of email addresses.
974 #[command(visible_alias = "b")]
975 Bulk {
976 /// Discourse name.
977 discourse: String,
978 /// Path to a file of email addresses (one per line; blank lines and
979 /// `#` comments ignored). Reads stdin when omitted or `-`.
980 local_path: Option<PathBuf>,
981 /// Add every invitee to one or more groups on accept (repeatable).
982 #[arg(long, short = 'g')]
983 group: Vec<u64>,
984 /// Land every invitee on a specific topic on accept.
985 #[arg(long, short = 't')]
986 topic: Option<u64>,
987 /// Custom invitation message attached to each invite.
988 #[arg(long, short = 'm')]
989 message: Option<String>,
990 },
991}
992
993#[derive(Subcommand)]
994pub enum UserCommand {
995 /// List users via the admin users endpoint.
996 #[command(visible_alias = "ls")]
997 List {
998 /// Discourse name.
999 discourse: String,
1000 /// Listing type: active | new | staff | suspended | silenced | staged.
1001 #[arg(long, short = 'l', default_value = "active")]
1002 listing: String,
1003 /// Page number (Discourse paginates 100 per page).
1004 #[arg(long, short = 'p', default_value_t = 1)]
1005 page: u32,
1006 /// Output format.
1007 #[arg(long, short = 'f', value_enum, default_value = "text")]
1008 format: ListFormat,
1009 },
1010 /// Show detailed info for a user.
1011 #[command(visible_alias = "i")]
1012 Info {
1013 /// Discourse name.
1014 discourse: String,
1015 /// Username.
1016 username: String,
1017 /// Output format.
1018 #[arg(long, short = 'f', value_enum, default_value = "text")]
1019 format: ListFormat,
1020 },
1021 /// Suspend a user.
1022 #[command(visible_alias = "sus")]
1023 Suspend {
1024 /// Discourse name.
1025 discourse: String,
1026 /// Username.
1027 username: String,
1028 /// When the suspension ends. ISO-8601 timestamp (e.g.
1029 /// `2026-12-31T00:00:00Z`) or `forever`.
1030 #[arg(long, short = 'u', default_value = "forever")]
1031 until: String,
1032 /// Reason shown to the user and in the audit log.
1033 #[arg(long, short = 'r', default_value = "")]
1034 reason: String,
1035 },
1036 /// Remove a suspension from a user.
1037 #[command(visible_alias = "uns")]
1038 Unsuspend {
1039 /// Discourse name.
1040 discourse: String,
1041 /// Username.
1042 username: String,
1043 },
1044 /// Silence a user (prevents posting; less visible than suspend).
1045 #[command(visible_alias = "sil")]
1046 Silence {
1047 /// Discourse name.
1048 discourse: String,
1049 /// Username.
1050 username: String,
1051 /// When the silence ends. ISO-8601 timestamp; empty means
1052 /// indefinite.
1053 #[arg(long, short = 'u', default_value = "")]
1054 until: String,
1055 /// Reason shown to the user and in the audit log.
1056 #[arg(long, short = 'r', default_value = "")]
1057 reason: String,
1058 },
1059 /// Lift a silence on a user.
1060 #[command(visible_alias = "unsil")]
1061 Unsilence {
1062 /// Discourse name.
1063 discourse: String,
1064 /// Username.
1065 username: String,
1066 },
1067 /// Grant the user the admin or moderator role.
1068 #[command(visible_alias = "pr")]
1069 Promote {
1070 /// Discourse name.
1071 discourse: String,
1072 /// Username.
1073 username: String,
1074 /// Role to grant.
1075 #[arg(long, short = 'r', value_enum)]
1076 role: RoleArg,
1077 },
1078 /// Revoke the user's admin or moderator role.
1079 #[command(visible_alias = "de")]
1080 Demote {
1081 /// Discourse name.
1082 discourse: String,
1083 /// Username.
1084 username: String,
1085 /// Role to revoke.
1086 #[arg(long, short = 'r', value_enum)]
1087 role: RoleArg,
1088 },
1089 /// Create a new user. `--approve` also marks the account approved
1090 /// (needed when site requires manual approval). Password is either
1091 /// supplied via stdin (`--password-stdin`) or omitted — in the
1092 /// latter case the user will have to set one via the reset flow.
1093 #[command(visible_alias = "cr")]
1094 Create {
1095 /// Discourse name.
1096 discourse: String,
1097 /// New user's email address.
1098 email: String,
1099 /// New user's username.
1100 username: String,
1101 /// Display name (optional).
1102 #[arg(long, short = 'N')]
1103 name: Option<String>,
1104 /// Read the password from stdin instead of auto-reset.
1105 #[arg(long)]
1106 password_stdin: bool,
1107 /// Also mark the user approved (for sites with manual approval).
1108 #[arg(long)]
1109 approve: bool,
1110 },
1111 /// Trigger Discourse's password-reset email flow for a user.
1112 #[command(name = "password-reset", visible_aliases = ["pwreset", "pw-reset"])]
1113 PasswordReset {
1114 /// Discourse name.
1115 discourse: String,
1116 /// Username or email.
1117 username: String,
1118 },
1119 /// Set a user's primary email address. Requires admin scope.
1120 #[command(name = "email-set", visible_alias = "email")]
1121 EmailSet {
1122 /// Discourse name.
1123 discourse: String,
1124 /// Username.
1125 username: String,
1126 /// New email address.
1127 email: String,
1128 },
1129 /// Show a user's recent public activity (topics + replies by default).
1130 ///
1131 /// Built for the "archive my own activity to a journal forum" loop —
1132 /// pipe the markdown output straight into `dsc topic reply`/`topic new`.
1133 #[command(visible_alias = "act")]
1134 Activity {
1135 /// Discourse name (the *source* forum to read activity from).
1136 discourse: String,
1137 /// Username whose activity to read.
1138 username: String,
1139 /// How far back to look. Accepts `7d`, `24h`, `30m`, `1w`, `90s`, or
1140 /// an ISO-8601 timestamp / date. Omit to fetch everything available.
1141 #[arg(long, short = 's')]
1142 since: Option<String>,
1143 /// Action types to include, comma-separated. Default: topics,replies.
1144 /// Also recognises: mentions, quotes, likes, edits, responses.
1145 #[arg(long, short = 't', default_value = "topics,replies")]
1146 types: String,
1147 /// Hard cap on number of items returned.
1148 #[arg(long, short = 'L')]
1149 limit: Option<u32>,
1150 /// Output format.
1151 #[arg(long, short = 'f', value_enum, default_value = "markdown")]
1152 format: ActivityFormatArg,
1153 },
1154 /// Manage a user's group memberships.
1155 #[command(visible_alias = "g")]
1156 Groups {
1157 #[command(subcommand)]
1158 command: UserGroupsCommand,
1159 },
1160}
1161
1162#[derive(ValueEnum, Clone, Copy)]
1163pub enum SectionArg {
1164 All,
1165 Growth,
1166 Activity,
1167 Health,
1168}
1169
1170#[derive(ValueEnum, Clone, Copy)]
1171pub enum AnalyticsFormat {
1172 /// Plain text (default). Fixed-width columns, no borders.
1173 Text,
1174 /// DuckDB-style box-drawing table. Falls through to `text` when
1175 /// stdout isn't a TTY.
1176 Table,
1177 /// Pretty JSON.
1178 Json,
1179 /// YAML.
1180 #[value(alias = "yml")]
1181 Yaml,
1182 /// Markdown bullet list per section.
1183 #[value(alias = "md")]
1184 Markdown,
1185 /// Markdown table per section.
1186 #[value(alias = "md-table", name = "markdown-table")]
1187 MarkdownTable,
1188 /// CSV — one row per metric.
1189 Csv,
1190}
1191
1192#[derive(ValueEnum, Clone, Copy)]
1193pub enum ActivityFormatArg {
1194 Text,
1195 Json,
1196 #[value(alias = "yml")]
1197 Yaml,
1198 #[value(alias = "md")]
1199 Markdown,
1200 Csv,
1201}
1202
1203#[derive(ValueEnum, Clone, Copy)]
1204pub enum RoleArg {
1205 Admin,
1206 Moderator,
1207}
1208
1209#[derive(Subcommand)]
1210pub enum UserGroupsCommand {
1211 /// List the groups a user belongs to.
1212 #[command(visible_alias = "ls")]
1213 List {
1214 /// Discourse name.
1215 discourse: String,
1216 /// Target username.
1217 username: String,
1218 /// Output format.
1219 #[arg(long, short = 'f', value_enum, default_value = "text")]
1220 format: ListFormat,
1221 },
1222 /// Add a user to a group.
1223 #[command(visible_alias = "a")]
1224 Add {
1225 /// Discourse name.
1226 discourse: String,
1227 /// Target username.
1228 username: String,
1229 /// Group ID.
1230 group_id: u64,
1231 /// Send Discourse notification to the user.
1232 #[arg(long)]
1233 notify: bool,
1234 },
1235 /// Remove a user from a group.
1236 #[command(visible_alias = "rm")]
1237 Remove {
1238 /// Discourse name.
1239 discourse: String,
1240 /// Target username.
1241 username: String,
1242 /// Group ID.
1243 group_id: u64,
1244 },
1245}
1246
1247#[derive(Subcommand)]
1248pub enum PostCommand {
1249 /// Pull a post's raw Markdown to a local file.
1250 #[command(visible_alias = "pl")]
1251 Pull {
1252 /// Discourse name.
1253 discourse: String,
1254 /// Post ID.
1255 post_id: u64,
1256 /// Output file path. Prints to stdout when omitted.
1257 local_path: Option<PathBuf>,
1258 },
1259 /// Push a local file to update a post (alias: edit).
1260 #[command(visible_alias = "ps", alias = "edit")]
1261 Push {
1262 /// Discourse name.
1263 discourse: String,
1264 /// Post ID.
1265 post_id: u64,
1266 /// Input file path. Reads stdin when omitted or `-`.
1267 local_path: Option<PathBuf>,
1268 },
1269 /// Delete a post by ID.
1270 #[command(visible_alias = "rm")]
1271 Delete {
1272 /// Discourse name.
1273 discourse: String,
1274 /// Post ID.
1275 post_id: u64,
1276 },
1277 /// Move a post to a different topic.
1278 #[command(visible_alias = "mv")]
1279 Move {
1280 /// Discourse name.
1281 discourse: String,
1282 /// Post ID to move.
1283 post_id: u64,
1284 /// Destination topic ID.
1285 #[arg(long = "to-topic", short = 't')]
1286 to_topic: u64,
1287 },
1288}
1289
1290#[derive(Subcommand)]
1291pub enum TagCommand {
1292 /// List every tag on the Discourse.
1293 #[command(visible_alias = "ls")]
1294 List {
1295 /// Discourse name.
1296 discourse: String,
1297 /// Output format.
1298 #[arg(long, short = 'f', value_enum, default_value = "text")]
1299 format: ListFormat,
1300 },
1301 /// Pull the tag taxonomy (tags + tag groups) to a local file.
1302 #[command(visible_alias = "pl")]
1303 Pull {
1304 /// Discourse name.
1305 discourse: String,
1306 /// Output file (default: tags.yaml). Extension determines format (.yaml/.json).
1307 #[arg(default_value = "tags.yaml")]
1308 local_path: PathBuf,
1309 },
1310 /// Push a local taxonomy file to the server (upsert; optionally prune).
1311 #[command(visible_alias = "ps")]
1312 Push {
1313 /// Discourse name.
1314 discourse: String,
1315 /// Input taxonomy file.
1316 local_path: PathBuf,
1317 /// Delete server tags/groups absent from the file.
1318 #[arg(long)]
1319 prune: bool,
1320 },
1321 /// Rename a tag, preserving topic associations.
1322 ///
1323 /// Discourse rewrites every topic's tag list in-place, so this avoids
1324 /// the delete-and-recreate pattern that loses topic membership.
1325 #[command(visible_alias = "rn")]
1326 Rename {
1327 /// Discourse name.
1328 discourse: String,
1329 /// Current tag name.
1330 old_name: String,
1331 /// New tag name.
1332 new_name: String,
1333 },
1334}
1335
1336#[derive(Subcommand)]
1337pub enum SettingCommand {
1338 /// Set a site setting on a Discourse (or all tagged Discourses).
1339 ///
1340 /// Usage:
1341 /// dsc setting set <discourse> <setting> <value>
1342 /// dsc setting set --tags <tag1,tag2> <setting> <value>
1343 #[command(visible_alias = "s")]
1344 Set {
1345 /// Discourse name. Required unless `--tags` is provided.
1346 discourse: Option<String>,
1347 /// Setting key. Required.
1348 setting: Option<String>,
1349 /// Setting value. Required.
1350 value: Option<String>,
1351 /// Tag filter (comma/semicolon separated, match-any). Apply across all
1352 /// Discourses matching any of the tags. When set, omit `<discourse>`
1353 /// and pass `<setting> <value>` as the only positionals.
1354 #[arg(long, value_name = "tag1,tag2")]
1355 tags: Option<String>,
1356 },
1357
1358 /// Get the current value of a site setting.
1359 #[command(visible_alias = "g")]
1360 Get {
1361 /// Discourse name.
1362 discourse: String,
1363 /// Setting key.
1364 setting: String,
1365 /// Output format.
1366 #[arg(long, short = 'f', value_enum, default_value = "text")]
1367 format: ListFormat,
1368 },
1369
1370 /// List all site settings.
1371 #[command(visible_alias = "ls")]
1372 List {
1373 /// Discourse name.
1374 discourse: String,
1375 /// Output format.
1376 #[arg(long, short = 'f', value_enum, default_value = "text")]
1377 format: ListFormat,
1378 /// Show output even when list is empty.
1379 #[arg(long, short = 'v')]
1380 verbose: bool,
1381 },
1382
1383 /// Snapshot all site settings (with metadata) to a local file.
1384 ///
1385 /// See spec/setting-sync.md for the full schema and workflow. The
1386 /// generated file is a self-documenting YAML (or JSON) including each
1387 /// setting's default, type, category, and description.
1388 #[command(visible_alias = "pl")]
1389 Pull {
1390 /// Discourse name.
1391 discourse: String,
1392 /// Output path. Format detected by extension (.json → JSON,
1393 /// otherwise YAML). Defaults to `settings.yaml`.
1394 #[arg(default_value = "settings.yaml")]
1395 local_path: PathBuf,
1396 /// Only include settings whose value differs from default. Produces
1397 /// a manageable file (~50-100 entries) suitable for version control.
1398 #[arg(long, short = 'c')]
1399 changed_only: bool,
1400 /// Limit to settings in this category (e.g. `required`, `email`,
1401 /// `security`).
1402 #[arg(long)]
1403 category: Option<String>,
1404 },
1405
1406 /// Apply a settings snapshot file to a Discourse (idempotent).
1407 ///
1408 /// Compares each setting in the file against the server and PUTs only
1409 /// values that differ. Combine with `--dry-run` to preview the plan.
1410 #[command(visible_alias = "ph")]
1411 Push {
1412 /// Discourse name.
1413 discourse: String,
1414 /// Path to the settings snapshot file (YAML or JSON).
1415 local_path: PathBuf,
1416 /// For settings present on the server but absent from the file,
1417 /// reset them to their default value. Off by default (file describes
1418 /// only the values you care about).
1419 #[arg(long)]
1420 reset_unlisted: bool,
1421 },
1422
1423 /// Compare site settings between two sources.
1424 ///
1425 /// Each source can be a Discourse name (live fetch) or a path to a
1426 /// snapshot file produced by `dsc setting pull`. Sources are detected
1427 /// by whether the argument refers to an existing file on disk; if not,
1428 /// it is treated as a Discourse name.
1429 #[command(visible_alias = "df")]
1430 Diff {
1431 /// First source: Discourse name or snapshot file path.
1432 source: String,
1433 /// Second source: Discourse name or snapshot file path.
1434 target: String,
1435 /// Filter to settings where at least one source differs from default.
1436 /// Reduces noise when most settings on both sides are still default.
1437 #[arg(long, short = 'c')]
1438 changed_only: bool,
1439 /// Limit to settings in this category (e.g. `required`, `email`).
1440 /// Only effective when both sources carry category metadata.
1441 #[arg(long)]
1442 category: Option<String>,
1443 /// Output format.
1444 #[arg(long, short = 'f', value_enum, default_value = "text")]
1445 format: ListFormat,
1446 },
1447
1448 /// Show the value of one setting across every configured forum
1449 /// (optionally filtered by `--tags`). Diff-friendly; distinct from `diff`,
1450 /// which compares two specific sources across all settings.
1451 Audit {
1452 /// Setting key.
1453 setting: String,
1454 /// Only audit forums carrying at least one of these tags
1455 /// (comma/semicolon-separated). Omit to audit every configured forum.
1456 #[arg(long, value_name = "tag1,tag2")]
1457 tags: Option<String>,
1458 /// Output format.
1459 #[arg(long, short = 'f', value_enum, default_value = "text")]
1460 format: ListFormat,
1461 },
1462}
1463
1464#[derive(ValueEnum, Clone, Copy)]
1465pub enum CompletionShell {
1466 /// Bash shell.
1467 Bash,
1468 /// Zsh shell.
1469 Zsh,
1470 /// Fish shell.
1471 Fish,
1472}
1473
1474impl From<CompletionShell> for Shell {
1475 fn from(value: CompletionShell) -> Self {
1476 match value {
1477 CompletionShell::Bash => Shell::Bash,
1478 CompletionShell::Zsh => Shell::Zsh,
1479 CompletionShell::Fish => Shell::Fish,
1480 }
1481 }
1482}
1483
1484#[derive(ValueEnum, Clone)]
1485pub enum OutputFormat {
1486 /// Plain text.
1487 #[value(alias = "plaintext")]
1488 Text,
1489 /// Markdown list.
1490 Markdown,
1491 /// Markdown table.
1492 MarkdownTable,
1493 /// Pretty JSON.
1494 Json,
1495 /// YAML.
1496 #[value(alias = "yml")]
1497 Yaml,
1498 /// CSV.
1499 Csv,
1500 /// One base URL per line (pipe-friendly).
1501 #[value(alias = "url")]
1502 Urls,
1503}
1504
1505#[derive(ValueEnum, Clone, Copy)]
1506pub enum ListFormat {
1507 /// Plain text.
1508 Text,
1509 /// Pretty JSON.
1510 Json,
1511 /// YAML.
1512 #[value(alias = "yml")]
1513 Yaml,
1514}
1515
1516#[derive(ValueEnum, Clone, Copy)]
1517pub enum StructuredFormat {
1518 /// Pretty JSON.
1519 Json,
1520 /// YAML.
1521 #[value(alias = "yml")]
1522 Yaml,
1523}