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