1use crate::command::api::ApiSubcommand;
16use crate::command::app::AppSubcommand;
17use crate::command::cloud::CloudSubcommand;
18use crate::command::component::ComponentSubcommand;
19use crate::command::plugin::PluginSubcommand;
20use crate::command::profile::ProfileSubcommand;
21use crate::command::worker::WorkerSubcommand;
22use crate::config::{BuildProfileName, ProfileName};
23use crate::log::LogColorize;
24use crate::model::{Format, WorkerName};
25use crate::{command_name, version};
26use anyhow::{anyhow, bail, Context as AnyhowContext};
27use chrono::{DateTime, Utc};
28use clap::error::{ContextKind, ContextValue, ErrorKind};
29use clap::{self, Command, CommandFactory, Subcommand};
30use clap::{Args, Parser};
31use clap_verbosity_flag::{ErrorLevel, LogLevel};
32use golem_client::model::ScanCursor;
33use lenient_bool::LenientBool;
34use std::collections::{BTreeSet, HashMap};
35use std::ffi::OsString;
36use std::path::PathBuf;
37use uuid::Uuid;
38
39#[cfg(feature = "server-commands")]
40use crate::command::server::ServerSubcommand;
41use crate::command::shared_args::ComponentOptionalComponentName;
42use crate::error::ShowClapHelpTarget;
43
44#[derive(Debug, Parser)]
46#[command(bin_name = command_name(), display_name = command_name(), long_version = version())]
47pub struct GolemCliCommand {
48 #[command(flatten)]
49 pub global_flags: GolemCliGlobalFlags,
50
51 #[clap(subcommand)]
52 pub subcommand: GolemCliSubcommand,
53}
54
55#[derive(clap::Args, Debug, Clone, Copy, Default)]
58#[command(about = None, long_about = None)]
59pub struct Verbosity<L: LogLevel = ErrorLevel> {
60 #[arg(
61 long,
62 short = 'v',
63 action = clap::ArgAction::Count,
64 global = true,
65 help = L::verbose_help(),
66 long_help = L::verbose_long_help(),
67 display_order = 201
68 )]
69 verbose: u8,
70
71 #[arg(
72 long,
73 short = 'q',
74 action = clap::ArgAction::Count,
75 global = true,
76 help = L::quiet_help(),
77 long_help = L::quiet_long_help(),
78 conflicts_with = "verbose",
79 display_order = 202
80 )]
81 quiet: u8,
82
83 #[arg(skip)]
84 phantom: std::marker::PhantomData<L>,
85}
86
87impl Verbosity {
88 pub fn as_clap_verbosity_flag(self) -> clap_verbosity_flag::Verbosity {
89 clap_verbosity_flag::Verbosity::new(self.verbose, self.quiet)
90 }
91}
92
93#[derive(Debug, Clone, Default, Args)]
94pub struct GolemCliGlobalFlags {
95 #[arg(long, short, global = true, display_order = 101)]
97 pub format: Option<Format>,
98
99 #[arg(long, short, global = true, conflicts_with_all = ["local", "cloud"], display_order = 102)]
101 pub profile: Option<ProfileName>,
102
103 #[arg(long, short, global = true, conflicts_with_all = ["profile", "cloud"], display_order = 103
105 )]
106 pub local: bool,
107
108 #[arg(long, short, global = true, conflicts_with_all = ["profile", "local"], display_order = 104
110 )]
111 pub cloud: bool,
112
113 #[arg(long, short, global = true, display_order = 105)]
115 pub app_manifest_path: Option<PathBuf>,
116
117 #[arg(long, short = 'A', global = true, display_order = 106)]
119 pub disable_app_manifest_discovery: bool,
120
121 #[arg(long, short, global = true, display_order = 107)]
123 pub build_profile: Option<BuildProfileName>,
124
125 #[arg(long, global = true, display_order = 108)]
127 pub config_dir: Option<PathBuf>,
128
129 #[arg(long, short, global = true, display_order = 109)]
131 pub yes: bool,
132
133 #[arg(long, global = true, display_order = 110)]
135 pub show_sensitive: bool,
136
137 #[command(flatten)]
138 pub verbosity: Verbosity,
139
140 #[arg(skip)]
143 pub golem_rust_path: Option<PathBuf>,
144
145 #[arg(skip)]
146 pub golem_rust_version: Option<String>,
147
148 #[arg(skip)]
149 pub wasm_rpc_offline: bool,
150
151 #[arg(skip)]
152 pub http_batch_size: Option<u64>,
153
154 #[arg(skip)]
155 pub auth_token: Option<Uuid>,
156
157 #[arg(skip)]
158 pub custom_global_cloud_profile: Option<ProfileName>,
159
160 #[arg(skip)]
161 pub local_server_auto_start: bool,
162}
163
164impl GolemCliGlobalFlags {
165 pub fn with_env_overrides(mut self) -> GolemCliGlobalFlags {
166 if self.profile.is_none() {
167 if let Ok(profile) = std::env::var("GOLEM_PROFILE") {
168 self.profile = Some(profile.into());
169 }
170 }
171
172 if self.app_manifest_path.is_none() {
173 if let Ok(app_manifest_path) = std::env::var("GOLEM_APP_MANIFEST_PATH") {
174 self.app_manifest_path = Some(PathBuf::from(app_manifest_path));
175 }
176 }
177
178 if !self.disable_app_manifest_discovery {
179 if let Ok(disable) = std::env::var("GOLEM_DISABLE_APP_MANIFEST_DISCOVERY") {
180 self.disable_app_manifest_discovery = disable
181 .parse::<LenientBool>()
182 .map(|b| b.into())
183 .unwrap_or_default()
184 }
185 }
186
187 if self.build_profile.is_none() {
188 if let Ok(build_profile) = std::env::var("GOLEM_BUILD_PROFILE") {
189 self.build_profile = Some(build_profile.into());
190 }
191 }
192
193 if let Ok(offline) = std::env::var("GOLEM_WASM_RPC_OFFLINE") {
194 self.wasm_rpc_offline = offline
195 .parse::<LenientBool>()
196 .map(|b| b.into())
197 .unwrap_or_default()
198 }
199
200 if self.golem_rust_path.is_none() {
201 if let Ok(wasm_rpc_path) = std::env::var("GOLEM_RUST_PATH") {
202 self.golem_rust_path = Some(PathBuf::from(wasm_rpc_path));
203 }
204 }
205
206 if self.golem_rust_version.is_none() {
207 if let Ok(version) = std::env::var("GOLEM_RUST_VERSION") {
208 self.golem_rust_version = Some(version);
209 }
210 }
211
212 if let Ok(batch_size) = std::env::var("GOLEM_HTTP_BATCH_SIZE") {
213 self.http_batch_size = Some(
214 batch_size
215 .parse()
216 .with_context(|| {
217 format!("Failed to parse GOLEM_HTTP_BATCH_SIZE: {}", batch_size)
218 })
219 .unwrap(),
220 )
221 }
222
223 if let Ok(auth_token) = std::env::var("GOLEM_AUTH_TOKEN") {
224 self.auth_token = Some(
225 auth_token
226 .parse()
227 .context("Failed to parse GOLEM_AUTH_TOKEN, expected uuid")
228 .unwrap(),
229 );
230 }
231
232 if let Ok(default_cloud_profile) = std::env::var("GOLEM_CUSTOM_GLOBAL_CLOUD_PROFILE") {
233 self.custom_global_cloud_profile = Some(default_cloud_profile.into());
234 }
235
236 if let Ok(auto_start) = std::env::var("GOLEM_LOCAL_SERVER_AUTO_START") {
237 self.local_server_auto_start = auto_start
238 .parse::<LenientBool>()
239 .map(|b| b.into())
240 .unwrap_or_default()
241 }
242
243 self
244 }
245
246 pub fn config_dir(&self) -> PathBuf {
247 self.config_dir
248 .clone()
249 .unwrap_or_else(|| dirs::home_dir().unwrap().join(".golem"))
250 }
251
252 pub fn verbosity(&self) -> clap_verbosity_flag::Verbosity {
253 self.verbosity.as_clap_verbosity_flag()
254 }
255}
256
257#[derive(Debug, Default, Parser)]
258#[command(ignore_errors = true)]
259pub struct GolemCliFallbackCommand {
260 #[command(flatten)]
261 pub global_flags: GolemCliGlobalFlags,
262
263 pub positional_args: Vec<String>,
264
265 #[arg(skip)]
266 pub parse_error: Option<clap::Error>,
267}
268
269impl GolemCliFallbackCommand {
270 fn try_parse_from<I, T>(args: I, with_env_overrides: bool) -> Self
271 where
272 I: IntoIterator<Item = T>,
273 T: Into<OsString> + Clone,
274 {
275 let args = args
276 .into_iter()
277 .map(|arg| arg.into())
278 .filter(|arg| arg != "-h" && arg != "--help")
279 .collect::<Vec<OsString>>();
280
281 let mut cmd = <Self as Parser>::try_parse_from(args).unwrap_or_else(|error| {
282 GolemCliFallbackCommand {
283 parse_error: Some(error),
284 ..Self::default()
285 }
286 });
287
288 if with_env_overrides {
289 cmd.global_flags = cmd.global_flags.with_env_overrides();
290 }
291
292 cmd
293 }
294}
295
296impl GolemCliCommand {
297 pub fn try_parse_from_lenient<I, T>(
298 iterator: I,
299 with_env_overrides: bool,
300 ) -> GolemCliCommandParseResult
301 where
302 I: IntoIterator<Item = T>,
303 T: Into<OsString> + Clone,
304 {
305 let args = iterator
306 .into_iter()
307 .map(|arg| arg.into())
308 .collect::<Vec<OsString>>();
309
310 match GolemCliCommand::try_parse_from(&args) {
311 Ok(mut command) => {
312 if with_env_overrides {
313 command.global_flags = command.global_flags.with_env_overrides()
314 }
315 GolemCliCommandParseResult::FullMatch(command)
316 }
317 Err(error) => {
318 let fallback_command =
319 GolemCliFallbackCommand::try_parse_from(&args, with_env_overrides);
320
321 let partial_match = match error.kind() {
322 ErrorKind::DisplayHelp => {
323 let positional_args = fallback_command
324 .positional_args
325 .iter()
326 .map(|arg| arg.as_ref())
327 .collect::<Vec<_>>();
328 match positional_args.as_slice() {
329 ["app"] => Some(GolemCliCommandPartialMatch::AppHelp),
330 ["component"] => Some(GolemCliCommandPartialMatch::ComponentHelp),
331 ["worker"] => Some(GolemCliCommandPartialMatch::WorkerHelp),
332 _ => None,
333 }
334 }
335 ErrorKind::MissingRequiredArgument => {
336 error.context().find_map(|context| match context {
337 (ContextKind::InvalidArg, ContextValue::Strings(args)) => {
338 Self::match_invalid_arg(
339 &fallback_command.positional_args,
340 args,
341 &Self::invalid_arg_matchers(),
342 )
343 }
344 _ => None,
345 })
346 }
347 ErrorKind::MissingSubcommand
348 | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => {
349 let positional_args = fallback_command
350 .positional_args
351 .iter()
352 .map(|arg| arg.as_ref())
353 .collect::<Vec<_>>();
354 match positional_args.as_slice() {
355 ["app"] => Some(GolemCliCommandPartialMatch::AppMissingSubcommandHelp),
356 ["component"] => {
357 Some(GolemCliCommandPartialMatch::ComponentMissingSubcommandHelp)
358 }
359 _ => None,
360 }
361 }
362 _ => None,
363 };
364
365 match partial_match {
366 Some(partial_match) => GolemCliCommandParseResult::ErrorWithPartialMatch {
367 error,
368 fallback_command,
369 partial_match,
370 },
371 None => GolemCliCommandParseResult::Error {
372 error,
373 fallback_command,
374 },
375 }
376 }
377 }
378 }
379
380 fn invalid_arg_matchers() -> Vec<InvalidArgMatcher> {
381 vec![
382 InvalidArgMatcher {
383 subcommands: vec!["worker", "invoke"],
384 found_positional_args: vec![],
385 missing_positional_arg: "worker_name",
386 to_partial_match: |_| GolemCliCommandPartialMatch::WorkerInvokeMissingWorkerName,
387 },
388 InvalidArgMatcher {
389 subcommands: vec!["worker", "invoke"],
390 found_positional_args: vec!["worker_name"],
391 missing_positional_arg: "function_name",
392 to_partial_match: |args| {
393 GolemCliCommandPartialMatch::WorkerInvokeMissingFunctionName {
394 worker_name: args[0].clone().into(),
395 }
396 },
397 },
398 InvalidArgMatcher {
399 subcommands: vec!["profile", "switch"],
400 missing_positional_arg: "profile_name",
401 found_positional_args: vec![],
402 to_partial_match: |_| GolemCliCommandPartialMatch::ProfileSwitchMissingProfileName,
403 },
404 ]
405 }
406
407 fn match_invalid_arg(
408 positional_args: &[String],
409 error_context_args: &[String],
410 matchers: &[InvalidArgMatcher],
411 ) -> Option<GolemCliCommandPartialMatch> {
412 let command = Self::command();
413
414 let positional_args = positional_args
415 .iter()
416 .map(|str| str.as_str())
417 .collect::<Vec<_>>();
418
419 for matcher in matchers {
420 if positional_args.len() < matcher.subcommands.len() {
421 continue;
422 }
423
424 let missing_arg_error_name =
425 format!("<{}>", matcher.missing_positional_arg.to_uppercase());
426 let missing_args_error_name = format!("{}...", missing_arg_error_name);
427 if !error_context_args.contains(&missing_arg_error_name)
428 && !error_context_args.contains(&missing_args_error_name)
429 {
430 continue;
431 }
432
433 if !positional_args.starts_with(&matcher.subcommands) {
434 continue;
435 }
436
437 let mut command = &command;
438 for subcommand in &matcher.subcommands {
439 command = command.find_subcommand(subcommand).unwrap();
440 }
441 let positional_arg_ids_to_idx = command
442 .get_arguments()
443 .filter(|arg| arg.is_positional())
444 .enumerate()
445 .map(|(idx, arg)| (arg.get_id().to_string(), idx))
446 .collect::<HashMap<_, _>>();
447
448 let mut found_args = Vec::<String>::with_capacity(matcher.found_positional_args.len());
449 for expected_arg_name in &matcher.found_positional_args {
450 let Some(idx) = positional_arg_ids_to_idx.get(*expected_arg_name) else {
451 break;
452 };
453 let Some(arg_value) = positional_args.get(matcher.subcommands.len() + *idx) else {
454 break;
455 };
456 found_args.push(arg_value.to_string());
457 }
458 if found_args.len() == matcher.found_positional_args.len() {
459 return Some((matcher.to_partial_match)(found_args));
460 }
461 }
462
463 None
464 }
465}
466
467#[derive(Debug)]
468struct InvalidArgMatcher {
469 pub subcommands: Vec<&'static str>,
470 pub found_positional_args: Vec<&'static str>,
471 pub missing_positional_arg: &'static str,
472 pub to_partial_match: fn(Vec<String>) -> GolemCliCommandPartialMatch,
473}
474
475#[allow(clippy::large_enum_variant)]
476pub enum GolemCliCommandParseResult {
477 FullMatch(GolemCliCommand),
478 ErrorWithPartialMatch {
479 error: clap::Error,
480 fallback_command: GolemCliFallbackCommand,
481 partial_match: GolemCliCommandPartialMatch,
482 },
483 Error {
484 error: clap::Error,
485 fallback_command: GolemCliFallbackCommand,
486 },
487}
488
489#[derive(Debug)]
490pub enum GolemCliCommandPartialMatch {
491 AppHelp,
492 AppMissingSubcommandHelp,
493 ComponentHelp,
494 ComponentMissingSubcommandHelp,
495 WorkerHelp,
496 WorkerInvokeMissingFunctionName { worker_name: WorkerName },
497 WorkerInvokeMissingWorkerName,
498 ProfileSwitchMissingProfileName,
499}
500
501#[derive(Debug, Subcommand)]
502pub enum GolemCliSubcommand {
503 #[command(alias = "application")]
504 App {
506 #[clap(subcommand)]
507 subcommand: AppSubcommand,
508 },
509 Component {
511 #[clap(subcommand)]
512 subcommand: ComponentSubcommand,
513 },
514 Worker {
516 #[clap(subcommand)]
517 subcommand: WorkerSubcommand,
518 },
519 Api {
521 #[clap(subcommand)]
522 subcommand: ApiSubcommand,
523 },
524 Plugin {
526 #[clap(subcommand)]
527 subcommand: PluginSubcommand,
528 },
529 Profile {
531 #[clap(subcommand)]
532 subcommand: ProfileSubcommand,
533 },
534 #[cfg(feature = "server-commands")]
536 Server {
537 #[clap(subcommand)]
538 subcommand: ServerSubcommand,
539 },
540 Cloud {
542 #[clap(subcommand)]
543 subcommand: CloudSubcommand,
544 },
545 Repl {
547 #[command(flatten)]
548 component_name: ComponentOptionalComponentName,
549 version: Option<u64>,
551 },
552 Completion {
554 shell: clap_complete::Shell,
556 },
557}
558
559pub mod shared_args {
560 use crate::cloud::AccountId;
561 use crate::model::app::AppBuildStep;
562 use crate::model::{
563 ComponentName, ProjectName, ProjectReference, WorkerName, WorkerUpdateMode,
564 };
565 use clap::Args;
566 use golem_templates::model::GuestLanguage;
567
568 pub type ComponentTemplateName = String;
569 pub type NewWorkerArgument = String;
570 pub type WorkerFunctionArgument = String;
571 pub type WorkerFunctionName = String;
572
573 #[derive(Debug, Args)]
574 pub struct ComponentMandatoryComponentName {
575 #[arg(verbatim_doc_comment)]
582 pub component_name: ComponentName,
583 }
584
585 #[derive(Debug, Args)]
586 pub struct ComponentOptionalComponentName {
587 #[arg(verbatim_doc_comment)]
594 pub component_name: Option<ComponentName>,
595 }
596
597 #[derive(Debug, Args)]
598 pub struct ComponentOptionalComponentNames {
599 #[arg(verbatim_doc_comment)]
606 pub component_name: Vec<ComponentName>,
607 }
608
609 #[derive(Debug, Args)]
610 pub struct AppOptionalComponentNames {
611 pub component_name: Vec<ComponentName>,
618 }
619
620 #[derive(Debug, Args)]
621 pub struct LanguageArg {
622 #[clap(long, short)]
623 pub language: GuestLanguage,
624 }
625
626 #[derive(Debug, Args)]
627 pub struct ForceBuildArg {
628 #[clap(long, default_value = "false")]
630 pub force_build: bool,
631 }
632
633 #[derive(Debug, Args)]
634 pub struct BuildArgs {
635 #[clap(long, short)]
637 pub step: Vec<AppBuildStep>,
638 #[command(flatten)]
639 pub force_build: ForceBuildArg,
640 }
641
642 #[derive(Debug, Args)]
643 pub struct WorkerNameArg {
644 #[arg(verbatim_doc_comment)]
651 pub worker_name: WorkerName,
652 }
653
654 #[derive(Debug, Args)]
655 pub struct StreamArgs {
656 #[clap(long, short = 'L')]
658 pub stream_no_log_level: bool,
659 #[clap(long, short = 'T')]
661 pub stream_no_timestamp: bool,
662 }
663
664 #[derive(Debug, Args)]
665 pub struct UpdateOrRedeployArgs {
666 #[clap(long, value_name = "UPDATE_MODE", short, conflicts_with_all = ["redeploy_workers", "redeploy_all"], num_args = 0..=1
668 )]
669 pub update_workers: Option<WorkerUpdateMode>,
670 #[clap(long, conflicts_with_all = ["update_workers"])]
672 pub redeploy_workers: bool,
673 #[clap(long, conflicts_with_all = ["redeploy_all"])]
675 pub redeploy_http_api: bool,
676 #[clap(long, short, conflicts_with_all = ["update_workers", "redeploy_workers", "redeploy_http_api"]
678 )]
679 pub redeploy_all: bool,
680 }
681
682 impl UpdateOrRedeployArgs {
683 pub fn none() -> Self {
684 UpdateOrRedeployArgs {
685 update_workers: None,
686 redeploy_workers: false,
687 redeploy_http_api: false,
688 redeploy_all: false,
689 }
690 }
691
692 pub fn redeploy_workers(&self, profile_args: &UpdateOrRedeployArgs) -> bool {
693 profile_args.redeploy_all
694 || profile_args.redeploy_workers
695 || self.redeploy_all
696 || self.redeploy_workers
697 }
698
699 pub fn redeploy_http_api(&self, profile_args: &UpdateOrRedeployArgs) -> bool {
700 profile_args.redeploy_all
701 || profile_args.redeploy_http_api
702 || self.redeploy_all
703 || self.redeploy_http_api
704 }
705 }
706
707 #[derive(Debug, Args)]
708 pub struct ProjectArg {
709 #[arg(verbatim_doc_comment)]
714 pub project: ProjectReference,
715 }
716
717 #[derive(Debug, Args)]
718 pub struct ProjectOptionalFlagArg {
719 #[arg(verbatim_doc_comment, long)]
724 pub project: Option<ProjectReference>,
725 }
726
727 #[derive(Debug, Args)]
728 pub struct AccountIdOptionalArg {
729 #[arg(long)]
731 pub account_id: Option<AccountId>,
732 }
733
734 #[derive(clap::Args, Debug, Clone)]
735 pub struct PluginScopeArgs {
736 #[arg(long, conflicts_with_all=["account", "project", "component"])]
738 pub global: bool,
739 #[arg(long, conflicts_with_all = ["global"])]
741 pub account: Option<String>,
742 #[arg(long, conflicts_with_all = ["global"])]
744 pub project: Option<ProjectName>,
745 #[arg(long, conflicts_with_all=["global"])]
747 pub component: Option<ComponentName>,
748 }
749
750 impl PluginScopeArgs {
751 pub fn is_global(&self) -> bool {
752 self.global
753 || (self.account.is_none() && self.project.is_none() && self.component.is_none())
754 }
755 }
756}
757
758pub mod app {
759 use crate::command::shared_args::{
760 AppOptionalComponentNames, BuildArgs, ForceBuildArg, UpdateOrRedeployArgs,
761 };
762 use crate::model::WorkerUpdateMode;
763 use clap::Subcommand;
764 use golem_templates::model::GuestLanguage;
765
766 #[derive(Debug, Subcommand)]
767 pub enum AppSubcommand {
768 New {
770 application_name: Option<String>,
772 language: Vec<GuestLanguage>,
774 },
775 Build {
777 #[command(flatten)]
778 component_name: AppOptionalComponentNames,
779 #[command(flatten)]
780 build: BuildArgs,
781 },
782 Deploy {
784 #[command(flatten)]
785 component_name: AppOptionalComponentNames,
786 #[command(flatten)]
787 force_build: ForceBuildArg,
788 #[command(flatten)]
789 update_or_redeploy: UpdateOrRedeployArgs,
790 },
791 Clean {
793 #[command(flatten)]
794 component_name: AppOptionalComponentNames,
795 },
796 UpdateWorkers {
798 #[command(flatten)]
799 component_name: AppOptionalComponentNames,
800 #[arg(long, short, default_value = "auto")]
802 update_mode: WorkerUpdateMode,
803 },
804 RedeployWorkers {
806 #[command(flatten)]
807 component_name: AppOptionalComponentNames,
808 },
809 Diagnose {
811 #[command(flatten)]
812 component_name: AppOptionalComponentNames,
813 },
814 #[clap(external_subcommand)]
816 CustomCommand(Vec<String>),
817 }
818}
819
820pub mod component {
821 use crate::command::component::plugin::ComponentPluginSubcommand;
822 use crate::command::shared_args::{
823 BuildArgs, ComponentOptionalComponentName, ComponentOptionalComponentNames,
824 ComponentTemplateName, ForceBuildArg, UpdateOrRedeployArgs,
825 };
826 use crate::model::app::DependencyType;
827 use crate::model::{ComponentName, WorkerUpdateMode};
828 use clap::Subcommand;
829 use golem_templates::model::PackageName;
830 use std::path::PathBuf;
831 use url::Url;
832
833 #[derive(Debug, Subcommand)]
834 pub enum ComponentSubcommand {
835 New {
837 component_template: Option<ComponentTemplateName>,
839 component_name: Option<PackageName>,
841 },
842 Templates {
844 filter: Option<String>,
846 },
847 Build {
849 #[command(flatten)]
850 component_name: ComponentOptionalComponentNames,
851 #[command(flatten)]
852 build: BuildArgs,
853 },
854 Deploy {
856 #[command(flatten)]
857 component_name: ComponentOptionalComponentNames,
858 #[command(flatten)]
859 force_build: ForceBuildArg,
860 #[command(flatten)]
861 update_or_redeploy: UpdateOrRedeployArgs,
862 },
863 Clean {
865 #[command(flatten)]
866 component_name: ComponentOptionalComponentNames,
867 },
868 AddDependency {
870 #[arg(long)]
872 component_name: Option<ComponentName>,
873 #[arg(long, conflicts_with_all = ["target_component_path", "target_component_url"])]
875 target_component_name: Option<ComponentName>,
876 #[arg(long, conflicts_with_all = ["target_component_name", "target_component_url"])]
878 target_component_path: Option<PathBuf>,
879 #[arg(long, conflicts_with_all = ["target_component_name", "target_component_path"])]
881 target_component_url: Option<Url>,
882 #[arg(long)]
884 dependency_type: Option<DependencyType>,
885 },
886 List {
888 #[command(flatten)]
889 component_name: ComponentOptionalComponentName,
890 },
891 Get {
893 #[command(flatten)]
894 component_name: ComponentOptionalComponentName,
895 version: Option<u64>,
897 },
898 UpdateWorkers {
900 #[command(flatten)]
901 component_name: ComponentOptionalComponentName,
902 #[arg(long, short, default_value_t = WorkerUpdateMode::Automatic)]
904 update_mode: WorkerUpdateMode,
905 },
906 RedeployWorkers {
908 #[command(flatten)]
909 component_name: ComponentOptionalComponentName,
910 },
911 Plugin {
913 #[command(subcommand)]
914 subcommand: ComponentPluginSubcommand,
915 },
916 Diagnose {
918 #[command(flatten)]
919 component_name: ComponentOptionalComponentNames,
920 },
921 }
922
923 pub mod plugin {
924 use crate::command::parse_key_val;
925 use crate::command::shared_args::ComponentOptionalComponentName;
926 use clap::Subcommand;
927 use golem_common::base_model::PluginInstallationId;
928
929 #[derive(Debug, Subcommand)]
930 pub enum ComponentPluginSubcommand {
931 Install {
933 #[command(flatten)]
934 component_name: ComponentOptionalComponentName,
935 #[arg(long)]
937 plugin_name: String,
938 #[arg(long)]
940 plugin_version: String,
941 #[arg(long)]
943 priority: i32,
944 #[arg(long, value_parser = parse_key_val, value_name = "KEY=VAL")]
946 param: Vec<(String, String)>,
947 },
948 Get {
950 #[command(flatten)]
951 component_name: ComponentOptionalComponentName,
952 version: Option<u64>,
954 },
955 Update {
957 #[command(flatten)]
959 component_name: ComponentOptionalComponentName,
960 #[arg(long)]
962 installation_id: PluginInstallationId,
963 #[arg(long)]
965 priority: i32,
966 #[arg(long, value_parser = parse_key_val, value_name = "KEY=VAL")]
968 param: Vec<(String, String)>,
969 },
970 Uninstall {
972 #[command(flatten)]
974 component_name: ComponentOptionalComponentName,
975 #[arg(long)]
977 installation_id: PluginInstallationId,
978 },
979 }
980 }
981}
982
983pub mod worker {
984 use crate::command::parse_cursor;
985 use crate::command::parse_key_val;
986 use crate::command::shared_args::{
987 ComponentOptionalComponentName, NewWorkerArgument, StreamArgs, WorkerFunctionArgument,
988 WorkerFunctionName, WorkerNameArg,
989 };
990 use crate::model::{IdempotencyKey, WorkerUpdateMode};
991 use clap::Subcommand;
992 use golem_client::model::ScanCursor;
993
994 #[derive(Debug, Subcommand)]
995 pub enum WorkerSubcommand {
996 New {
998 #[command(flatten)]
999 worker_name: WorkerNameArg,
1000 arguments: Vec<NewWorkerArgument>,
1002 #[arg(short, long, value_parser = parse_key_val, value_name = "ENV=VAL")]
1004 env: Vec<(String, String)>,
1005 },
1006 Invoke {
1009 #[command(flatten)]
1010 worker_name: WorkerNameArg,
1011 function_name: WorkerFunctionName,
1013 arguments: Vec<WorkerFunctionArgument>,
1015 #[clap(long, short)]
1017 enqueue: bool,
1018 #[clap(long, short)]
1020 idempotency_key: Option<IdempotencyKey>,
1021 #[clap(long, short)]
1022 stream: bool,
1025 #[command(flatten)]
1026 stream_args: StreamArgs,
1027 },
1028 Get {
1030 #[command(flatten)]
1031 worker_name: WorkerNameArg,
1032 },
1033 Delete {
1035 #[command(flatten)]
1036 worker_name: WorkerNameArg,
1037 },
1038 List {
1040 #[command(flatten)]
1041 component_name: ComponentOptionalComponentName,
1042 #[arg(long)]
1047 filter: Vec<String>,
1048 #[arg(long, short, value_parser = parse_cursor)]
1054 scan_cursor: Option<ScanCursor>,
1055 #[arg(long, short)]
1058 max_count: Option<u64>,
1059 #[arg(long, default_value_t = false)]
1061 precise: bool,
1062 },
1063 Stream {
1065 #[command(flatten)]
1066 worker_name: WorkerNameArg,
1067 #[command(flatten)]
1068 stream_args: StreamArgs,
1069 },
1070 Update {
1072 #[command(flatten)]
1073 worker_name: WorkerNameArg,
1074 mode: Option<WorkerUpdateMode>,
1076 target_version: Option<u64>,
1078 },
1079 Interrupt {
1081 #[command(flatten)]
1082 worker_name: WorkerNameArg,
1083 },
1084 Resume {
1086 #[command(flatten)]
1087 worker_name: WorkerNameArg,
1088 },
1089 SimulateCrash {
1093 #[command(flatten)]
1094 worker_name: WorkerNameArg,
1095 },
1096 Oplog {
1098 #[command(flatten)]
1099 worker_name: WorkerNameArg,
1100 #[arg(long, conflicts_with = "query")]
1102 from: Option<u64>,
1103 #[arg(long, conflicts_with = "from")]
1105 query: Option<String>,
1106 },
1107 Revert {
1109 #[command(flatten)]
1110 worker_name: WorkerNameArg,
1111 #[arg(long, conflicts_with = "number_of_invocations")]
1113 last_oplog_index: Option<u64>,
1114 #[arg(long, conflicts_with = "last_oplog_index")]
1116 number_of_invocations: Option<u64>,
1117 },
1118 CancelInvocation {
1120 #[command(flatten)]
1121 worker_name: WorkerNameArg,
1122 idempotency_key: IdempotencyKey,
1124 },
1125 }
1126}
1127
1128pub mod api {
1129 use crate::command::api::cloud::ApiCloudSubcommand;
1130 use crate::command::api::definition::ApiDefinitionSubcommand;
1131 use crate::command::api::deployment::ApiDeploymentSubcommand;
1132 use crate::command::api::security_scheme::ApiSecuritySchemeSubcommand;
1133 use crate::command::shared_args::UpdateOrRedeployArgs;
1134 use clap::Subcommand;
1135
1136 #[derive(Debug, Subcommand)]
1137 pub enum ApiSubcommand {
1138 Deploy {
1140 #[command(flatten)]
1141 update_or_redeploy: UpdateOrRedeployArgs,
1142 },
1143 Definition {
1145 #[clap(subcommand)]
1146 subcommand: ApiDefinitionSubcommand,
1147 },
1148 Deployment {
1150 #[clap(subcommand)]
1151 subcommand: ApiDeploymentSubcommand,
1152 },
1153 SecurityScheme {
1155 #[clap(subcommand)]
1156 subcommand: ApiSecuritySchemeSubcommand,
1157 },
1158 Cloud {
1160 #[clap(subcommand)]
1161 subcommand: ApiCloudSubcommand,
1162 },
1163 }
1164
1165 pub mod definition {
1166 use crate::command::shared_args::{ProjectOptionalFlagArg, UpdateOrRedeployArgs};
1167 use crate::model::api::{ApiDefinitionId, ApiDefinitionVersion};
1168 use crate::model::app::HttpApiDefinitionName;
1169 use clap::Subcommand;
1170
1171 #[derive(Debug, Subcommand)]
1172 pub enum ApiDefinitionSubcommand {
1173 Deploy {
1175 http_api_definition_name: Option<HttpApiDefinitionName>,
1177 #[command(flatten)]
1178 update_or_redeploy: UpdateOrRedeployArgs,
1179 },
1180 Get {
1182 #[command(flatten)]
1183 project: ProjectOptionalFlagArg,
1184 #[arg(short, long)]
1186 id: ApiDefinitionId,
1187 #[arg(long)]
1189 version: ApiDefinitionVersion,
1190 },
1191 List {
1193 #[command(flatten)]
1194 project: ProjectOptionalFlagArg,
1195 #[arg(short, long)]
1197 id: Option<ApiDefinitionId>,
1198 },
1199 Delete {
1201 #[command(flatten)]
1202 project: ProjectOptionalFlagArg,
1203 #[arg(short, long)]
1205 id: ApiDefinitionId,
1206 #[arg(long)]
1208 version: ApiDefinitionVersion,
1209 },
1210 }
1211 }
1212
1213 pub mod deployment {
1214 use crate::command::shared_args::{ProjectOptionalFlagArg, UpdateOrRedeployArgs};
1215 use crate::model::api::ApiDefinitionId;
1216 use clap::Subcommand;
1217
1218 #[derive(Debug, Subcommand)]
1219 pub enum ApiDeploymentSubcommand {
1220 Deploy {
1222 host_or_site: Option<String>,
1224 #[command(flatten)]
1225 update_or_redeploy: UpdateOrRedeployArgs,
1226 },
1227 Get {
1229 #[command(flatten)]
1230 project: ProjectOptionalFlagArg,
1231 #[arg(value_name = "subdomain.host")]
1233 site: String,
1234 },
1235 List {
1237 #[command(flatten)]
1238 project: ProjectOptionalFlagArg,
1239 definition: Option<ApiDefinitionId>,
1241 },
1242 Delete {
1244 #[command(flatten)]
1245 project: ProjectOptionalFlagArg,
1246 #[arg(value_name = "subdomain.host")]
1248 site: String,
1249 },
1250 }
1251 }
1252
1253 pub mod security_scheme {
1254 use crate::command::shared_args::ProjectOptionalFlagArg;
1255 use crate::model::api::IdentityProviderType;
1256 use clap::Subcommand;
1257
1258 #[derive(Debug, Subcommand)]
1259 pub enum ApiSecuritySchemeSubcommand {
1260 Create {
1262 #[command(flatten)]
1263 project: ProjectOptionalFlagArg,
1264 security_scheme_id: String,
1266 #[arg(long)]
1268 provider_type: IdentityProviderType,
1269 #[arg(long)]
1271 client_id: String,
1272 #[arg(long)]
1274 client_secret: String,
1275 #[arg(long)]
1276 scope: Vec<String>,
1278 #[arg(long)]
1279 redirect_url: String,
1281 },
1282
1283 Get {
1285 #[command(flatten)]
1286 project: ProjectOptionalFlagArg,
1287 security_scheme_id: String,
1289 },
1290 }
1291 }
1292
1293 pub mod cloud {
1294 use crate::command::api::cloud::certificate::ApiCertificateSubcommand;
1295 use crate::command::api::cloud::domain::ApiDomainSubcommand;
1296 use clap::Subcommand;
1297
1298 #[derive(Debug, Subcommand)]
1299 pub enum ApiCloudSubcommand {
1300 Domain {
1302 #[clap(subcommand)]
1303 subcommand: ApiDomainSubcommand,
1304 },
1305 Certificate {
1307 #[clap(subcommand)]
1308 subcommand: ApiCertificateSubcommand,
1309 },
1310 }
1311
1312 pub mod domain {
1313 use crate::command::shared_args::ProjectArg;
1314 use clap::Subcommand;
1315
1316 #[derive(Debug, Subcommand)]
1317 pub enum ApiDomainSubcommand {
1318 Get {
1320 #[clap(flatten)]
1321 project: ProjectArg,
1322 },
1323 New {
1325 #[clap(flatten)]
1326 project: ProjectArg,
1327 domain_name: String,
1329 },
1330 Delete {
1332 #[clap(flatten)]
1333 project: ProjectArg,
1334 domain_name: String,
1336 },
1337 }
1338 }
1339
1340 pub mod certificate {
1341 use crate::command::shared_args::ProjectArg;
1342 use crate::model::PathBufOrStdin;
1343 use clap::Subcommand;
1344 use uuid::Uuid;
1345
1346 #[derive(Debug, Subcommand)]
1347 pub enum ApiCertificateSubcommand {
1348 Get {
1350 #[clap(flatten)]
1351 project: ProjectArg,
1352 certificate_id: Option<Uuid>,
1354 },
1355 New {
1357 #[clap(flatten)]
1358 project: ProjectArg,
1359 #[arg(short, long)]
1361 domain_name: String,
1362 #[arg(long, value_hint = clap::ValueHint::FilePath)]
1364 certificate_body: PathBufOrStdin,
1365 #[arg(long, value_hint = clap::ValueHint::FilePath)]
1367 certificate_private_key: PathBufOrStdin,
1368 },
1369 #[command()]
1371 Delete {
1372 #[clap(flatten)]
1373 project: ProjectArg,
1374 certificate_id: Uuid,
1376 },
1377 }
1378 }
1379 }
1380}
1381
1382pub mod plugin {
1383 use crate::command::shared_args::PluginScopeArgs;
1384 use crate::model::PathBufOrStdin;
1385 use clap::Subcommand;
1386
1387 #[derive(Debug, Subcommand)]
1388 pub enum PluginSubcommand {
1389 List {
1391 #[command(flatten)]
1393 scope: PluginScopeArgs,
1394 },
1395 Get {
1397 plugin_name: String,
1399 version: String,
1401 },
1402 Register {
1404 #[command(flatten)]
1405 scope: PluginScopeArgs,
1406 manifest: PathBufOrStdin,
1408 },
1409 Unregister {
1411 plugin_name: String,
1413 version: String,
1415 },
1416 }
1417}
1418
1419pub mod profile {
1420 use crate::command::profile::config::ProfileConfigSubcommand;
1421 use crate::config::{ProfileKind, ProfileName};
1422 use crate::model::Format;
1423 use clap::Subcommand;
1424 use url::Url;
1425
1426 #[allow(clippy::large_enum_variant)]
1427 #[derive(Debug, Subcommand)]
1428 pub enum ProfileSubcommand {
1429 New {
1431 profile_kind: ProfileKind,
1433 name: Option<ProfileName>,
1435 #[arg(long)]
1437 set_active: bool,
1438 #[arg(long)]
1440 component_url: Option<Url>,
1441 #[arg(long)]
1443 worker_url: Option<Url>,
1444 #[arg(long)]
1446 cloud_url: Option<Url>,
1447 #[arg(long, default_value_t = Format::Text)]
1449 default_format: Format,
1450 #[arg(long, hide = true)]
1457 allow_insecure: bool,
1458 },
1459 List,
1461 Switch {
1463 profile_name: ProfileName,
1465 },
1466 Get {
1468 profile_name: Option<ProfileName>,
1470 },
1471 Delete {
1473 profile_name: ProfileName,
1475 },
1476 Config {
1478 profile_name: ProfileName,
1480 #[command(subcommand)]
1481 subcommand: ProfileConfigSubcommand,
1482 },
1483 }
1484
1485 pub mod config {
1486 use crate::model::Format;
1487 use clap::Subcommand;
1488
1489 #[derive(Debug, Subcommand)]
1490 pub enum ProfileConfigSubcommand {
1491 SetFormat {
1493 format: Format,
1495 },
1496 }
1497 }
1498}
1499
1500pub mod cloud {
1501 use crate::command::cloud::account::AccountSubcommand;
1502 use crate::command::cloud::project::ProjectSubcommand;
1503 use crate::command::cloud::token::TokenSubcommand;
1504 use clap::Subcommand;
1505
1506 #[derive(Debug, Subcommand)]
1507 pub enum CloudSubcommand {
1508 Project {
1510 #[clap(subcommand)]
1511 subcommand: ProjectSubcommand,
1512 },
1513 Account {
1515 #[clap(subcommand)]
1516 subcommand: AccountSubcommand,
1517 },
1518 Token {
1520 #[clap(subcommand)]
1521 subcommand: TokenSubcommand,
1522 },
1523 }
1524
1525 pub mod token {
1526 use crate::command::parse_instant;
1527 use crate::model::TokenId;
1528 use chrono::{DateTime, Utc};
1529 use clap::Subcommand;
1530
1531 #[derive(Debug, Subcommand)]
1532 pub enum TokenSubcommand {
1533 List,
1535 New {
1537 #[arg(long, value_parser = parse_instant, default_value = "2100-01-01T00:00:00Z")]
1539 expires_at: DateTime<Utc>,
1540 },
1541 Delete {
1543 token_id: TokenId,
1545 },
1546 }
1547 }
1548
1549 pub mod account {
1550 use crate::command::cloud::account::grant::GrantSubcommand;
1551 use crate::command::shared_args::AccountIdOptionalArg;
1552 use clap::Subcommand;
1553
1554 #[derive(Debug, Subcommand)]
1555 pub enum AccountSubcommand {
1556 Get {
1558 #[command(flatten)]
1559 account_id: AccountIdOptionalArg,
1560 },
1561 Update {
1563 #[command(flatten)]
1564 account_id: AccountIdOptionalArg,
1565 account_name: Option<String>,
1567 account_email: Option<String>,
1569 },
1570 New {
1572 account_name: String,
1574 account_email: String,
1576 },
1577 Delete {
1579 #[command(flatten)]
1580 account_id: AccountIdOptionalArg,
1581 },
1582 Grant {
1584 #[command(subcommand)]
1585 subcommand: GrantSubcommand,
1586 },
1587 }
1588
1589 pub mod grant {
1590 use crate::command::shared_args::AccountIdOptionalArg;
1591 use crate::model::Role;
1592 use clap::Subcommand;
1593
1594 #[derive(Subcommand, Debug)]
1595 pub enum GrantSubcommand {
1596 Get {
1598 #[command(flatten)]
1599 account_id: AccountIdOptionalArg,
1600 },
1601 New {
1603 #[command(flatten)]
1604 account_id: AccountIdOptionalArg,
1605 role: Role,
1607 },
1608 Delete {
1610 #[command(flatten)]
1611 account_id: AccountIdOptionalArg,
1612 role: Role,
1614 },
1615 }
1616 }
1617 }
1618
1619 pub mod project {
1620
1621 use crate::command::cloud::project::plugin::ProjectPluginSubcommand;
1622 use crate::command::cloud::project::policy::PolicySubcommand;
1623 use crate::model::{ProjectAction, ProjectName, ProjectPolicyId, ProjectReference};
1624 use clap::Subcommand;
1625
1626 #[derive(clap::Args, Debug)]
1627 #[group(required = true, multiple = false)]
1628 pub struct ProjectActionsOrPolicyId {
1629 #[arg(long, required = true, group = "project_actions_or_policy")]
1631 pub policy_id: Option<ProjectPolicyId>,
1632 #[arg(long, required = true, group = "project_actions_or_policy")]
1634 pub action: Option<Vec<ProjectAction>>,
1635 }
1636
1637 #[derive(Debug, Subcommand)]
1638 pub enum ProjectSubcommand {
1639 New {
1641 project_name: ProjectName,
1643 #[arg(short, long)]
1645 description: Option<String>,
1646 },
1647 List {
1649 project_name: Option<ProjectName>,
1651 },
1652 GetDefault,
1654 Grant {
1656 project_reference: ProjectReference,
1658 recipient_email: String,
1660 #[command(flatten)]
1661 project_actions_or_policy_id: ProjectActionsOrPolicyId,
1662 },
1663 Policy {
1665 #[command(subcommand)]
1666 subcommand: PolicySubcommand,
1667 },
1668 Plugin {
1670 #[command(subcommand)]
1671 subcommand: ProjectPluginSubcommand,
1672 },
1673 }
1674
1675 pub mod policy {
1676 use crate::model::{ProjectAction, ProjectPolicyId};
1677 use clap::Subcommand;
1678
1679 #[derive(Subcommand, Debug)]
1680 pub enum PolicySubcommand {
1681 New {
1683 policy_name: String,
1685 actions: Vec<ProjectAction>,
1687 },
1688 #[command()]
1690 Get {
1691 policy_id: ProjectPolicyId,
1693 },
1694 }
1695 }
1696
1697 pub mod plugin {
1698 use crate::command::parse_key_val;
1699 use crate::command::shared_args::ProjectArg;
1700 use clap::Subcommand;
1701 use golem_common::base_model::PluginInstallationId;
1702
1703 #[derive(Debug, Subcommand)]
1704 pub enum ProjectPluginSubcommand {
1705 Install {
1707 #[clap(flatten)]
1708 project: ProjectArg,
1709 #[arg(long)]
1711 plugin_name: String,
1712 #[arg(long)]
1714 plugin_version: String,
1715 #[arg(long)]
1717 priority: i32,
1718 #[arg(long, value_parser = parse_key_val, value_name = "KEY=VAL")]
1720 param: Vec<(String, String)>,
1721 },
1722 Get {
1724 #[clap(flatten)]
1725 project: ProjectArg,
1726 },
1731 Update {
1733 #[clap(flatten)]
1734 project: ProjectArg,
1735 plugin_installation_id: PluginInstallationId,
1737 #[arg(long)]
1739 priority: i32,
1740 #[arg(long, value_parser = parse_key_val, value_name = "KEY=VAL")]
1742 param: Vec<(String, String)>,
1743 },
1744 Uninstall {
1746 #[clap(flatten)]
1747 project: ProjectArg,
1748 plugin_installation_id: PluginInstallationId,
1750 },
1751 }
1752 }
1753 }
1754}
1755
1756pub mod server {
1757 use clap::{Args, Subcommand};
1758 use std::path::PathBuf;
1759
1760 #[derive(Debug, Args, Default)]
1761 pub struct RunArgs {
1762 #[clap(long)]
1764 pub router_addr: Option<String>,
1765
1766 #[clap(long)]
1768 pub router_port: Option<u16>,
1769
1770 #[clap(long)]
1772 pub custom_request_port: Option<u16>,
1773
1774 #[clap(long)]
1776 pub data_dir: Option<PathBuf>,
1777
1778 #[clap(long)]
1780 pub clean: bool,
1781 }
1782
1783 impl RunArgs {
1784 pub fn router_addr(&self) -> &str {
1785 self.router_addr.as_deref().unwrap_or("0.0.0.0")
1786 }
1787
1788 pub fn router_port(&self) -> u16 {
1789 self.router_port.unwrap_or(9881)
1790 }
1791
1792 pub fn custom_request_port(&self) -> u16 {
1793 self.custom_request_port.unwrap_or(9006)
1794 }
1795 }
1796
1797 #[derive(Debug, Subcommand)]
1798 pub enum ServerSubcommand {
1799 Run {
1801 #[clap(flatten)]
1802 args: RunArgs,
1803 },
1804 Clean,
1806 }
1807}
1808
1809pub fn builtin_app_subcommands() -> BTreeSet<String> {
1810 GolemCliCommand::command()
1811 .find_subcommand("app")
1812 .unwrap()
1813 .get_subcommands()
1814 .map(|subcommand| subcommand.get_name().to_string())
1815 .collect()
1816}
1817
1818fn help_target_to_subcommand_names(target: ShowClapHelpTarget) -> Vec<&'static str> {
1819 match target {
1820 ShowClapHelpTarget::AppNew => {
1821 vec!["app", "new"]
1822 }
1823 ShowClapHelpTarget::ComponentNew => {
1824 vec!["component", "new"]
1825 }
1826 ShowClapHelpTarget::ComponentAddDependency => {
1827 vec!["component", "add-dependency"]
1828 }
1829 }
1830}
1831
1832pub fn help_target_to_command(target: ShowClapHelpTarget) -> Command {
1833 let command = GolemCliCommand::command();
1834 let mut command = &command;
1835
1836 for subcommand in help_target_to_subcommand_names(target) {
1837 command = command.find_subcommand(subcommand).unwrap();
1838 }
1839
1840 command.clone()
1841}
1842
1843fn parse_key_val(key_and_val: &str) -> anyhow::Result<(String, String)> {
1844 let pos = key_and_val.find('=').ok_or_else(|| {
1845 anyhow!(
1846 "invalid KEY=VALUE: no `=` found in `{}`",
1847 key_and_val.log_color_error_highlight()
1848 )
1849 })?;
1850 Ok((
1851 key_and_val[..pos].to_string(),
1852 key_and_val[pos + 1..].to_string(),
1853 ))
1854}
1855
1856fn parse_cursor(cursor: &str) -> anyhow::Result<ScanCursor> {
1858 let parts = cursor.split('/').collect::<Vec<_>>();
1859
1860 if parts.len() != 2 {
1861 bail!("Invalid cursor format: {}", cursor);
1862 }
1863
1864 Ok(ScanCursor {
1865 layer: parts[0].parse()?,
1866 cursor: parts[1].parse()?,
1867 })
1868}
1869
1870fn parse_instant(
1871 s: &str,
1872) -> Result<DateTime<Utc>, Box<dyn std::error::Error + Send + Sync + 'static>> {
1873 match s.parse::<DateTime<Utc>>() {
1874 Ok(dt) => Ok(dt),
1875 Err(err) => Err(err.into()),
1876 }
1877}
1878
1879#[cfg(test)]
1880mod test {
1881 use crate::command::{
1882 builtin_app_subcommands, help_target_to_subcommand_names, GolemCliCommand,
1883 };
1884 use crate::error::ShowClapHelpTarget;
1885 use assert2::assert;
1886 use clap::builder::StyledStr;
1887 use clap::{Command, CommandFactory};
1888 use itertools::Itertools;
1889 use std::collections::{BTreeMap, BTreeSet};
1890 use strum::IntoEnumIterator;
1891 use test_r::test;
1892
1893 #[test]
1894 fn command_debug_assert() {
1895 GolemCliCommand::command().debug_assert();
1896 }
1897
1898 #[test]
1899 fn all_commands_and_args_has_doc() {
1900 fn collect_docs(
1901 path: &mut Vec<String>,
1902 doc_by_cmd_path: &mut BTreeMap<String, Option<StyledStr>>,
1903 command: &Command,
1904 ) {
1905 path.push(command.get_name().to_string());
1906 let key = path.iter().join(" ");
1907
1908 doc_by_cmd_path.insert(key.clone(), command.get_about().cloned());
1909
1910 for arg in command.get_arguments() {
1911 doc_by_cmd_path.insert(
1912 if arg.is_positional() {
1913 format!("{} |{}|", key, arg.get_id().to_string().to_uppercase())
1914 } else {
1915 format!("{} --{}", key, arg.get_id())
1916 },
1917 arg.get_help().cloned(),
1918 );
1919 }
1920
1921 for subcommand in command.get_subcommands() {
1922 collect_docs(path, doc_by_cmd_path, subcommand);
1923 }
1924
1925 path.pop();
1926 }
1927
1928 let mut path = vec![];
1929 let mut doc_by_cmd_path = BTreeMap::new();
1930 collect_docs(&mut path, &mut doc_by_cmd_path, &GolemCliCommand::command());
1931
1932 let elems_without_about = doc_by_cmd_path
1933 .into_iter()
1934 .filter_map(|(path, about)| about.is_none().then_some(path))
1935 .collect::<Vec<_>>();
1936
1937 assert!(
1938 elems_without_about.is_empty(),
1939 "\n{}",
1940 elems_without_about.join("\n")
1941 );
1942 }
1943
1944 #[test]
1945 fn invalid_arg_matchers_are_using_valid_commands_and_args_names() {
1946 fn collect_positional_args(
1947 path: &mut Vec<String>,
1948 positional_args_by_cmd: &mut BTreeMap<String, BTreeSet<String>>,
1949 command: &Command,
1950 ) {
1951 path.push(command.get_name().to_string());
1952 let key = path.iter().join(" ");
1953
1954 positional_args_by_cmd.insert(
1955 key,
1956 command
1957 .get_arguments()
1958 .filter(|arg| arg.is_positional())
1959 .map(|arg| arg.get_id().to_string())
1960 .collect(),
1961 );
1962
1963 for subcommand in command.get_subcommands() {
1964 collect_positional_args(path, positional_args_by_cmd, subcommand);
1965 }
1966
1967 path.pop();
1968 }
1969
1970 let mut path = vec![];
1971 let mut positional_args_by_cmd = BTreeMap::new();
1972
1973 collect_positional_args(
1974 &mut path,
1975 &mut positional_args_by_cmd,
1976 &GolemCliCommand::command(),
1977 );
1978
1979 let bad_matchers = GolemCliCommand::invalid_arg_matchers()
1980 .into_iter()
1981 .filter_map(|matcher| {
1982 let cmd_path = format!("golem-cli {}", matcher.subcommands.iter().join(" "));
1983
1984 let Some(args) = positional_args_by_cmd.get(&cmd_path) else {
1985 return Some(("command not found".to_string(), matcher));
1986 };
1987
1988 let missing_arg = [matcher.missing_positional_arg];
1989
1990 let bad_args = matcher
1991 .found_positional_args
1992 .iter()
1993 .chain(&missing_arg)
1994 .filter(|&&arg| !args.contains(arg))
1995 .collect::<Vec<_>>();
1996
1997 if !bad_args.is_empty() {
1998 return Some((
1999 format!("args not found: {}", bad_args.into_iter().join(", ")),
2000 matcher,
2001 ));
2002 }
2003
2004 None
2005 })
2006 .collect::<Vec<_>>();
2007
2008 assert!(
2009 bad_matchers.is_empty(),
2010 "\n{}",
2011 bad_matchers
2012 .into_iter()
2013 .map(|(error, matcher)| format!("error: {}\nmatcher: {:?}\n", error, matcher))
2014 .join("\n")
2015 )
2016 }
2017
2018 #[test]
2019 fn no_overlapping_flags() {
2020 fn collect_flags(
2021 path: &mut Vec<String>,
2022 flags_by_cmd_path: &mut BTreeMap<String, Vec<String>>,
2023 global_flags: &mut Vec<String>,
2024 command: &Command,
2025 ) {
2026 path.push(command.get_name().to_string());
2027 let key = path.iter().join(" ");
2028
2029 let mut cmd_flag_names = Vec::<String>::new();
2030 for arg in command.get_arguments() {
2031 let mut arg_flag_names = Vec::<String>::new();
2032 if arg.is_positional() {
2033 continue;
2034 }
2035
2036 arg_flag_names.extend(
2037 arg.get_long_and_visible_aliases()
2038 .into_iter()
2039 .flatten()
2040 .map(|s| s.to_string()),
2041 );
2042 arg_flag_names.extend(
2043 arg.get_short_and_visible_aliases()
2044 .into_iter()
2045 .flatten()
2046 .map(|s| s.to_string()),
2047 );
2048
2049 if arg.is_global_set() {
2050 global_flags.extend(arg_flag_names);
2051 } else {
2052 cmd_flag_names.extend(arg_flag_names);
2053 }
2054 }
2055
2056 flags_by_cmd_path.insert(key, cmd_flag_names);
2057
2058 for subcommand in command.get_subcommands() {
2059 collect_flags(path, flags_by_cmd_path, global_flags, subcommand);
2060 }
2061
2062 path.pop();
2063 }
2064
2065 let mut path = vec![];
2066 let mut flags_by_cmd_path = BTreeMap::<String, Vec<String>>::new();
2067 let mut global_flags = Vec::<String>::new();
2068 collect_flags(
2069 &mut path,
2070 &mut flags_by_cmd_path,
2071 &mut global_flags,
2072 &GolemCliCommand::command(),
2073 );
2074
2075 let commands_with_conflicting_flags = flags_by_cmd_path
2076 .into_iter()
2077 .map(|(path, flags)| {
2078 (
2079 path,
2080 flags
2081 .into_iter()
2082 .chain(global_flags.iter().cloned())
2083 .counts()
2084 .into_iter()
2085 .filter(|(_, count)| *count > 1)
2086 .collect::<Vec<_>>(),
2087 )
2088 })
2089 .filter(|(_, flags)| !flags.is_empty())
2090 .collect::<Vec<_>>();
2091
2092 assert!(
2093 commands_with_conflicting_flags.is_empty(),
2094 "\n{}",
2095 commands_with_conflicting_flags
2096 .iter()
2097 .map(|e| format!("{:?}", e))
2098 .join("\n")
2099 );
2100 }
2101
2102 #[test]
2103 fn builtin_app_subcommands_no_panic() {
2104 println!("{:?}", builtin_app_subcommands())
2105 }
2106
2107 #[test]
2108 fn help_targets_to_subcommands_uses_valid_subcommands() {
2109 for target in ShowClapHelpTarget::iter() {
2110 let command = GolemCliCommand::command();
2111 let mut command = &command;
2112 let subcommands = help_target_to_subcommand_names(target);
2113 for subcommand in &subcommands {
2114 match command.find_subcommand(subcommand) {
2115 Some(subcommand) => command = subcommand,
2116 None => {
2117 panic!(
2118 "Invalid help target: {}, {:?}, {}",
2119 target, subcommands, subcommand
2120 );
2121 }
2122 }
2123 }
2124 }
2125 }
2126}