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 searches standard locations.
10 #[arg(long, short = 'c')]
11 pub config: Option<PathBuf>,
12 /// Describe destructive actions without sending them. Read-only commands
13 /// ignore the flag.
14 #[arg(long, short = 'n', global = true)]
15 pub dry_run: bool,
16 #[command(subcommand)]
17 pub command: Commands,
18}
19
20#[derive(Subcommand)]
21pub enum Commands {
22 /// List configured Discourses.
23 #[command(visible_alias = "ls")]
24 List {
25 /// Output format for the listing.
26 #[arg(long, short = 'f', value_enum, default_value = "text")]
27 format: OutputFormat,
28 /// Filter by tags (comma/semicolon separated, match-any).
29 #[arg(long, value_name = "tag1,tag2")]
30 tags: Option<String>,
31 /// Open each listed Discourse base URL in a browser tab/window.
32 #[arg(long, short = 'o')]
33 open: bool,
34 /// Include empty results and verbose listing details where supported.
35 #[arg(long, short = 'v')]
36 verbose: bool,
37 #[command(subcommand)]
38 command: Option<ListCommand>,
39 },
40 /// Add one or more Discourses to the config.
41 #[command(visible_alias = "a")]
42 Add {
43 /// Comma-separated discourse names to add.
44 names: String,
45 /// Prompt for additional optional fields while adding.
46 #[arg(long, short = 'i')]
47 interactive: bool,
48 },
49 /// Import Discourses from a file or stdin.
50 #[command(visible_alias = "imp")]
51 Import {
52 /// Path to import input (text/CSV). Reads stdin when omitted.
53 path: Option<PathBuf>,
54 },
55 /// Run remote OS + Discourse update workflow for one or all Discourses.
56 #[command(visible_alias = "up")]
57 Update {
58 /// Discourse name, or 'all' to update every configured Discourse.
59 name: String,
60 /// Parallel update mode for `dsc update all`.
61 #[arg(long, short = 'p')]
62 parallel: bool,
63 /// Maximum workers when parallel mode is enabled (default: 3).
64 #[arg(long, short = 'm')]
65 max: Option<usize>,
66 /// Disable changelog posting (posting prompt is on by default).
67 #[arg(long = "no-changelog", action = ArgAction::SetFalse, default_value_t = true)]
68 post_changelog: bool,
69 /// Auto-confirm changelog posting prompt (non-interactive mode).
70 #[arg(long, short = 'y')]
71 yes: bool,
72 },
73 /// Manage custom emoji.
74 #[command(visible_alias = "em")]
75 Emoji {
76 #[command(subcommand)]
77 command: EmojiCommand,
78 },
79 /// Pull/push/sync topics as local Markdown.
80 #[command(visible_alias = "t")]
81 Topic {
82 #[command(subcommand)]
83 command: TopicCommand,
84 },
85 /// List/copy/pull/push categories.
86 #[command(visible_alias = "cat")]
87 Category {
88 #[command(subcommand)]
89 command: CategoryCommand,
90 },
91 /// List/inspect/copy groups.
92 #[command(visible_alias = "grp")]
93 Group {
94 #[command(subcommand)]
95 command: GroupCommand,
96 },
97 /// Operations that act from a user's perspective.
98 #[command(visible_alias = "usr")]
99 User {
100 #[command(subcommand)]
101 command: UserCommand,
102 },
103 /// Send invites — single or bulk from a file.
104 #[command(visible_alias = "inv")]
105 Invite {
106 #[command(subcommand)]
107 command: InviteCommand,
108 },
109 /// Create/list/restore backups.
110 #[command(visible_alias = "bk")]
111 Backup {
112 #[command(subcommand)]
113 command: BackupCommand,
114 },
115 /// List/pull/push color palettes.
116 #[command(visible_alias = "pal")]
117 Palette {
118 #[command(subcommand)]
119 command: PaletteCommand,
120 },
121 /// List/install/remove plugins.
122 #[command(visible_alias = "plg")]
123 Plugin {
124 #[command(subcommand)]
125 command: PluginCommand,
126 },
127 /// List/install/remove/pull/push/duplicate themes.
128 #[command(visible_alias = "th")]
129 Theme {
130 #[command(subcommand)]
131 command: ThemeCommand,
132 },
133 /// Update site settings.
134 #[command(visible_alias = "set")]
135 Setting {
136 #[command(subcommand)]
137 command: SettingCommand,
138 },
139 /// List tags and apply/remove them on topics.
140 #[command(visible_alias = "tg")]
141 Tag {
142 #[command(subcommand)]
143 command: TagCommand,
144 },
145 /// Post-level operations: edit / delete / move.
146 #[command(visible_alias = "po")]
147 Post {
148 #[command(subcommand)]
149 command: PostCommand,
150 },
151 /// Open a Discourse in the default browser.
152 #[command(visible_alias = "o")]
153 Open {
154 /// Discourse name.
155 discourse: String,
156 },
157 /// Search topics on a Discourse.
158 #[command(visible_alias = "s")]
159 Search {
160 /// Discourse name.
161 discourse: String,
162 /// Search query (passed through verbatim, including any
163 /// Discourse filter syntax like `category:foo` or `@user`).
164 query: String,
165 /// Output format.
166 #[arg(long, short = 'f', value_enum, default_value = "text")]
167 format: ListFormat,
168 },
169 /// Upload a file. Prints the resulting upload:// short URL by default.
170 #[command(visible_alias = "u")]
171 Upload {
172 /// Discourse name.
173 discourse: String,
174 /// Path to the file to upload.
175 file: PathBuf,
176 /// Discourse upload context. Default `composer` is correct for
177 /// embedding in posts; other values include `avatar`,
178 /// `profile_background`, `card_background`, `custom_emoji`.
179 #[arg(long, short = 't', default_value = "composer")]
180 upload_type: String,
181 /// Output format. Text mode prints just the short URL.
182 #[arg(long, short = 'f', value_enum, default_value = "text")]
183 format: ListFormat,
184 },
185 /// Inspect and validate configuration.
186 #[command(visible_alias = "cfg")]
187 Config {
188 #[command(subcommand)]
189 command: ConfigCommand,
190 },
191 /// Generate shell completion scripts.
192 #[command(visible_alias = "comp")]
193 Completions {
194 /// Target shell.
195 #[arg(value_enum)]
196 shell: CompletionShell,
197 /// Output directory. Prints to stdout when omitted.
198 #[arg(long, short = 'd')]
199 dir: Option<PathBuf>,
200 },
201 /// Print the dsc version.
202 #[command(visible_alias = "ver")]
203 Version,
204}
205
206#[derive(Subcommand)]
207pub enum ConfigCommand {
208 /// Probe each configured Discourse: API auth and (optionally) SSH reachability.
209 #[command(visible_alias = "ck")]
210 Check {
211 /// Output format.
212 #[arg(long, short = 'f', value_enum, default_value = "text")]
213 format: ListFormat,
214 /// Skip the SSH reachability probe.
215 #[arg(long)]
216 skip_ssh: bool,
217 },
218}
219
220#[derive(Subcommand)]
221pub enum ListCommand {
222 /// Sort discourse entries by name and rewrite config in-place.
223 /// Also inserts placeholder values for unset template keys.
224 #[command(visible_alias = "ty")]
225 Tidy,
226}
227
228#[derive(Subcommand)]
229pub enum EmojiCommand {
230 /// Upload one emoji file, or bulk-upload from a directory.
231 #[command(visible_alias = "a")]
232 Add {
233 /// Discourse name.
234 discourse: String,
235 /// Local file or directory path.
236 emoji_path: PathBuf,
237 /// Optional emoji name (file uploads only).
238 emoji_name: Option<String>,
239 },
240
241 /// List custom emojis on a Discourse.
242 #[command(visible_alias = "ls")]
243 List {
244 /// Discourse name.
245 discourse: String,
246 /// Output format.
247 #[arg(long, short = 'f', value_enum, default_value = "text")]
248 format: ListFormat,
249 /// Include additional fields where supported.
250 #[arg(long, short = 'v')]
251 verbose: bool,
252 /// Render inline images when terminal protocol support is available.
253 #[arg(long, short = 'i')]
254 inline: bool,
255 },
256}
257
258#[derive(Subcommand)]
259pub enum TopicCommand {
260 /// Pull a topic to a local Markdown file.
261 #[command(visible_alias = "pl")]
262 Pull {
263 /// Discourse name.
264 discourse: String,
265 /// Topic ID.
266 topic_id: u64,
267 /// Destination file or directory (auto-derived when omitted).
268 local_path: Option<PathBuf>,
269 },
270 /// Push a local Markdown file to a topic.
271 #[command(visible_alias = "ps")]
272 Push {
273 /// Discourse name.
274 discourse: String,
275 /// Topic ID.
276 topic_id: u64,
277 /// Local Markdown file path.
278 local_path: PathBuf,
279 },
280 /// Sync a topic and local Markdown file using newest timestamp.
281 #[command(visible_alias = "sy")]
282 Sync {
283 /// Discourse name.
284 discourse: String,
285 /// Topic ID.
286 topic_id: u64,
287 /// Local Markdown file path.
288 local_path: PathBuf,
289 /// Skip sync confirmation prompt.
290 #[arg(long, short = 'y')]
291 yes: bool,
292 },
293 /// Reply to a topic with content from a file or stdin.
294 #[command(visible_alias = "r")]
295 Reply {
296 /// Discourse name.
297 discourse: String,
298 /// Topic ID.
299 topic_id: u64,
300 /// Input file path. Reads stdin when omitted or `-`.
301 local_path: Option<PathBuf>,
302 },
303 /// Create a new topic in a category, body from a file or stdin.
304 #[command(visible_alias = "n")]
305 New {
306 /// Discourse name.
307 discourse: String,
308 /// Target category ID.
309 category_id: u64,
310 /// Topic title.
311 #[arg(long, short = 't')]
312 title: String,
313 /// Input file path. Reads stdin when omitted or `-`.
314 local_path: Option<PathBuf>,
315 },
316}
317
318#[derive(Subcommand)]
319pub enum CategoryCommand {
320 /// List categories.
321 #[command(visible_alias = "ls")]
322 List {
323 /// Discourse name.
324 discourse: String,
325 /// Output format.
326 #[arg(long, short = 'f', value_enum, default_value = "text")]
327 format: ListFormat,
328 /// Include additional fields where supported.
329 #[arg(long, short = 'v')]
330 verbose: bool,
331 /// Show category hierarchy tree.
332 #[arg(long)]
333 tree: bool,
334 },
335 /// Copy a category to another Discourse.
336 #[command(visible_alias = "cp")]
337 Copy {
338 /// Source discourse name.
339 discourse: String,
340 /// Target discourse name (defaults to source when omitted).
341 #[arg(long, short = 't')]
342 target: Option<String>,
343 /// Category ID or slug.
344 category: String,
345 },
346 /// Pull all topics from a category into local Markdown files.
347 #[command(visible_alias = "pl")]
348 Pull {
349 /// Discourse name.
350 discourse: String,
351 /// Category ID or slug.
352 category: String,
353 /// Destination directory (auto-derived when omitted).
354 local_path: Option<PathBuf>,
355 },
356 /// Push local Markdown files into a category.
357 #[command(visible_alias = "ps")]
358 Push {
359 /// Discourse name.
360 discourse: String,
361 /// Category ID or slug.
362 category: String,
363 /// Local directory containing Markdown files.
364 local_path: PathBuf,
365 },
366}
367
368#[derive(Subcommand)]
369pub enum GroupCommand {
370 /// List groups.
371 #[command(visible_alias = "ls")]
372 List {
373 /// Discourse name.
374 discourse: String,
375 /// Output format.
376 #[arg(long, short = 'f', value_enum, default_value = "text")]
377 format: ListFormat,
378 /// Include additional fields where supported.
379 #[arg(long, short = 'v')]
380 verbose: bool,
381 },
382 /// Show group details.
383 #[command(visible_alias = "i")]
384 Info {
385 /// Discourse name.
386 discourse: String,
387 /// Group ID.
388 group: u64,
389 /// Output format.
390 #[arg(long, short = 'f', value_enum, default_value = "json")]
391 format: StructuredFormat,
392 },
393 /// List members of a group.
394 #[command(visible_alias = "m")]
395 Members {
396 /// Discourse name.
397 discourse: String,
398 /// Group ID.
399 group: u64,
400 /// Output format.
401 #[arg(long, short = 'f', value_enum, default_value = "text")]
402 format: ListFormat,
403 },
404 /// Copy a group to another Discourse.
405 #[command(visible_alias = "cp")]
406 Copy {
407 /// Source discourse name.
408 discourse: String,
409 /// Target discourse name (defaults to source when omitted).
410 #[arg(long, short = 't')]
411 target: Option<String>,
412 /// Group ID.
413 group: u64,
414 },
415 /// Bulk add members to a group from a file (or stdin) of email addresses.
416 #[command(visible_alias = "a")]
417 Add {
418 /// Discourse name.
419 discourse: String,
420 /// Group ID.
421 group: u64,
422 /// Path to a file of email addresses (one per line; blank
423 /// lines and `#` comments are ignored). Reads stdin when
424 /// omitted or `-`.
425 local_path: Option<PathBuf>,
426 /// Send Discourse notifications to added users.
427 #[arg(long)]
428 notify: bool,
429 },
430}
431
432#[derive(Subcommand)]
433pub enum BackupCommand {
434 /// Create a new backup.
435 #[command(visible_alias = "cr")]
436 Create {
437 /// Discourse name.
438 discourse: String,
439 },
440 /// List backups.
441 #[command(visible_alias = "ls")]
442 List {
443 /// Discourse name.
444 discourse: String,
445 /// Output format.
446 #[arg(long, short = 'f', value_enum, default_value = "text")]
447 format: OutputFormat,
448 /// Include additional fields where supported.
449 #[arg(long, short = 'v')]
450 verbose: bool,
451 },
452 /// Restore a backup.
453 #[command(visible_alias = "rs")]
454 Restore {
455 /// Discourse name.
456 discourse: String,
457 /// Backup filename/path on the target system.
458 backup_path: String,
459 },
460}
461
462#[derive(Subcommand)]
463pub enum PaletteCommand {
464 /// List color palettes.
465 #[command(visible_alias = "ls")]
466 List {
467 /// Discourse name.
468 discourse: String,
469 /// Output format.
470 #[arg(long, short = 'f', value_enum, default_value = "text")]
471 format: ListFormat,
472 /// Include additional fields where supported.
473 #[arg(long, short = 'v')]
474 verbose: bool,
475 },
476 /// Pull a palette to local JSON.
477 #[command(visible_alias = "pl")]
478 Pull {
479 /// Discourse name.
480 discourse: String,
481 /// Palette ID.
482 palette_id: u64,
483 /// Destination file path (auto-derived when omitted).
484 local_path: Option<PathBuf>,
485 },
486 /// Push local JSON to create or update a palette.
487 #[command(visible_alias = "ps")]
488 Push {
489 /// Discourse name.
490 discourse: String,
491 /// Local JSON file path.
492 local_path: PathBuf,
493 /// Palette ID to update (creates a new palette when omitted).
494 palette_id: Option<u64>,
495 },
496}
497
498#[derive(Subcommand)]
499pub enum PluginCommand {
500 /// List installed plugins.
501 #[command(visible_alias = "ls")]
502 List {
503 /// Discourse name.
504 discourse: String,
505 /// Output format.
506 #[arg(long, short = 'f', value_enum, default_value = "text")]
507 format: ListFormat,
508 /// Include additional fields where supported.
509 #[arg(long, short = 'v')]
510 verbose: bool,
511 },
512 /// Install a plugin from URL.
513 #[command(visible_alias = "i")]
514 Install {
515 /// Discourse name.
516 discourse: String,
517 /// Plugin repository URL.
518 url: String,
519 },
520 /// Remove a plugin by name.
521 #[command(visible_alias = "rm")]
522 Remove {
523 /// Discourse name.
524 discourse: String,
525 /// Plugin name.
526 name: String,
527 },
528}
529
530#[derive(Subcommand)]
531pub enum ThemeCommand {
532 /// List installed themes.
533 #[command(visible_alias = "ls")]
534 List {
535 /// Discourse name.
536 discourse: String,
537 /// Output format.
538 #[arg(long, short = 'f', value_enum, default_value = "text")]
539 format: ListFormat,
540 /// Include additional fields where supported.
541 #[arg(long, short = 'v')]
542 verbose: bool,
543 },
544 /// Install a theme from URL.
545 #[command(visible_alias = "i")]
546 Install {
547 /// Discourse name.
548 discourse: String,
549 /// Theme repository URL.
550 url: String,
551 },
552 /// Remove a theme by name.
553 #[command(visible_alias = "rm")]
554 Remove {
555 /// Discourse name.
556 discourse: String,
557 /// Theme name.
558 name: String,
559 },
560 /// Pull a theme to a local JSON file.
561 #[command(visible_alias = "pl")]
562 Pull {
563 /// Discourse name.
564 discourse: String,
565 /// Theme ID (from `dsc theme list`).
566 theme_id: u64,
567 /// Destination file path (auto-derived from theme name when omitted).
568 local_path: Option<PathBuf>,
569 },
570 /// Push a local JSON file to create or update a theme.
571 #[command(visible_alias = "ps")]
572 Push {
573 /// Discourse name.
574 discourse: String,
575 /// Local JSON file path.
576 local_path: PathBuf,
577 /// Theme ID to update (creates a new theme when omitted).
578 theme_id: Option<u64>,
579 },
580 /// Duplicate a theme and print the new theme ID.
581 #[command(visible_alias = "dup")]
582 Duplicate {
583 /// Discourse name.
584 discourse: String,
585 /// Theme ID to duplicate (from `dsc theme list`).
586 theme_id: u64,
587 },
588}
589
590#[derive(Subcommand)]
591pub enum InviteCommand {
592 /// Invite a single email address.
593 #[command(visible_alias = "s")]
594 Send {
595 /// Discourse name.
596 discourse: String,
597 /// Email address to invite.
598 email: String,
599 /// Add invitee to one or more groups on accept (repeatable).
600 #[arg(long, short = 'g')]
601 group: Vec<u64>,
602 /// Land the invitee on a specific topic on accept.
603 #[arg(long, short = 't')]
604 topic: Option<u64>,
605 /// Custom invitation message.
606 #[arg(long, short = 'm')]
607 message: Option<String>,
608 },
609 /// Bulk-invite from a file (or stdin) of email addresses.
610 #[command(visible_alias = "b")]
611 Bulk {
612 /// Discourse name.
613 discourse: String,
614 /// Path to a file of email addresses (one per line; blank lines and
615 /// `#` comments ignored). Reads stdin when omitted or `-`.
616 local_path: Option<PathBuf>,
617 /// Add every invitee to one or more groups on accept (repeatable).
618 #[arg(long, short = 'g')]
619 group: Vec<u64>,
620 /// Land every invitee on a specific topic on accept.
621 #[arg(long, short = 't')]
622 topic: Option<u64>,
623 /// Custom invitation message attached to each invite.
624 #[arg(long, short = 'm')]
625 message: Option<String>,
626 },
627}
628
629#[derive(Subcommand)]
630pub enum UserCommand {
631 /// List users via the admin users endpoint.
632 #[command(visible_alias = "ls")]
633 List {
634 /// Discourse name.
635 discourse: String,
636 /// Listing type: active | new | staff | suspended | silenced | staged.
637 #[arg(long, short = 'l', default_value = "active")]
638 listing: String,
639 /// Page number (Discourse paginates 100 per page).
640 #[arg(long, short = 'p', default_value_t = 1)]
641 page: u32,
642 /// Output format.
643 #[arg(long, short = 'f', value_enum, default_value = "text")]
644 format: ListFormat,
645 },
646 /// Show detailed info for a user.
647 #[command(visible_alias = "i")]
648 Info {
649 /// Discourse name.
650 discourse: String,
651 /// Username.
652 username: String,
653 /// Output format.
654 #[arg(long, short = 'f', value_enum, default_value = "text")]
655 format: ListFormat,
656 },
657 /// Suspend a user.
658 #[command(visible_alias = "sus")]
659 Suspend {
660 /// Discourse name.
661 discourse: String,
662 /// Username.
663 username: String,
664 /// When the suspension ends. ISO-8601 timestamp (e.g.
665 /// `2026-12-31T00:00:00Z`) or `forever`.
666 #[arg(long, short = 'u', default_value = "forever")]
667 until: String,
668 /// Reason shown to the user and in the audit log.
669 #[arg(long, short = 'r', default_value = "")]
670 reason: String,
671 },
672 /// Remove a suspension from a user.
673 #[command(visible_alias = "uns")]
674 Unsuspend {
675 /// Discourse name.
676 discourse: String,
677 /// Username.
678 username: String,
679 },
680 /// Silence a user (prevents posting; less visible than suspend).
681 #[command(visible_alias = "sil")]
682 Silence {
683 /// Discourse name.
684 discourse: String,
685 /// Username.
686 username: String,
687 /// When the silence ends. ISO-8601 timestamp; empty means
688 /// indefinite.
689 #[arg(long, short = 'u', default_value = "")]
690 until: String,
691 /// Reason shown to the user and in the audit log.
692 #[arg(long, short = 'r', default_value = "")]
693 reason: String,
694 },
695 /// Lift a silence on a user.
696 #[command(visible_alias = "unsil")]
697 Unsilence {
698 /// Discourse name.
699 discourse: String,
700 /// Username.
701 username: String,
702 },
703 /// Grant the user the admin or moderator role.
704 #[command(visible_alias = "pr")]
705 Promote {
706 /// Discourse name.
707 discourse: String,
708 /// Username.
709 username: String,
710 /// Role to grant.
711 #[arg(long, short = 'r', value_enum)]
712 role: RoleArg,
713 },
714 /// Revoke the user's admin or moderator role.
715 #[command(visible_alias = "de")]
716 Demote {
717 /// Discourse name.
718 discourse: String,
719 /// Username.
720 username: String,
721 /// Role to revoke.
722 #[arg(long, short = 'r', value_enum)]
723 role: RoleArg,
724 },
725 /// Manage a user's group memberships.
726 #[command(visible_alias = "g")]
727 Groups {
728 #[command(subcommand)]
729 command: UserGroupsCommand,
730 },
731}
732
733#[derive(ValueEnum, Clone, Copy)]
734pub enum RoleArg {
735 Admin,
736 Moderator,
737}
738
739#[derive(Subcommand)]
740pub enum UserGroupsCommand {
741 /// List the groups a user belongs to.
742 #[command(visible_alias = "ls")]
743 List {
744 /// Discourse name.
745 discourse: String,
746 /// Target username.
747 username: String,
748 /// Output format.
749 #[arg(long, short = 'f', value_enum, default_value = "text")]
750 format: ListFormat,
751 },
752 /// Add a user to a group.
753 #[command(visible_alias = "a")]
754 Add {
755 /// Discourse name.
756 discourse: String,
757 /// Target username.
758 username: String,
759 /// Group ID.
760 group_id: u64,
761 /// Send Discourse notification to the user.
762 #[arg(long)]
763 notify: bool,
764 },
765 /// Remove a user from a group.
766 #[command(visible_alias = "rm")]
767 Remove {
768 /// Discourse name.
769 discourse: String,
770 /// Target username.
771 username: String,
772 /// Group ID.
773 group_id: u64,
774 },
775}
776
777#[derive(Subcommand)]
778pub enum PostCommand {
779 /// Edit a post by ID. Reads the new body from file or stdin.
780 #[command(visible_alias = "e")]
781 Edit {
782 /// Discourse name.
783 discourse: String,
784 /// Post ID.
785 post_id: u64,
786 /// Input file path. Reads stdin when omitted or `-`.
787 local_path: Option<PathBuf>,
788 },
789 /// Delete a post by ID.
790 #[command(visible_alias = "rm")]
791 Delete {
792 /// Discourse name.
793 discourse: String,
794 /// Post ID.
795 post_id: u64,
796 },
797 /// Move a post to a different topic.
798 #[command(visible_alias = "mv")]
799 Move {
800 /// Discourse name.
801 discourse: String,
802 /// Post ID to move.
803 post_id: u64,
804 /// Destination topic ID.
805 #[arg(long = "to-topic", short = 't')]
806 to_topic: u64,
807 },
808}
809
810#[derive(Subcommand)]
811pub enum TagCommand {
812 /// List every tag on the Discourse.
813 #[command(visible_alias = "ls")]
814 List {
815 /// Discourse name.
816 discourse: String,
817 /// Output format.
818 #[arg(long, short = 'f', value_enum, default_value = "text")]
819 format: ListFormat,
820 },
821 /// Add a tag to a topic.
822 #[command(visible_alias = "a")]
823 Apply {
824 /// Discourse name.
825 discourse: String,
826 /// Topic ID.
827 topic_id: u64,
828 /// Tag to add.
829 tag: String,
830 },
831 /// Remove a tag from a topic.
832 #[command(visible_alias = "rm")]
833 Remove {
834 /// Discourse name.
835 discourse: String,
836 /// Topic ID.
837 topic_id: u64,
838 /// Tag to remove.
839 tag: String,
840 },
841}
842
843#[derive(Subcommand)]
844pub enum SettingCommand {
845 /// Set a site setting on a Discourse (or all tagged Discourses).
846 #[command(visible_alias = "s")]
847 Set {
848 /// Discourse name. Required when targeting a single discourse.
849 discourse: String,
850 /// Setting key.
851 setting: String,
852 /// Setting value.
853 value: String,
854 /// Optional tag filter (comma/semicolon separated, match-any). Ignored when discourse is specified.
855 #[arg(long, value_name = "tag1,tag2")]
856 tags: Option<String>,
857 },
858
859 /// Get the current value of a site setting.
860 #[command(visible_alias = "g")]
861 Get {
862 /// Discourse name.
863 discourse: String,
864 /// Setting key.
865 setting: String,
866 },
867
868 /// List all site settings.
869 #[command(visible_alias = "ls")]
870 List {
871 /// Discourse name.
872 discourse: String,
873 /// Output format.
874 #[arg(long, short = 'f', value_enum, default_value = "text")]
875 format: ListFormat,
876 /// Show output even when list is empty.
877 #[arg(long, short = 'v')]
878 verbose: bool,
879 },
880}
881
882#[derive(ValueEnum, Clone, Copy)]
883pub enum CompletionShell {
884 /// Bash shell.
885 Bash,
886 /// Zsh shell.
887 Zsh,
888 /// Fish shell.
889 Fish,
890}
891
892impl From<CompletionShell> for Shell {
893 fn from(value: CompletionShell) -> Self {
894 match value {
895 CompletionShell::Bash => Shell::Bash,
896 CompletionShell::Zsh => Shell::Zsh,
897 CompletionShell::Fish => Shell::Fish,
898 }
899 }
900}
901
902#[derive(ValueEnum, Clone)]
903pub enum OutputFormat {
904 /// Plain text.
905 #[value(alias = "plaintext")]
906 Text,
907 /// Markdown list.
908 Markdown,
909 /// Markdown table.
910 MarkdownTable,
911 /// Pretty JSON.
912 Json,
913 /// YAML.
914 #[value(alias = "yml")]
915 Yaml,
916 /// CSV.
917 Csv,
918 /// One base URL per line (pipe-friendly).
919 #[value(alias = "url")]
920 Urls,
921}
922
923#[derive(ValueEnum, Clone, Copy)]
924pub enum ListFormat {
925 /// Plain text.
926 Text,
927 /// Pretty JSON.
928 Json,
929 /// YAML.
930 #[value(alias = "yml")]
931 Yaml,
932}
933
934#[derive(ValueEnum, Clone, Copy)]
935pub enum StructuredFormat {
936 /// Pretty JSON.
937 Json,
938 /// YAML.
939 #[value(alias = "yml")]
940 Yaml,
941}