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. Supported on a subset
13 /// of commands; unsupported commands run normally.
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 /// Create/list/restore backups.
98 #[command(visible_alias = "bk")]
99 Backup {
100 #[command(subcommand)]
101 command: BackupCommand,
102 },
103 /// List/pull/push color palettes.
104 #[command(visible_alias = "pal")]
105 Palette {
106 #[command(subcommand)]
107 command: PaletteCommand,
108 },
109 /// List/install/remove plugins.
110 #[command(visible_alias = "plg")]
111 Plugin {
112 #[command(subcommand)]
113 command: PluginCommand,
114 },
115 /// List/install/remove/pull/push/duplicate themes.
116 #[command(visible_alias = "th")]
117 Theme {
118 #[command(subcommand)]
119 command: ThemeCommand,
120 },
121 /// Update site settings.
122 #[command(visible_alias = "set")]
123 Setting {
124 #[command(subcommand)]
125 command: SettingCommand,
126 },
127 /// Open a Discourse in the default browser.
128 #[command(visible_alias = "o")]
129 Open {
130 /// Discourse name.
131 discourse: String,
132 },
133 /// Inspect and validate configuration.
134 #[command(visible_alias = "cfg")]
135 Config {
136 #[command(subcommand)]
137 command: ConfigCommand,
138 },
139 /// Generate shell completion scripts.
140 #[command(visible_alias = "comp")]
141 Completions {
142 /// Target shell.
143 #[arg(value_enum)]
144 shell: CompletionShell,
145 /// Output directory. Prints to stdout when omitted.
146 #[arg(long, short = 'd')]
147 dir: Option<PathBuf>,
148 },
149 /// Print the dsc version.
150 #[command(visible_alias = "ver")]
151 Version,
152}
153
154#[derive(Subcommand)]
155pub enum ConfigCommand {
156 /// Probe each configured Discourse: API auth and (optionally) SSH reachability.
157 #[command(visible_alias = "ck")]
158 Check {
159 /// Output format.
160 #[arg(long, short = 'f', value_enum, default_value = "text")]
161 format: ListFormat,
162 /// Skip the SSH reachability probe.
163 #[arg(long)]
164 skip_ssh: bool,
165 },
166}
167
168#[derive(Subcommand)]
169pub enum ListCommand {
170 /// Sort discourse entries by name and rewrite config in-place.
171 /// Also inserts placeholder values for unset template keys.
172 #[command(visible_alias = "ty")]
173 Tidy,
174}
175
176#[derive(Subcommand)]
177pub enum EmojiCommand {
178 /// Upload one emoji file, or bulk-upload from a directory.
179 #[command(visible_alias = "a")]
180 Add {
181 /// Discourse name.
182 discourse: String,
183 /// Local file or directory path.
184 emoji_path: PathBuf,
185 /// Optional emoji name (file uploads only).
186 emoji_name: Option<String>,
187 },
188
189 /// List custom emojis on a Discourse.
190 #[command(visible_alias = "ls")]
191 List {
192 /// Discourse name.
193 discourse: String,
194 /// Output format.
195 #[arg(long, short = 'f', value_enum, default_value = "text")]
196 format: ListFormat,
197 /// Include additional fields where supported.
198 #[arg(long, short = 'v')]
199 verbose: bool,
200 /// Render inline images when terminal protocol support is available.
201 #[arg(long, short = 'i')]
202 inline: bool,
203 },
204}
205
206#[derive(Subcommand)]
207pub enum TopicCommand {
208 /// Pull a topic to a local Markdown file.
209 #[command(visible_alias = "pl")]
210 Pull {
211 /// Discourse name.
212 discourse: String,
213 /// Topic ID.
214 topic_id: u64,
215 /// Destination file or directory (auto-derived when omitted).
216 local_path: Option<PathBuf>,
217 },
218 /// Push a local Markdown file to a topic.
219 #[command(visible_alias = "ps")]
220 Push {
221 /// Discourse name.
222 discourse: String,
223 /// Topic ID.
224 topic_id: u64,
225 /// Local Markdown file path.
226 local_path: PathBuf,
227 },
228 /// Sync a topic and local Markdown file using newest timestamp.
229 #[command(visible_alias = "sy")]
230 Sync {
231 /// Discourse name.
232 discourse: String,
233 /// Topic ID.
234 topic_id: u64,
235 /// Local Markdown file path.
236 local_path: PathBuf,
237 /// Skip sync confirmation prompt.
238 #[arg(long, short = 'y')]
239 yes: bool,
240 },
241 /// Reply to a topic with content from a file or stdin.
242 #[command(visible_alias = "r")]
243 Reply {
244 /// Discourse name.
245 discourse: String,
246 /// Topic ID.
247 topic_id: u64,
248 /// Input file path. Reads stdin when omitted or `-`.
249 local_path: Option<PathBuf>,
250 },
251 /// Create a new topic in a category, body from a file or stdin.
252 #[command(visible_alias = "n")]
253 New {
254 /// Discourse name.
255 discourse: String,
256 /// Target category ID.
257 category_id: u64,
258 /// Topic title.
259 #[arg(long, short = 't')]
260 title: String,
261 /// Input file path. Reads stdin when omitted or `-`.
262 local_path: Option<PathBuf>,
263 },
264}
265
266#[derive(Subcommand)]
267pub enum CategoryCommand {
268 /// List categories.
269 #[command(visible_alias = "ls")]
270 List {
271 /// Discourse name.
272 discourse: String,
273 /// Output format.
274 #[arg(long, short = 'f', value_enum, default_value = "text")]
275 format: ListFormat,
276 /// Include additional fields where supported.
277 #[arg(long, short = 'v')]
278 verbose: bool,
279 /// Show category hierarchy tree.
280 #[arg(long)]
281 tree: bool,
282 },
283 /// Copy a category to another Discourse.
284 #[command(visible_alias = "cp")]
285 Copy {
286 /// Source discourse name.
287 discourse: String,
288 /// Target discourse name (defaults to source when omitted).
289 #[arg(long, short = 't')]
290 target: Option<String>,
291 /// Category ID or slug.
292 category: String,
293 },
294 /// Pull all topics from a category into local Markdown files.
295 #[command(visible_alias = "pl")]
296 Pull {
297 /// Discourse name.
298 discourse: String,
299 /// Category ID or slug.
300 category: String,
301 /// Destination directory (auto-derived when omitted).
302 local_path: Option<PathBuf>,
303 },
304 /// Push local Markdown files into a category.
305 #[command(visible_alias = "ps")]
306 Push {
307 /// Discourse name.
308 discourse: String,
309 /// Category ID or slug.
310 category: String,
311 /// Local directory containing Markdown files.
312 local_path: PathBuf,
313 },
314}
315
316#[derive(Subcommand)]
317pub enum GroupCommand {
318 /// List groups.
319 #[command(visible_alias = "ls")]
320 List {
321 /// Discourse name.
322 discourse: String,
323 /// Output format.
324 #[arg(long, short = 'f', value_enum, default_value = "text")]
325 format: ListFormat,
326 /// Include additional fields where supported.
327 #[arg(long, short = 'v')]
328 verbose: bool,
329 },
330 /// Show group details.
331 #[command(visible_alias = "i")]
332 Info {
333 /// Discourse name.
334 discourse: String,
335 /// Group ID.
336 group: u64,
337 /// Output format.
338 #[arg(long, short = 'f', value_enum, default_value = "json")]
339 format: StructuredFormat,
340 },
341 /// List members of a group.
342 #[command(visible_alias = "m")]
343 Members {
344 /// Discourse name.
345 discourse: String,
346 /// Group ID.
347 group: u64,
348 /// Output format.
349 #[arg(long, short = 'f', value_enum, default_value = "text")]
350 format: ListFormat,
351 },
352 /// Copy a group to another Discourse.
353 #[command(visible_alias = "cp")]
354 Copy {
355 /// Source discourse name.
356 discourse: String,
357 /// Target discourse name (defaults to source when omitted).
358 #[arg(long, short = 't')]
359 target: Option<String>,
360 /// Group ID.
361 group: u64,
362 },
363}
364
365#[derive(Subcommand)]
366pub enum BackupCommand {
367 /// Create a new backup.
368 #[command(visible_alias = "cr")]
369 Create {
370 /// Discourse name.
371 discourse: String,
372 },
373 /// List backups.
374 #[command(visible_alias = "ls")]
375 List {
376 /// Discourse name.
377 discourse: String,
378 /// Output format.
379 #[arg(long, short = 'f', value_enum, default_value = "text")]
380 format: OutputFormat,
381 /// Include additional fields where supported.
382 #[arg(long, short = 'v')]
383 verbose: bool,
384 },
385 /// Restore a backup.
386 #[command(visible_alias = "rs")]
387 Restore {
388 /// Discourse name.
389 discourse: String,
390 /// Backup filename/path on the target system.
391 backup_path: String,
392 },
393}
394
395#[derive(Subcommand)]
396pub enum PaletteCommand {
397 /// List color palettes.
398 #[command(visible_alias = "ls")]
399 List {
400 /// Discourse name.
401 discourse: String,
402 /// Output format.
403 #[arg(long, short = 'f', value_enum, default_value = "text")]
404 format: ListFormat,
405 /// Include additional fields where supported.
406 #[arg(long, short = 'v')]
407 verbose: bool,
408 },
409 /// Pull a palette to local JSON.
410 #[command(visible_alias = "pl")]
411 Pull {
412 /// Discourse name.
413 discourse: String,
414 /// Palette ID.
415 palette_id: u64,
416 /// Destination file path (auto-derived when omitted).
417 local_path: Option<PathBuf>,
418 },
419 /// Push local JSON to create or update a palette.
420 #[command(visible_alias = "ps")]
421 Push {
422 /// Discourse name.
423 discourse: String,
424 /// Local JSON file path.
425 local_path: PathBuf,
426 /// Palette ID to update (creates a new palette when omitted).
427 palette_id: Option<u64>,
428 },
429}
430
431#[derive(Subcommand)]
432pub enum PluginCommand {
433 /// List installed plugins.
434 #[command(visible_alias = "ls")]
435 List {
436 /// Discourse name.
437 discourse: String,
438 /// Output format.
439 #[arg(long, short = 'f', value_enum, default_value = "text")]
440 format: ListFormat,
441 /// Include additional fields where supported.
442 #[arg(long, short = 'v')]
443 verbose: bool,
444 },
445 /// Install a plugin from URL.
446 #[command(visible_alias = "i")]
447 Install {
448 /// Discourse name.
449 discourse: String,
450 /// Plugin repository URL.
451 url: String,
452 },
453 /// Remove a plugin by name.
454 #[command(visible_alias = "rm")]
455 Remove {
456 /// Discourse name.
457 discourse: String,
458 /// Plugin name.
459 name: String,
460 },
461}
462
463#[derive(Subcommand)]
464pub enum ThemeCommand {
465 /// List installed themes.
466 #[command(visible_alias = "ls")]
467 List {
468 /// Discourse name.
469 discourse: String,
470 /// Output format.
471 #[arg(long, short = 'f', value_enum, default_value = "text")]
472 format: ListFormat,
473 /// Include additional fields where supported.
474 #[arg(long, short = 'v')]
475 verbose: bool,
476 },
477 /// Install a theme from URL.
478 #[command(visible_alias = "i")]
479 Install {
480 /// Discourse name.
481 discourse: String,
482 /// Theme repository URL.
483 url: String,
484 },
485 /// Remove a theme by name.
486 #[command(visible_alias = "rm")]
487 Remove {
488 /// Discourse name.
489 discourse: String,
490 /// Theme name.
491 name: String,
492 },
493 /// Pull a theme to a local JSON file.
494 #[command(visible_alias = "pl")]
495 Pull {
496 /// Discourse name.
497 discourse: String,
498 /// Theme ID (from `dsc theme list`).
499 theme_id: u64,
500 /// Destination file path (auto-derived from theme name when omitted).
501 local_path: Option<PathBuf>,
502 },
503 /// Push a local JSON file to create or update a theme.
504 #[command(visible_alias = "ps")]
505 Push {
506 /// Discourse name.
507 discourse: String,
508 /// Local JSON file path.
509 local_path: PathBuf,
510 /// Theme ID to update (creates a new theme when omitted).
511 theme_id: Option<u64>,
512 },
513 /// Duplicate a theme and print the new theme ID.
514 #[command(visible_alias = "dup")]
515 Duplicate {
516 /// Discourse name.
517 discourse: String,
518 /// Theme ID to duplicate (from `dsc theme list`).
519 theme_id: u64,
520 },
521}
522
523#[derive(Subcommand)]
524pub enum SettingCommand {
525 /// Set a site setting on a Discourse (or all tagged Discourses).
526 #[command(visible_alias = "s")]
527 Set {
528 /// Discourse name. Required when targeting a single discourse.
529 discourse: String,
530 /// Setting key.
531 setting: String,
532 /// Setting value.
533 value: String,
534 /// Optional tag filter (comma/semicolon separated, match-any). Ignored when discourse is specified.
535 #[arg(long, value_name = "tag1,tag2")]
536 tags: Option<String>,
537 },
538
539 /// Get the current value of a site setting.
540 #[command(visible_alias = "g")]
541 Get {
542 /// Discourse name.
543 discourse: String,
544 /// Setting key.
545 setting: String,
546 },
547
548 /// List all site settings.
549 #[command(visible_alias = "ls")]
550 List {
551 /// Discourse name.
552 discourse: String,
553 /// Output format.
554 #[arg(long, short = 'f', value_enum, default_value = "text")]
555 format: ListFormat,
556 /// Show output even when list is empty.
557 #[arg(long, short = 'v')]
558 verbose: bool,
559 },
560}
561
562#[derive(ValueEnum, Clone, Copy)]
563pub enum CompletionShell {
564 /// Bash shell.
565 Bash,
566 /// Zsh shell.
567 Zsh,
568 /// Fish shell.
569 Fish,
570}
571
572impl From<CompletionShell> for Shell {
573 fn from(value: CompletionShell) -> Self {
574 match value {
575 CompletionShell::Bash => Shell::Bash,
576 CompletionShell::Zsh => Shell::Zsh,
577 CompletionShell::Fish => Shell::Fish,
578 }
579 }
580}
581
582#[derive(ValueEnum, Clone)]
583pub enum OutputFormat {
584 /// Plain text.
585 #[value(alias = "plaintext")]
586 Text,
587 /// Markdown list.
588 Markdown,
589 /// Markdown table.
590 MarkdownTable,
591 /// Pretty JSON.
592 Json,
593 /// YAML.
594 #[value(alias = "yml")]
595 Yaml,
596 /// CSV.
597 Csv,
598 /// One base URL per line (pipe-friendly).
599 #[value(alias = "url")]
600 Urls,
601}
602
603#[derive(ValueEnum, Clone, Copy)]
604pub enum ListFormat {
605 /// Plain text.
606 Text,
607 /// Pretty JSON.
608 Json,
609 /// YAML.
610 #[value(alias = "yml")]
611 Yaml,
612}
613
614#[derive(ValueEnum, Clone, Copy)]
615pub enum StructuredFormat {
616 /// Pretty JSON.
617 Json,
618 /// YAML.
619 #[value(alias = "yml")]
620 Yaml,
621}