1use std::path::PathBuf;
4
5use clap::{Args, Parser, Subcommand};
6
7const DEFAULT_TENANT: &str = "default";
21pub(crate) const DEFAULT_TEAM: &str = "default";
23
24#[derive(Parser, Debug)]
25#[command(name = "greentic-setup")]
26#[command(version)]
27#[command(about = "Greentic bundle setup CLI")]
28#[command(after_help = r#"EXAMPLES:
29 Interactive wizard:
30 greentic-setup ./my-bundle
31
32 Preview without executing:
33 greentic-setup --dry-run ./my-bundle
34
35 Generate answers template:
36 greentic-setup --dry-run --emit-answers answers.json ./my-bundle
37
38 Apply answers file:
39 greentic-setup --answers answers.json ./my-bundle.gtbundle
40
41 Deploy a bundle into an environment:
42 greentic-setup env-deploy ./my-bundle.gtbundle
43 greentic-setup env-deploy ./my-bundle.gtbundle --env staging
44 greentic-setup env-deploy --dry-run ./my-bundle.gtbundle
45
46 Add a messaging provider to an environment:
47 greentic-setup provider add telegram
48 greentic-setup provider add slack --env staging
49 greentic-setup provider add telegram --answers answers.json --non-interactive
50 greentic-setup provider list
51 greentic-setup provider remove <endpoint-id>
52
53 Advanced (bundle subcommands):
54 greentic-setup bundle init ./my-bundle
55 greentic-setup bundle add pack.gtpack --bundle ./my-bundle
56 greentic-setup bundle status --bundle ./my-bundle
57"#)]
58pub struct Cli {
59 #[arg(value_name = "BUNDLE")]
61 pub bundle: Option<PathBuf>,
62
63 #[arg(long = "dry-run", global = true)]
65 pub dry_run: bool,
66
67 #[arg(long = "emit-answers", value_name = "FILE", global = true)]
69 pub emit_answers: Option<PathBuf>,
70
71 #[arg(long = "answers", short = 'a', value_name = "FILE", global = true)]
73 pub answers: Option<PathBuf>,
74
75 #[arg(long = "key", value_name = "KEY", global = true)]
77 pub key: Option<String>,
78
79 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT, global = true)]
81 pub tenant: String,
82
83 #[arg(long = "team", global = true)]
85 pub team: Option<String>,
86
87 #[arg(long = "env", short = 'e', default_value = "local", global = true)]
90 pub env: String,
91
92 #[arg(long = "locale", global = true)]
94 pub locale: Option<String>,
95
96 #[arg(long = "advanced", global = true)]
98 pub advanced: bool,
99
100 #[arg(long = "ui", global = true, default_value_t = true)]
103 pub ui: bool,
104
105 #[arg(long = "no-ui", global = true)]
107 pub no_ui: bool,
108
109 #[arg(long = "non-interactive", global = true)]
111 pub non_interactive: bool,
112
113 #[arg(long = "new-cache", global = true)]
117 pub new_cache: bool,
118
119 #[command(subcommand)]
120 pub command: Option<Command>,
121}
122
123#[derive(Subcommand, Debug)]
124pub enum Command {
125 Doctor(DoctorArgs),
127 EnvDeploy(EnvDeployArgs),
129 #[command(subcommand)]
131 Provider(ProviderCommand),
132 #[command(subcommand)]
134 Bundle(Box<BundleCommand>),
135}
136
137#[derive(Args, Debug, Clone)]
138pub struct EnvDeployArgs {
139 #[arg(value_name = "BUNDLE")]
141 pub bundle: PathBuf,
142}
143
144#[derive(Args, Debug, Clone)]
145pub struct DoctorArgs {
146 #[arg(value_name = "BUNDLE")]
148 pub bundle: Option<PathBuf>,
149 #[command(subcommand)]
150 pub command: Option<DoctorCommand>,
151 #[arg(long = "json")]
153 pub json: bool,
154 #[arg(long = "strict")]
156 pub strict: bool,
157 #[arg(long = "fix-hints")]
159 pub fix_hints: bool,
160 #[arg(long = "show-info")]
162 pub show_info: bool,
163 #[arg(long = "stage", value_enum)]
165 pub stage: Option<DoctorStageArg>,
166}
167
168#[derive(Subcommand, Debug, Clone)]
169pub enum DoctorCommand {
170 Provider(DoctorProviderArgs),
172}
173
174#[derive(Args, Debug, Clone)]
175pub struct DoctorProviderArgs {
176 #[arg(value_name = "PACK")]
178 pub pack: PathBuf,
179}
180
181#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
182pub enum DoctorStageArg {
183 Setup,
184 Cache,
185 Locks,
186 Answers,
187 Runtime,
188 Routes,
189}
190
191#[derive(Subcommand, Debug, Clone)]
192pub enum BundleCommand {
193 Init(BundleInitArgs),
195 Add(BundleAddArgs),
197 Setup(BundleSetupArgs),
199 Update(BundleSetupArgs),
201 SetupStatus(BundleSetupStatusArgs),
203 SetupNext(BundleSetupNextArgs),
205 SetupRetry(BundleSetupRetryArgs),
207 SetupReset(BundleSetupResetArgs),
209 SetupMigrate(BundleSetupMigrateArgs),
211 Remove(BundleRemoveArgs),
213 Build(BundleBuildArgs),
215 List(BundleListArgs),
217 Status(BundleStatusArgs),
219}
220
221#[derive(Args, Debug, Clone)]
222pub struct BundleInitArgs {
223 #[arg(value_name = "PATH")]
225 pub path: Option<PathBuf>,
226 #[arg(long = "name", short = 'n')]
228 pub name: Option<String>,
229}
230
231#[derive(Args, Debug, Clone)]
232pub struct BundleAddArgs {
233 #[arg(value_name = "PACK_REF")]
235 pub pack_ref: String,
236 #[arg(long = "bundle", short = 'b')]
238 pub bundle: Option<PathBuf>,
239 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
241 pub tenant: String,
242 #[arg(long = "team")]
244 pub team: Option<String>,
245 #[arg(long = "env", short = 'e', default_value = "local")]
248 pub env: String,
249 #[arg(long = "dry-run")]
251 pub dry_run: bool,
252}
253
254#[derive(Args, Debug, Clone)]
255pub struct BundleSetupArgs {
256 #[arg(value_name = "PROVIDER_ID")]
258 pub provider_id: Option<String>,
259 #[arg(long = "bundle", short = 'b')]
261 pub bundle: Option<PathBuf>,
262 #[arg(long = "answers", short = 'a')]
264 pub answers: Option<PathBuf>,
265 #[arg(long = "key", value_name = "KEY")]
267 pub key: Option<String>,
268 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
270 pub tenant: String,
271 #[arg(long = "team")]
273 pub team: Option<String>,
274 #[arg(long = "env", short = 'e', default_value = "local")]
277 pub env: String,
278 #[arg(long = "domain", short = 'd', default_value = "all")]
280 pub domain: String,
281 #[arg(long = "parallel", default_value = "1")]
283 pub parallel: usize,
284 #[arg(long = "backup")]
286 pub backup: bool,
287 #[arg(long = "skip-secrets-init")]
289 pub skip_secrets_init: bool,
290 #[arg(long = "best-effort")]
292 pub best_effort: bool,
293 #[arg(skip)]
295 pub non_interactive: bool,
296 #[arg(long = "dry-run")]
298 pub dry_run: bool,
299 #[arg(long = "emit-answers")]
301 pub emit_answers: Option<PathBuf>,
302 #[arg(long = "advanced")]
304 pub advanced: bool,
305}
306
307#[derive(Args, Debug, Clone)]
308pub struct BundleSetupStatusArgs {
309 #[arg(value_name = "PROVIDER_ID")]
311 pub provider_id: String,
312 #[arg(long = "bundle", short = 'b')]
314 pub bundle: Option<PathBuf>,
315 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
317 pub tenant: String,
318 #[arg(long = "team")]
320 pub team: Option<String>,
321 #[arg(long = "env", short = 'e', default_value = "dev")]
323 pub env: String,
324 #[arg(long = "format", default_value = "text")]
326 pub format: String,
327}
328
329#[derive(Args, Debug, Clone)]
330pub struct BundleSetupNextArgs {
331 #[arg(value_name = "PROVIDER_ID")]
333 pub provider_id: String,
334 #[arg(long = "bundle", short = 'b')]
336 pub bundle: Option<PathBuf>,
337 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
339 pub tenant: String,
340 #[arg(long = "team")]
342 pub team: Option<String>,
343 #[arg(long = "env", short = 'e', default_value = "dev")]
345 pub env: String,
346 #[arg(long = "format", default_value = "text")]
348 pub format: String,
349 #[arg(long = "dry-run")]
351 pub dry_run: bool,
352}
353
354#[derive(Args, Debug, Clone)]
355pub struct BundleSetupRetryArgs {
356 #[arg(value_name = "PROVIDER_ID")]
358 pub provider_id: String,
359 #[arg(long = "bundle", short = 'b')]
361 pub bundle: Option<PathBuf>,
362 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
364 pub tenant: String,
365 #[arg(long = "team")]
367 pub team: Option<String>,
368 #[arg(long = "env", short = 'e', default_value = "dev")]
370 pub env: String,
371 #[arg(long = "step")]
373 pub step: Option<String>,
374 #[arg(long = "json")]
376 pub json: bool,
377}
378
379#[derive(Args, Debug, Clone)]
380pub struct BundleSetupResetArgs {
381 #[arg(value_name = "PROVIDER_ID")]
383 pub provider_id: String,
384 #[arg(long = "bundle", short = 'b')]
386 pub bundle: Option<PathBuf>,
387 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
389 pub tenant: String,
390 #[arg(long = "team")]
392 pub team: Option<String>,
393 #[arg(long = "yes")]
395 pub yes: bool,
396 #[arg(long = "json")]
398 pub json: bool,
399}
400
401#[derive(Args, Debug, Clone)]
402pub struct BundleSetupMigrateArgs {
403 #[arg(value_name = "PROVIDER_ID")]
405 pub provider_id: String,
406 #[arg(long = "bundle", short = 'b')]
408 pub bundle: Option<PathBuf>,
409 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
411 pub tenant: String,
412 #[arg(long = "team")]
414 pub team: Option<String>,
415 #[arg(long = "env", short = 'e', default_value = "dev")]
417 pub env: String,
418 #[arg(long = "json")]
420 pub json: bool,
421}
422
423#[derive(Args, Debug, Clone)]
424pub struct BundleRemoveArgs {
425 #[arg(value_name = "PROVIDER_ID")]
427 pub provider_id: String,
428 #[arg(long = "bundle", short = 'b')]
430 pub bundle: Option<PathBuf>,
431 #[arg(long = "tenant", short = 't', default_value = DEFAULT_TENANT)]
433 pub tenant: String,
434 #[arg(long = "team")]
436 pub team: Option<String>,
437 #[arg(long = "force", short = 'f')]
439 pub force: bool,
440}
441
442#[derive(Args, Debug, Clone)]
443pub struct BundleBuildArgs {
444 #[arg(long = "bundle", short = 'b')]
446 pub bundle: Option<PathBuf>,
447 #[arg(long = "out", short = 'o')]
449 pub out: PathBuf,
450 #[arg(long = "tenant", short = 't')]
452 pub tenant: Option<String>,
453 #[arg(long = "team")]
455 pub team: Option<String>,
456 #[arg(long = "only-used-providers")]
458 pub only_used_providers: bool,
459 #[arg(long = "doctor")]
461 pub doctor: bool,
462 #[arg(long = "skip-doctor")]
464 pub skip_doctor: bool,
465}
466
467#[derive(Args, Debug, Clone)]
468pub struct BundleListArgs {
469 #[arg(long = "bundle", short = 'b')]
471 pub bundle: Option<PathBuf>,
472 #[arg(long = "domain", short = 'd', default_value = "messaging")]
474 pub domain: String,
475 #[arg(long = "pack", short = 'p')]
477 pub pack: Option<String>,
478 #[arg(long = "format", default_value = "text")]
480 pub format: String,
481}
482
483#[derive(Args, Debug, Clone)]
484pub struct BundleStatusArgs {
485 #[arg(long = "bundle", short = 'b')]
487 pub bundle: Option<PathBuf>,
488 #[arg(long = "format", default_value = "text")]
490 pub format: String,
491}
492
493#[derive(Subcommand, Debug, Clone)]
496pub enum ProviderCommand {
497 Add(ProviderAddArgs),
499 List(ProviderListArgs),
501 Remove(ProviderRemoveArgs),
503}
504
505#[derive(Args, Debug, Clone)]
506pub struct ProviderAddArgs {
507 #[arg(value_name = "KIND")]
509 pub kind: String,
510 #[arg(long = "bundle-id")]
512 pub bundle_id: Option<String>,
513 #[arg(long = "pack")]
515 pub pack: Option<PathBuf>,
516 #[arg(long = "pack-version")]
519 pub pack_version: Option<String>,
520 #[arg(long = "provider-id")]
522 pub provider_id: Option<String>,
523 #[arg(long = "display-name")]
525 pub display_name: Option<String>,
526}
527
528#[derive(Args, Debug, Clone)]
529pub struct ProviderListArgs {}
530
531#[derive(Args, Debug, Clone)]
532pub struct ProviderRemoveArgs {
533 #[arg(value_name = "ENDPOINT_ID")]
535 pub endpoint_id: String,
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use clap::{CommandFactory, Parser};
542
543 #[test]
550 fn tenant_defaults_to_the_shared_constant_not_demo() {
551 let cli = Cli::parse_from(["greentic-setup", "env-deploy", "b.gtbundle"]);
552 assert_eq!(cli.tenant, DEFAULT_TENANT);
553 assert_ne!(cli.tenant, "demo");
554 assert_eq!(DEFAULT_TENANT, "default");
557 assert_eq!(DEFAULT_TEAM, "default");
558 }
559
560 #[test]
565 fn every_tenant_arg_in_the_command_tree_uses_the_shared_constant() {
566 fn walk(cmd: &clap::Command, path: &str, seen: &mut usize) {
567 for arg in cmd.get_arguments() {
568 if arg.get_id() != "tenant" {
569 continue;
570 }
571 let defaults: Vec<String> = arg
572 .get_default_values()
573 .iter()
574 .map(|value| value.to_string_lossy().into_owned())
575 .collect();
576 if defaults.is_empty() {
579 continue;
580 }
581 *seen += 1;
582 assert_eq!(
583 defaults,
584 vec![DEFAULT_TENANT.to_string()],
585 "`{path} --tenant` must default to DEFAULT_TENANT"
586 );
587 }
588 for sub in cmd.get_subcommands() {
589 walk(sub, &format!("{path} {}", sub.get_name()), seen);
590 }
591 }
592
593 let mut seen = 0;
594 walk(&Cli::command(), "greentic-setup", &mut seen);
595 assert_eq!(seen, 10, "the defaulted tenant-arg count changed");
600 }
601}