Skip to main content

greentic_dev/
cli.rs

1use std::{ffi::OsString, path::PathBuf};
2
3use crate::secrets_cli::SecretsCommand;
4use clap::{Arg, ArgAction, Args, CommandFactory, Parser, Subcommand};
5
6#[derive(Parser, Debug)]
7#[command(name = "greentic-dev")]
8#[command(version)]
9#[command(about = "cli.root.about")]
10pub struct Cli {
11    #[command(subcommand)]
12    pub command: Command,
13}
14
15pub fn localized_help_command(locale: &str) -> clap::Command {
16    let mut command = Cli::command()
17        .about(crate::i18n::t(locale, "cli.root.about"))
18        .disable_help_subcommand(true)
19        .disable_help_flag(true)
20        .disable_version_flag(true)
21        .arg(
22            Arg::new("help")
23                .short('h')
24                .long("help")
25                .action(ArgAction::Help)
26                .help(crate::i18n::t(locale, "cli.help.flag")),
27        )
28        .arg(
29            Arg::new("version")
30                .short('V')
31                .long("version")
32                .action(ArgAction::Version)
33                .help(crate::i18n::t(locale, "cli.version.flag")),
34        )
35        .arg(
36            Arg::new("locale")
37                .long("locale")
38                .global(true)
39                .value_name("LOCALE")
40                .help(crate::i18n::t(locale, "cli.option.locale")),
41        );
42
43    for (name, key) in [
44        ("flow", "cli.command.flow.about"),
45        ("pack", "cli.command.pack.about"),
46        ("component", "cli.command.component.about"),
47        ("bundle", "cli.command.bundle.about"),
48        ("runner", "cli.command.runner.about"),
49        ("config", "cli.command.config.about"),
50        ("coverage", "cli.command.coverage.about"),
51        ("mcp", "cli.command.mcp.about"),
52        ("gui", "cli.command.gui.about"),
53        ("secrets", "cli.command.secrets.about"),
54        ("tools", "cli.command.tools.about"),
55        ("install", "cli.command.install.about"),
56        ("cbor", "cli.command.cbor.about"),
57        ("wizard", "cli.command.wizard.about"),
58    ] {
59        command = command.mut_subcommand(name, |sub| sub.about(crate::i18n::t(locale, key)));
60    }
61
62    command = command.mut_subcommand("secrets", |sub| {
63        sub.about(crate::i18n::t(locale, "cli.command.secrets.about"))
64            .mut_subcommand("init", |sub| {
65                sub.about(crate::i18n::t(locale, "cli.command.secrets.init.about"))
66                    .mut_arg("pack", |arg| {
67                        arg.help(crate::i18n::t(locale, "cli.command.secrets.init.pack"))
68                    })
69                    .mut_arg("passthrough", |arg| {
70                        arg.help(crate::i18n::t(
71                            locale,
72                            "cli.command.secrets.init.passthrough",
73                        ))
74                    })
75            })
76    });
77    command = command
78        .mut_subcommand("config", |sub| {
79            sub.about(crate::i18n::t(locale, "cli.command.config.about"))
80                .mut_subcommand("set", |sub| {
81                    sub.about(crate::i18n::t(locale, "cli.command.config.set.about"))
82                        .mut_arg("key", |arg| {
83                            arg.help(crate::i18n::t(locale, "cli.command.config.set.key"))
84                        })
85                        .mut_arg("value", |arg| {
86                            arg.help(crate::i18n::t(locale, "cli.command.config.set.value"))
87                        })
88                        .mut_arg("file", |arg| {
89                            arg.help(crate::i18n::t(locale, "cli.command.config.set.file"))
90                        })
91                })
92        })
93        .mut_subcommand("mcp", |sub| {
94            sub.about(crate::i18n::t(locale, "cli.command.mcp.about"))
95                .mut_subcommand("doctor", |sub| {
96                    sub.about(crate::i18n::t(locale, "cli.command.mcp.doctor.about"))
97                        .mut_arg("provider", |arg| {
98                            arg.help(crate::i18n::t(locale, "cli.command.mcp.doctor.provider"))
99                        })
100                        .mut_arg("json", |arg| {
101                            arg.help(crate::i18n::t(locale, "cli.command.mcp.doctor.json"))
102                        })
103                })
104        })
105        .mut_subcommand("tools", |sub| {
106            sub.about(crate::i18n::t(locale, "cli.command.tools.about"))
107                .mut_subcommand("install", |sub| {
108                    sub.about(crate::i18n::t(locale, "cli.command.tools.install.about"))
109                        .mut_arg("latest", |arg| {
110                            arg.help(crate::i18n::t(locale, "cli.command.tools.install.latest"))
111                        })
112                })
113        })
114        .mut_subcommand("install", |sub| {
115            sub.about(crate::i18n::t(locale, "cli.command.install.about"))
116                .mut_subcommand("tools", |sub| {
117                    sub.about(crate::i18n::t(locale, "cli.command.install.tools.about"))
118                        .mut_arg("latest", |arg| {
119                            arg.help(crate::i18n::t(locale, "cli.command.tools.install.latest"))
120                        })
121                })
122                .mut_arg("tenant", |arg| {
123                    arg.help(crate::i18n::t(locale, "cli.command.install.tenant"))
124                })
125                .mut_arg("token", |arg| {
126                    arg.help(crate::i18n::t(locale, "cli.command.install.token"))
127                })
128                .mut_arg("bin_dir", |arg| {
129                    arg.help(crate::i18n::t(locale, "cli.command.install.bin_dir"))
130                })
131                .mut_arg("docs_dir", |arg| {
132                    arg.help(crate::i18n::t(locale, "cli.command.install.docs_dir"))
133                })
134                .mut_arg("locale", |arg| {
135                    arg.help(crate::i18n::t(locale, "cli.command.install.locale"))
136                })
137        })
138        .mut_subcommand("cbor", |sub| {
139            sub.about(crate::i18n::t(locale, "cli.command.cbor.about"))
140                .mut_arg("path", |arg| {
141                    arg.help(crate::i18n::t(locale, "cli.command.cbor.path"))
142                })
143        })
144        .mut_subcommand("coverage", |sub| {
145            sub.about(crate::i18n::t(locale, "cli.command.coverage.about"))
146                .mut_arg("skip_run", |arg| {
147                    arg.help(crate::i18n::t(locale, "cli.command.coverage.skip_run"))
148                })
149        })
150        .mut_subcommand("wizard", |sub| {
151            sub.about(crate::i18n::t(locale, "cli.command.wizard.about"))
152                .mut_arg("answers", |arg| {
153                    arg.help(crate::i18n::t(locale, "cli.command.wizard.answers"))
154                })
155                .mut_arg("frontend", |arg| {
156                    arg.help(crate::i18n::t(locale, "cli.command.wizard.frontend"))
157                })
158                .mut_arg("locale", |arg| {
159                    arg.help(crate::i18n::t(locale, "cli.command.wizard.locale"))
160                })
161                .mut_arg("emit_answers", |arg| {
162                    arg.help(crate::i18n::t(locale, "cli.command.wizard.emit_answers"))
163                })
164                .mut_arg("schema", |arg| {
165                    arg.help(crate::i18n::t(locale, "cli.command.wizard.schema"))
166                        .long_help(crate::i18n::t(locale, "cli.command.wizard.schema_long"))
167                })
168                .mut_arg("schema_version", |arg| {
169                    arg.help(crate::i18n::t(locale, "cli.command.wizard.schema_version"))
170                })
171                .mut_arg("migrate", |arg| {
172                    arg.help(crate::i18n::t(locale, "cli.command.wizard.migrate"))
173                })
174                .mut_arg("out", |arg| {
175                    arg.help(crate::i18n::t(locale, "cli.command.wizard.out"))
176                })
177                .mut_arg("dry_run", |arg| {
178                    arg.help(crate::i18n::t(locale, "cli.command.wizard.dry_run"))
179                })
180                .mut_arg("yes", |arg| {
181                    arg.help(crate::i18n::t(locale, "cli.command.wizard.yes"))
182                })
183                .mut_arg("non_interactive", |arg| {
184                    arg.help(crate::i18n::t(locale, "cli.command.wizard.non_interactive"))
185                })
186                .mut_arg("unsafe_commands", |arg| {
187                    arg.help(crate::i18n::t(locale, "cli.command.wizard.unsafe_commands"))
188                })
189                .mut_arg("allow_destructive", |arg| {
190                    arg.help(crate::i18n::t(
191                        locale,
192                        "cli.command.wizard.allow_destructive",
193                    ))
194                })
195                .mut_subcommand("validate", |sub| {
196                    sub.about(crate::i18n::t(locale, "cli.command.wizard.validate.about"))
197                        .mut_arg("answers", |arg| {
198                            arg.help(crate::i18n::t(locale, "cli.command.wizard.answers"))
199                        })
200                        .mut_arg("frontend", |arg| {
201                            arg.help(crate::i18n::t(locale, "cli.command.wizard.frontend"))
202                        })
203                        .mut_arg("locale", |arg| {
204                            arg.help(crate::i18n::t(locale, "cli.command.wizard.locale"))
205                        })
206                        .mut_arg("emit_answers", |arg| {
207                            arg.help(crate::i18n::t(locale, "cli.command.wizard.emit_answers"))
208                        })
209                        .mut_arg("schema_version", |arg| {
210                            arg.help(crate::i18n::t(locale, "cli.command.wizard.schema_version"))
211                        })
212                        .mut_arg("migrate", |arg| {
213                            arg.help(crate::i18n::t(locale, "cli.command.wizard.migrate"))
214                        })
215                        .mut_arg("out", |arg| {
216                            arg.help(crate::i18n::t(locale, "cli.command.wizard.out"))
217                        })
218                })
219                .mut_subcommand("apply", |sub| {
220                    sub.about(crate::i18n::t(locale, "cli.command.wizard.apply.about"))
221                        .mut_arg("answers", |arg| {
222                            arg.help(crate::i18n::t(locale, "cli.command.wizard.answers"))
223                        })
224                        .mut_arg("frontend", |arg| {
225                            arg.help(crate::i18n::t(locale, "cli.command.wizard.frontend"))
226                        })
227                        .mut_arg("locale", |arg| {
228                            arg.help(crate::i18n::t(locale, "cli.command.wizard.locale"))
229                        })
230                        .mut_arg("emit_answers", |arg| {
231                            arg.help(crate::i18n::t(locale, "cli.command.wizard.emit_answers"))
232                        })
233                        .mut_arg("schema_version", |arg| {
234                            arg.help(crate::i18n::t(locale, "cli.command.wizard.schema_version"))
235                        })
236                        .mut_arg("migrate", |arg| {
237                            arg.help(crate::i18n::t(locale, "cli.command.wizard.migrate"))
238                        })
239                        .mut_arg("out", |arg| {
240                            arg.help(crate::i18n::t(locale, "cli.command.wizard.out"))
241                        })
242                        .mut_arg("yes", |arg| {
243                            arg.help(crate::i18n::t(locale, "cli.command.wizard.yes"))
244                        })
245                        .mut_arg("non_interactive", |arg| {
246                            arg.help(crate::i18n::t(locale, "cli.command.wizard.non_interactive"))
247                        })
248                        .mut_arg("unsafe_commands", |arg| {
249                            arg.help(crate::i18n::t(locale, "cli.command.wizard.unsafe_commands"))
250                        })
251                        .mut_arg("allow_destructive", |arg| {
252                            arg.help(crate::i18n::t(
253                                locale,
254                                "cli.command.wizard.allow_destructive",
255                            ))
256                        })
257                })
258        });
259
260    localize_help_tree(command, locale, true)
261}
262
263fn localize_help_tree(mut command: clap::Command, locale: &str, is_root: bool) -> clap::Command {
264    command = command
265        .disable_help_subcommand(true)
266        .disable_help_flag(true);
267    let arg_ids = command
268        .get_arguments()
269        .map(|arg| arg.get_id().as_str().to_string())
270        .collect::<Vec<_>>();
271    if arg_ids.iter().any(|id| id == "help") {
272        command = command.mut_arg("help", |arg| {
273            arg.help(crate::i18n::t(locale, "cli.help.flag"))
274        });
275    } else {
276        command = command.arg(
277            Arg::new("help")
278                .short('h')
279                .long("help")
280                .action(ArgAction::Help)
281                .help(crate::i18n::t(locale, "cli.help.flag")),
282        );
283    }
284    if is_root && arg_ids.iter().any(|id| id == "version") {
285        command = command.mut_arg("version", |arg| {
286            arg.help(crate::i18n::t(locale, "cli.version.flag"))
287        });
288    }
289
290    let sub_names = command
291        .get_subcommands()
292        .map(|sub| sub.get_name().to_string())
293        .collect::<Vec<_>>();
294    for name in sub_names {
295        command = command.mut_subcommand(name, |sub| localize_help_tree(sub, locale, false));
296    }
297
298    command
299}
300
301#[derive(Subcommand, Debug)]
302pub enum Command {
303    /// cli.command.flow.about
304    Flow(PassthroughArgs),
305    /// cli.command.pack.about
306    Pack(PassthroughArgs),
307    /// cli.command.component.about
308    Component(PassthroughArgs),
309    /// cli.command.bundle.about
310    Bundle(PassthroughArgs),
311    /// cli.command.runner.about
312    Runner(PassthroughArgs),
313    /// cli.command.config.about
314    #[command(subcommand)]
315    Config(ConfigCommand),
316    /// cli.command.coverage.about
317    Coverage(CoverageArgs),
318    /// cli.command.mcp.about
319    #[command(subcommand)]
320    Mcp(McpCommand),
321    /// cli.command.gui.about
322    Gui(PassthroughArgs),
323    /// cli.command.secrets.about
324    #[command(subcommand)]
325    Secrets(SecretsCommand),
326    /// cli.command.tools.about
327    #[command(subcommand)]
328    Tools(ToolsCommand),
329    /// cli.command.install.about
330    Install(InstallArgs),
331    /// cli.command.cbor.about
332    Cbor(CborArgs),
333    /// cli.command.wizard.about
334    Wizard(Box<WizardCommand>),
335}
336
337#[derive(Args, Debug, Clone)]
338#[command(disable_help_flag = true)]
339pub struct PassthroughArgs {
340    /// cli.command.passthrough.args
341    #[arg(
342        value_name = "ARGS",
343        trailing_var_arg = true,
344        allow_hyphen_values = true
345    )]
346    pub args: Vec<OsString>,
347}
348
349#[derive(Subcommand, Debug)]
350pub enum McpCommand {
351    /// cli.command.mcp.doctor.about
352    Doctor(McpDoctorArgs),
353}
354
355#[derive(Args, Debug)]
356pub struct McpDoctorArgs {
357    /// cli.command.mcp.doctor.provider
358    pub provider: String,
359    /// cli.command.mcp.doctor.json
360    #[arg(long = "json")]
361    pub json: bool,
362}
363
364#[derive(Subcommand, Debug)]
365pub enum ConfigCommand {
366    /// cli.command.config.set.about
367    Set(ConfigSetArgs),
368}
369
370#[derive(Subcommand, Debug)]
371pub enum ToolsCommand {
372    /// cli.command.tools.install.about
373    Install(ToolsInstallArgs),
374}
375
376#[derive(Subcommand, Debug)]
377pub enum InstallSubcommand {
378    /// cli.command.install.tools.about
379    Tools(ToolsInstallArgs),
380}
381
382#[derive(Args, Debug)]
383pub struct InstallArgs {
384    #[command(subcommand)]
385    pub command: Option<InstallSubcommand>,
386    /// cli.command.install.tenant
387    #[arg(long = "tenant")]
388    pub tenant: Option<String>,
389    /// cli.command.install.token
390    #[arg(long = "token")]
391    pub token: Option<String>,
392    /// cli.command.install.bin_dir
393    #[arg(long = "bin-dir")]
394    pub bin_dir: Option<PathBuf>,
395    /// cli.command.install.docs_dir
396    #[arg(long = "docs-dir")]
397    pub docs_dir: Option<PathBuf>,
398    /// cli.command.install.locale
399    #[arg(long = "locale")]
400    pub locale: Option<String>,
401}
402
403#[derive(Args, Debug)]
404pub struct ToolsInstallArgs {
405    /// cli.command.tools.install.latest
406    #[arg(long = "latest")]
407    pub latest: bool,
408}
409
410#[derive(Args, Debug)]
411pub struct ConfigSetArgs {
412    /// cli.command.config.set.key
413    pub key: String,
414    /// cli.command.config.set.value
415    pub value: String,
416    /// cli.command.config.set.file
417    #[arg(long = "file")]
418    pub file: Option<PathBuf>,
419}
420
421#[derive(Args, Debug)]
422pub struct CborArgs {
423    /// cli.command.cbor.path
424    #[arg(value_name = "PATH")]
425    pub path: PathBuf,
426}
427
428#[derive(Args, Debug, Clone)]
429pub struct CoverageArgs {
430    /// cli.command.coverage.skip_run
431    #[arg(long = "skip-run")]
432    pub skip_run: bool,
433}
434
435#[derive(Args, Debug, Clone)]
436pub struct WizardCommand {
437    #[command(subcommand)]
438    pub command: Option<WizardSubcommand>,
439    #[command(flatten)]
440    pub launch: WizardLaunchArgs,
441}
442
443#[derive(Subcommand, Debug, Clone)]
444pub enum WizardSubcommand {
445    /// cli.command.wizard.validate.about
446    Validate(WizardValidateArgs),
447    /// cli.command.wizard.apply.about
448    Apply(WizardApplyArgs),
449}
450
451#[derive(Args, Debug, Clone)]
452pub struct WizardLaunchArgs {
453    /// cli.command.wizard.answers (local path or http/https URL)
454    #[arg(long = "answers")]
455    pub answers: Option<String>,
456    /// cli.command.wizard.frontend
457    #[arg(long = "frontend", default_value = "json")]
458    pub frontend: String,
459    /// cli.command.wizard.locale
460    #[arg(long = "locale")]
461    pub locale: Option<String>,
462    /// cli.command.wizard.emit_answers
463    #[arg(long = "emit-answers")]
464    pub emit_answers: Option<PathBuf>,
465    /// cli.command.wizard.schema
466    #[arg(long = "schema")]
467    pub schema: bool,
468    /// cli.command.wizard.schema_version
469    #[arg(long = "schema-version")]
470    pub schema_version: Option<String>,
471    /// cli.command.wizard.migrate
472    #[arg(long = "migrate")]
473    pub migrate: bool,
474    /// cli.command.wizard.out
475    #[arg(long = "out")]
476    pub out: Option<PathBuf>,
477    /// cli.command.wizard.dry_run
478    #[arg(long = "dry-run")]
479    pub dry_run: bool,
480    /// cli.command.wizard.yes
481    #[arg(long = "yes")]
482    pub yes: bool,
483    /// cli.command.wizard.non_interactive
484    #[arg(long = "non-interactive")]
485    pub non_interactive: bool,
486    /// cli.command.wizard.unsafe_commands
487    #[arg(long = "unsafe-commands")]
488    pub unsafe_commands: bool,
489    /// cli.command.wizard.allow_destructive
490    #[arg(long = "allow-destructive")]
491    pub allow_destructive: bool,
492}
493
494#[derive(Args, Debug, Clone)]
495pub struct WizardValidateArgs {
496    /// cli.command.wizard.answers (local path or http/https URL)
497    #[arg(long = "answers")]
498    pub answers: String,
499    /// cli.command.wizard.frontend
500    #[arg(long = "frontend", default_value = "json")]
501    pub frontend: String,
502    /// cli.command.wizard.locale
503    #[arg(long = "locale")]
504    pub locale: Option<String>,
505    /// cli.command.wizard.emit_answers
506    #[arg(long = "emit-answers")]
507    pub emit_answers: Option<PathBuf>,
508    /// cli.command.wizard.schema_version
509    #[arg(long = "schema-version")]
510    pub schema_version: Option<String>,
511    /// cli.command.wizard.migrate
512    #[arg(long = "migrate")]
513    pub migrate: bool,
514    /// cli.command.wizard.out
515    #[arg(long = "out")]
516    pub out: Option<PathBuf>,
517}
518
519#[derive(Args, Debug, Clone)]
520pub struct WizardApplyArgs {
521    /// cli.command.wizard.answers (local path or http/https URL)
522    #[arg(long = "answers")]
523    pub answers: String,
524    /// cli.command.wizard.frontend
525    #[arg(long = "frontend", default_value = "json")]
526    pub frontend: String,
527    /// cli.command.wizard.locale
528    #[arg(long = "locale")]
529    pub locale: Option<String>,
530    /// cli.command.wizard.emit_answers
531    #[arg(long = "emit-answers")]
532    pub emit_answers: Option<PathBuf>,
533    /// cli.command.wizard.schema_version
534    #[arg(long = "schema-version")]
535    pub schema_version: Option<String>,
536    /// cli.command.wizard.migrate
537    #[arg(long = "migrate")]
538    pub migrate: bool,
539    /// cli.command.wizard.out
540    #[arg(long = "out")]
541    pub out: Option<PathBuf>,
542    /// cli.command.wizard.yes
543    #[arg(long = "yes")]
544    pub yes: bool,
545    /// cli.command.wizard.non_interactive
546    #[arg(long = "non-interactive")]
547    pub non_interactive: bool,
548    /// cli.command.wizard.unsafe_commands
549    #[arg(long = "unsafe-commands")]
550    pub unsafe_commands: bool,
551    /// cli.command.wizard.allow_destructive
552    #[arg(long = "allow-destructive")]
553    pub allow_destructive: bool,
554}