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 /// Create a new user. `--approve` also marks the account approved
805 /// (needed when site requires manual approval). Password is either
806 /// supplied via stdin (`--password-stdin`) or omitted — in the
807 /// latter case the user will have to set one via the reset flow.
808 #[command(visible_alias = "cr")]
809 Create {
810 /// Discourse name.
811 discourse: String,
812 /// New user's email address.
813 email: String,
814 /// New user's username.
815 username: String,
816 /// Display name (optional).
817 #[arg(long, short = 'N')]
818 name: Option<String>,
819 /// Read the password from stdin instead of auto-reset.
820 #[arg(long)]
821 password_stdin: bool,
822 /// Also mark the user approved (for sites with manual approval).
823 #[arg(long)]
824 approve: bool,
825 },
826 /// Trigger Discourse's password-reset email flow for a user.
827 #[command(name = "password-reset", visible_aliases = ["pwreset", "pw-reset"])]
828 PasswordReset {
829 /// Discourse name.
830 discourse: String,
831 /// Username or email.
832 username: String,
833 },
834 /// Set a user's primary email address. Requires admin scope.
835 #[command(name = "email-set", visible_alias = "email")]
836 EmailSet {
837 /// Discourse name.
838 discourse: String,
839 /// Username.
840 username: String,
841 /// New email address.
842 email: String,
843 },
844 /// Show a user's recent public activity (topics + replies by default).
845 ///
846 /// Built for the "archive my own activity to a journal forum" loop —
847 /// pipe the markdown output straight into `dsc topic reply`/`topic new`.
848 #[command(visible_alias = "act")]
849 Activity {
850 /// Discourse name (the *source* forum to read activity from).
851 discourse: String,
852 /// Username whose activity to read.
853 username: String,
854 /// How far back to look. Accepts `7d`, `24h`, `30m`, `1w`, `90s`, or
855 /// an ISO-8601 timestamp / date. Omit to fetch everything available.
856 #[arg(long, short = 's')]
857 since: Option<String>,
858 /// Action types to include, comma-separated. Default: topics,replies.
859 /// Also recognises: mentions, quotes, likes, edits, responses.
860 #[arg(long, short = 't', default_value = "topics,replies")]
861 types: String,
862 /// Hard cap on number of items returned.
863 #[arg(long, short = 'L')]
864 limit: Option<u32>,
865 /// Output format.
866 #[arg(long, short = 'f', value_enum, default_value = "markdown")]
867 format: ActivityFormatArg,
868 },
869 /// Manage a user's group memberships.
870 #[command(visible_alias = "g")]
871 Groups {
872 #[command(subcommand)]
873 command: UserGroupsCommand,
874 },
875}
876
877#[derive(ValueEnum, Clone, Copy)]
878pub enum ActivityFormatArg {
879 Text,
880 Json,
881 #[value(alias = "yml")]
882 Yaml,
883 #[value(alias = "md")]
884 Markdown,
885 Csv,
886}
887
888#[derive(ValueEnum, Clone, Copy)]
889pub enum RoleArg {
890 Admin,
891 Moderator,
892}
893
894#[derive(Subcommand)]
895pub enum UserGroupsCommand {
896 /// List the groups a user belongs to.
897 #[command(visible_alias = "ls")]
898 List {
899 /// Discourse name.
900 discourse: String,
901 /// Target username.
902 username: String,
903 /// Output format.
904 #[arg(long, short = 'f', value_enum, default_value = "text")]
905 format: ListFormat,
906 },
907 /// Add a user to a group.
908 #[command(visible_alias = "a")]
909 Add {
910 /// Discourse name.
911 discourse: String,
912 /// Target username.
913 username: String,
914 /// Group ID.
915 group_id: u64,
916 /// Send Discourse notification to the user.
917 #[arg(long)]
918 notify: bool,
919 },
920 /// Remove a user from a group.
921 #[command(visible_alias = "rm")]
922 Remove {
923 /// Discourse name.
924 discourse: String,
925 /// Target username.
926 username: String,
927 /// Group ID.
928 group_id: u64,
929 },
930}
931
932#[derive(Subcommand)]
933pub enum PostCommand {
934 /// Edit a post by ID. Reads the new body from file or stdin.
935 #[command(visible_alias = "e")]
936 Edit {
937 /// Discourse name.
938 discourse: String,
939 /// Post ID.
940 post_id: u64,
941 /// Input file path. Reads stdin when omitted or `-`.
942 local_path: Option<PathBuf>,
943 },
944 /// Delete a post by ID.
945 #[command(visible_alias = "rm")]
946 Delete {
947 /// Discourse name.
948 discourse: String,
949 /// Post ID.
950 post_id: u64,
951 },
952 /// Move a post to a different topic.
953 #[command(visible_alias = "mv")]
954 Move {
955 /// Discourse name.
956 discourse: String,
957 /// Post ID to move.
958 post_id: u64,
959 /// Destination topic ID.
960 #[arg(long = "to-topic", short = 't')]
961 to_topic: u64,
962 },
963}
964
965#[derive(Subcommand)]
966pub enum TagCommand {
967 /// List every tag on the Discourse.
968 #[command(visible_alias = "ls")]
969 List {
970 /// Discourse name.
971 discourse: String,
972 /// Output format.
973 #[arg(long, short = 'f', value_enum, default_value = "text")]
974 format: ListFormat,
975 },
976 /// Add a tag to a topic.
977 #[command(visible_alias = "a")]
978 Apply {
979 /// Discourse name.
980 discourse: String,
981 /// Topic ID.
982 topic_id: u64,
983 /// Tag to add.
984 tag: String,
985 },
986 /// Remove a tag from a topic.
987 #[command(visible_alias = "rm")]
988 Remove {
989 /// Discourse name.
990 discourse: String,
991 /// Topic ID.
992 topic_id: u64,
993 /// Tag to remove.
994 tag: String,
995 },
996}
997
998#[derive(Subcommand)]
999pub enum SettingCommand {
1000 /// Set a site setting on a Discourse (or all tagged Discourses).
1001 #[command(visible_alias = "s")]
1002 Set {
1003 /// Discourse name. Required when targeting a single discourse.
1004 discourse: String,
1005 /// Setting key.
1006 setting: String,
1007 /// Setting value.
1008 value: String,
1009 /// Optional tag filter (comma/semicolon separated, match-any). Ignored when discourse is specified.
1010 #[arg(long, value_name = "tag1,tag2")]
1011 tags: Option<String>,
1012 },
1013
1014 /// Get the current value of a site setting.
1015 #[command(visible_alias = "g")]
1016 Get {
1017 /// Discourse name.
1018 discourse: String,
1019 /// Setting key.
1020 setting: String,
1021 },
1022
1023 /// List all site settings.
1024 #[command(visible_alias = "ls")]
1025 List {
1026 /// Discourse name.
1027 discourse: String,
1028 /// Output format.
1029 #[arg(long, short = 'f', value_enum, default_value = "text")]
1030 format: ListFormat,
1031 /// Show output even when list is empty.
1032 #[arg(long, short = 'v')]
1033 verbose: bool,
1034 },
1035}
1036
1037#[derive(ValueEnum, Clone, Copy)]
1038pub enum CompletionShell {
1039 /// Bash shell.
1040 Bash,
1041 /// Zsh shell.
1042 Zsh,
1043 /// Fish shell.
1044 Fish,
1045}
1046
1047impl From<CompletionShell> for Shell {
1048 fn from(value: CompletionShell) -> Self {
1049 match value {
1050 CompletionShell::Bash => Shell::Bash,
1051 CompletionShell::Zsh => Shell::Zsh,
1052 CompletionShell::Fish => Shell::Fish,
1053 }
1054 }
1055}
1056
1057#[derive(ValueEnum, Clone)]
1058pub enum OutputFormat {
1059 /// Plain text.
1060 #[value(alias = "plaintext")]
1061 Text,
1062 /// Markdown list.
1063 Markdown,
1064 /// Markdown table.
1065 MarkdownTable,
1066 /// Pretty JSON.
1067 Json,
1068 /// YAML.
1069 #[value(alias = "yml")]
1070 Yaml,
1071 /// CSV.
1072 Csv,
1073 /// One base URL per line (pipe-friendly).
1074 #[value(alias = "url")]
1075 Urls,
1076}
1077
1078#[derive(ValueEnum, Clone, Copy)]
1079pub enum ListFormat {
1080 /// Plain text.
1081 Text,
1082 /// Pretty JSON.
1083 Json,
1084 /// YAML.
1085 #[value(alias = "yml")]
1086 Yaml,
1087}
1088
1089#[derive(ValueEnum, Clone, Copy)]
1090pub enum StructuredFormat {
1091 /// Pretty JSON.
1092 Json,
1093 /// YAML.
1094 #[value(alias = "yml")]
1095 Yaml,
1096}