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