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 #[command(subcommand)]
13 pub command: Commands,
14}
15
16#[derive(Subcommand)]
17pub enum Commands {
18 /// List configured Discourses.
19 #[command(visible_alias = "ls")]
20 List {
21 /// Output format for the listing.
22 #[arg(long, short = 'f', value_enum, default_value = "text")]
23 format: OutputFormat,
24 /// Filter by tags (comma/semicolon separated, match-any).
25 #[arg(long, value_name = "tag1,tag2")]
26 tags: Option<String>,
27 /// Open each listed Discourse base URL in a browser tab/window.
28 #[arg(long, short = 'o')]
29 open: bool,
30 /// Include empty results and verbose listing details where supported.
31 #[arg(long, short = 'v')]
32 verbose: bool,
33 #[command(subcommand)]
34 command: Option<ListCommand>,
35 },
36 /// Add one or more Discourses to the config.
37 #[command(visible_alias = "a")]
38 Add {
39 /// Comma-separated discourse names to add.
40 names: String,
41 /// Prompt for additional optional fields while adding.
42 #[arg(long, short = 'i')]
43 interactive: bool,
44 },
45 /// Import Discourses from a file or stdin.
46 #[command(visible_alias = "imp")]
47 Import {
48 /// Path to import input (text/CSV). Reads stdin when omitted.
49 path: Option<PathBuf>,
50 },
51 /// Run remote OS + Discourse update workflow for one or all Discourses.
52 #[command(visible_alias = "up")]
53 Update {
54 /// Discourse name, or 'all' to update every configured Discourse.
55 name: String,
56 /// Parallel update mode for `dsc update all`.
57 #[arg(long, short = 'p')]
58 parallel: bool,
59 /// Maximum workers when parallel mode is enabled (default: 3).
60 #[arg(long, short = 'm')]
61 max: Option<usize>,
62 /// Disable changelog posting (posting prompt is on by default).
63 #[arg(long = "no-changelog", action = ArgAction::SetFalse, default_value_t = true)]
64 post_changelog: bool,
65 /// Auto-confirm changelog posting prompt (non-interactive mode).
66 #[arg(long, short = 'y')]
67 yes: bool,
68 },
69 /// Manage custom emoji.
70 #[command(visible_alias = "em")]
71 Emoji {
72 #[command(subcommand)]
73 command: EmojiCommand,
74 },
75 /// Pull/push/sync topics as local Markdown.
76 #[command(visible_alias = "t")]
77 Topic {
78 #[command(subcommand)]
79 command: TopicCommand,
80 },
81 /// List/copy/pull/push categories.
82 #[command(visible_alias = "cat")]
83 Category {
84 #[command(subcommand)]
85 command: CategoryCommand,
86 },
87 /// List/inspect/copy groups.
88 #[command(visible_alias = "grp")]
89 Group {
90 #[command(subcommand)]
91 command: GroupCommand,
92 },
93 /// Create/list/restore backups.
94 #[command(visible_alias = "bk")]
95 Backup {
96 #[command(subcommand)]
97 command: BackupCommand,
98 },
99 /// List/pull/push color palettes.
100 #[command(visible_alias = "pal")]
101 Palette {
102 #[command(subcommand)]
103 command: PaletteCommand,
104 },
105 /// List/install/remove plugins.
106 #[command(visible_alias = "plg")]
107 Plugin {
108 #[command(subcommand)]
109 command: PluginCommand,
110 },
111 /// List/install/remove/pull/push/duplicate themes.
112 #[command(visible_alias = "th")]
113 Theme {
114 #[command(subcommand)]
115 command: ThemeCommand,
116 },
117 /// Update site settings.
118 #[command(visible_alias = "set")]
119 Setting {
120 #[command(subcommand)]
121 command: SettingCommand,
122 },
123 /// Open a Discourse in the default browser.
124 #[command(visible_alias = "o")]
125 Open {
126 /// Discourse name.
127 discourse: String,
128 },
129 /// Generate shell completion scripts.
130 #[command(visible_alias = "comp")]
131 Completions {
132 /// Target shell.
133 #[arg(value_enum)]
134 shell: CompletionShell,
135 /// Output directory. Prints to stdout when omitted.
136 #[arg(long, short = 'd')]
137 dir: Option<PathBuf>,
138 },
139 /// Print the dsc version.
140 #[command(visible_alias = "ver")]
141 Version,
142}
143
144#[derive(Subcommand)]
145pub enum ListCommand {
146 /// Sort discourse entries by name and rewrite config in-place.
147 /// Also inserts placeholder values for unset template keys.
148 #[command(visible_alias = "ty")]
149 Tidy,
150}
151
152#[derive(Subcommand)]
153pub enum EmojiCommand {
154 /// Upload one emoji file, or bulk-upload from a directory.
155 #[command(visible_alias = "a")]
156 Add {
157 /// Discourse name.
158 discourse: String,
159 /// Local file or directory path.
160 emoji_path: PathBuf,
161 /// Optional emoji name (file uploads only).
162 emoji_name: Option<String>,
163 },
164
165 /// List custom emojis on a Discourse.
166 #[command(visible_alias = "ls")]
167 List {
168 /// Discourse name.
169 discourse: String,
170 /// Output format.
171 #[arg(long, short = 'f', value_enum, default_value = "text")]
172 format: ListFormat,
173 /// Include additional fields where supported.
174 #[arg(long, short = 'v')]
175 verbose: bool,
176 /// Render inline images when terminal protocol support is available.
177 #[arg(long, short = 'i')]
178 inline: bool,
179 },
180}
181
182#[derive(Subcommand)]
183pub enum TopicCommand {
184 /// Pull a topic to a local Markdown file.
185 #[command(visible_alias = "pl")]
186 Pull {
187 /// Discourse name.
188 discourse: String,
189 /// Topic ID.
190 topic_id: u64,
191 /// Destination file or directory (auto-derived when omitted).
192 local_path: Option<PathBuf>,
193 },
194 /// Push a local Markdown file to a topic.
195 #[command(visible_alias = "ps")]
196 Push {
197 /// Discourse name.
198 discourse: String,
199 /// Local Markdown file path.
200 local_path: PathBuf,
201 /// Topic ID.
202 topic_id: u64,
203 },
204 /// Sync a topic and local Markdown file using newest timestamp.
205 #[command(visible_alias = "sy")]
206 Sync {
207 /// Discourse name.
208 discourse: String,
209 /// Topic ID.
210 topic_id: u64,
211 /// Local Markdown file path.
212 local_path: PathBuf,
213 /// Skip sync confirmation prompt.
214 #[arg(long, short = 'y')]
215 yes: bool,
216 },
217}
218
219#[derive(Subcommand)]
220pub enum CategoryCommand {
221 /// List categories.
222 #[command(visible_alias = "ls")]
223 List {
224 /// Discourse name.
225 discourse: String,
226 /// Output format.
227 #[arg(long, short = 'f', value_enum, default_value = "text")]
228 format: ListFormat,
229 /// Include additional fields where supported.
230 #[arg(long, short = 'v')]
231 verbose: bool,
232 /// Show category hierarchy tree.
233 #[arg(long)]
234 tree: bool,
235 },
236 /// Copy a category to another Discourse.
237 #[command(visible_alias = "cp")]
238 Copy {
239 /// Source discourse name.
240 discourse: String,
241 /// Target discourse name (defaults to source when omitted).
242 #[arg(long, short = 't')]
243 target: Option<String>,
244 /// Category ID or slug.
245 category: String,
246 },
247 /// Pull all topics from a category into local Markdown files.
248 #[command(visible_alias = "pl")]
249 Pull {
250 /// Discourse name.
251 discourse: String,
252 /// Category ID or slug.
253 category: String,
254 /// Destination directory (auto-derived when omitted).
255 local_path: Option<PathBuf>,
256 },
257 /// Push local Markdown files into a category.
258 #[command(visible_alias = "ps")]
259 Push {
260 /// Discourse name.
261 discourse: String,
262 /// Local directory containing Markdown files.
263 local_path: PathBuf,
264 /// Category ID or slug.
265 category: String,
266 },
267}
268
269#[derive(Subcommand)]
270pub enum GroupCommand {
271 /// List groups.
272 #[command(visible_alias = "ls")]
273 List {
274 /// Discourse name.
275 discourse: String,
276 /// Output format.
277 #[arg(long, short = 'f', value_enum, default_value = "text")]
278 format: ListFormat,
279 /// Include additional fields where supported.
280 #[arg(long, short = 'v')]
281 verbose: bool,
282 },
283 /// Show group details.
284 #[command(visible_alias = "i")]
285 Info {
286 /// Discourse name.
287 discourse: String,
288 /// Group ID.
289 group: u64,
290 /// Output format.
291 #[arg(long, short = 'f', value_enum, default_value = "json")]
292 format: StructuredFormat,
293 },
294 /// List members of a group.
295 #[command(visible_alias = "m")]
296 Members {
297 /// Discourse name.
298 discourse: String,
299 /// Group ID.
300 group: u64,
301 /// Output format.
302 #[arg(long, short = 'f', value_enum, default_value = "text")]
303 format: ListFormat,
304 },
305 /// Copy a group to another Discourse.
306 #[command(visible_alias = "cp")]
307 Copy {
308 /// Source discourse name.
309 discourse: String,
310 /// Target discourse name (defaults to source when omitted).
311 #[arg(long, short = 't')]
312 target: Option<String>,
313 /// Group ID.
314 group: u64,
315 },
316}
317
318#[derive(Subcommand)]
319pub enum BackupCommand {
320 /// Create a new backup.
321 #[command(visible_alias = "cr")]
322 Create {
323 /// Discourse name.
324 discourse: String,
325 },
326 /// List backups.
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: OutputFormat,
334 /// Include additional fields where supported.
335 #[arg(long, short = 'v')]
336 verbose: bool,
337 },
338 /// Restore a backup.
339 #[command(visible_alias = "rs")]
340 Restore {
341 /// Discourse name.
342 discourse: String,
343 /// Backup filename/path on the target system.
344 backup_path: String,
345 },
346}
347
348#[derive(Subcommand)]
349pub enum PaletteCommand {
350 /// List color palettes.
351 #[command(visible_alias = "ls")]
352 List {
353 /// Discourse name.
354 discourse: String,
355 /// Output format.
356 #[arg(long, short = 'f', value_enum, default_value = "text")]
357 format: ListFormat,
358 /// Include additional fields where supported.
359 #[arg(long, short = 'v')]
360 verbose: bool,
361 },
362 /// Pull a palette to local JSON.
363 #[command(visible_alias = "pl")]
364 Pull {
365 /// Discourse name.
366 discourse: String,
367 /// Palette ID.
368 palette_id: u64,
369 /// Destination file path (auto-derived when omitted).
370 local_path: Option<PathBuf>,
371 },
372 /// Push local JSON to create or update a palette.
373 #[command(visible_alias = "ps")]
374 Push {
375 /// Discourse name.
376 discourse: String,
377 /// Local JSON file path.
378 local_path: PathBuf,
379 /// Palette ID to update (creates a new palette when omitted).
380 palette_id: Option<u64>,
381 },
382}
383
384#[derive(Subcommand)]
385pub enum PluginCommand {
386 /// List installed plugins.
387 #[command(visible_alias = "ls")]
388 List {
389 /// Discourse name.
390 discourse: String,
391 /// Output format.
392 #[arg(long, short = 'f', value_enum, default_value = "text")]
393 format: ListFormat,
394 /// Include additional fields where supported.
395 #[arg(long, short = 'v')]
396 verbose: bool,
397 },
398 /// Install a plugin from URL.
399 #[command(visible_alias = "i")]
400 Install {
401 /// Discourse name.
402 discourse: String,
403 /// Plugin repository URL.
404 url: String,
405 },
406 /// Remove a plugin by name.
407 #[command(visible_alias = "rm")]
408 Remove {
409 /// Discourse name.
410 discourse: String,
411 /// Plugin name.
412 name: String,
413 },
414}
415
416#[derive(Subcommand)]
417pub enum ThemeCommand {
418 /// List installed themes.
419 #[command(visible_alias = "ls")]
420 List {
421 /// Discourse name.
422 discourse: String,
423 /// Output format.
424 #[arg(long, short = 'f', value_enum, default_value = "text")]
425 format: ListFormat,
426 /// Include additional fields where supported.
427 #[arg(long, short = 'v')]
428 verbose: bool,
429 },
430 /// Install a theme from URL.
431 #[command(visible_alias = "i")]
432 Install {
433 /// Discourse name.
434 discourse: String,
435 /// Theme repository URL.
436 url: String,
437 },
438 /// Remove a theme by name.
439 #[command(visible_alias = "rm")]
440 Remove {
441 /// Discourse name.
442 discourse: String,
443 /// Theme name.
444 name: String,
445 },
446 /// Pull a theme to a local JSON file.
447 #[command(visible_alias = "pl")]
448 Pull {
449 /// Discourse name.
450 discourse: String,
451 /// Theme ID (from `dsc theme list`).
452 theme_id: u64,
453 /// Destination file path (auto-derived from theme name when omitted).
454 local_path: Option<PathBuf>,
455 },
456 /// Push a local JSON file to create or update a theme.
457 #[command(visible_alias = "ps")]
458 Push {
459 /// Discourse name.
460 discourse: String,
461 /// Local JSON file path.
462 local_path: PathBuf,
463 /// Theme ID to update (creates a new theme when omitted).
464 theme_id: Option<u64>,
465 },
466 /// Duplicate a theme and print the new theme ID.
467 #[command(visible_alias = "dup")]
468 Duplicate {
469 /// Discourse name.
470 discourse: String,
471 /// Theme ID to duplicate (from `dsc theme list`).
472 theme_id: u64,
473 },
474}
475
476#[derive(Subcommand)]
477pub enum SettingCommand {
478 /// Set a site setting on a Discourse (or all tagged Discourses).
479 #[command(visible_alias = "s")]
480 Set {
481 /// Discourse name. Required when targeting a single discourse.
482 discourse: String,
483 /// Setting key.
484 setting: String,
485 /// Setting value.
486 value: String,
487 /// Optional tag filter (comma/semicolon separated, match-any). Ignored when discourse is specified.
488 #[arg(long, value_name = "tag1,tag2")]
489 tags: Option<String>,
490 },
491
492 /// Get the current value of a site setting.
493 #[command(visible_alias = "g")]
494 Get {
495 /// Discourse name.
496 discourse: String,
497 /// Setting key.
498 setting: String,
499 },
500
501 /// List all site settings.
502 #[command(visible_alias = "ls")]
503 List {
504 /// Discourse name.
505 discourse: String,
506 /// Output format.
507 #[arg(long, short = 'f', value_enum, default_value = "text")]
508 format: ListFormat,
509 /// Show output even when list is empty.
510 #[arg(long, short = 'v')]
511 verbose: bool,
512 },
513}
514
515#[derive(ValueEnum, Clone, Copy)]
516pub enum CompletionShell {
517 /// Bash shell.
518 Bash,
519 /// Zsh shell.
520 Zsh,
521 /// Fish shell.
522 Fish,
523}
524
525impl From<CompletionShell> for Shell {
526 fn from(value: CompletionShell) -> Self {
527 match value {
528 CompletionShell::Bash => Shell::Bash,
529 CompletionShell::Zsh => Shell::Zsh,
530 CompletionShell::Fish => Shell::Fish,
531 }
532 }
533}
534
535#[derive(ValueEnum, Clone)]
536pub enum OutputFormat {
537 /// Plain text.
538 #[value(alias = "plaintext")]
539 Text,
540 /// Markdown list.
541 Markdown,
542 /// Markdown table.
543 MarkdownTable,
544 /// Pretty JSON.
545 Json,
546 /// YAML.
547 #[value(alias = "yml")]
548 Yaml,
549 /// CSV.
550 Csv,
551 /// One base URL per line (pipe-friendly).
552 #[value(alias = "url")]
553 Urls,
554}
555
556#[derive(ValueEnum, Clone, Copy)]
557pub enum ListFormat {
558 /// Plain text.
559 Text,
560 /// Pretty JSON.
561 Json,
562 /// YAML.
563 #[value(alias = "yml")]
564 Yaml,
565}
566
567#[derive(ValueEnum, Clone, Copy)]
568pub enum StructuredFormat {
569 /// Pretty JSON.
570 Json,
571 /// YAML.
572 #[value(alias = "yml")]
573 Yaml,
574}