1use std::{
4 env,
5 io::Cursor,
6 path::{Path, PathBuf},
7 str::FromStr,
8 time::Duration,
9};
10
11use anyhow::Context;
12use colored::Colorize;
13use dialoguer::{Confirm, Select, theme::ColorfulTheme};
14use futures::stream::TryStreamExt;
15use indexmap::IndexMap;
16use is_terminal::IsTerminal;
17use wasmer_backend_api::{
18 WasmerClient,
19 types::{AppTemplate, TemplateLanguage},
20};
21use wasmer_config::{app::AppConfigV1, package::PackageSource};
22
23use super::{deploy::CmdAppDeploy, util::login_user};
24use crate::{
25 commands::AsyncCliCommand,
26 config::WasmerEnv,
27 opts::ItemFormatOpts,
28 utils::{load_package_manifest, prompts::PackageCheckMode},
29};
30
31pub(crate) async fn write_app_config(
32 app_config: &AppConfigV1,
33 dir: Option<PathBuf>,
34) -> anyhow::Result<()> {
35 let raw_app_config = app_config.clone().to_yaml()?;
36
37 let app_dir = match dir {
38 Some(dir) => dir,
39 None => std::env::current_dir()?,
40 };
41
42 tokio::fs::create_dir_all(&app_dir).await?;
43
44 let app_config_path = app_dir.join(AppConfigV1::CANONICAL_FILE_NAME);
45 tokio::fs::write(&app_config_path, raw_app_config)
46 .await
47 .with_context(|| {
48 format!(
49 "could not write app config to '{}'",
50 app_config_path.display()
51 )
52 })
53}
54
55pub(crate) fn minimal_app_config(owner: &str, name: &str) -> AppConfigV1 {
56 AppConfigV1 {
57 name: Some(String::from(name)),
58 owner: Some(String::from(owner)),
59 package: PackageSource::Path(String::from(".")),
60 app_id: None,
61 domains: None,
62 env: IndexMap::new(),
63 cli_args: None,
64 capabilities: None,
65 scheduled_tasks: None,
66 volumes: None,
67 health_checks: None,
68 debug: None,
69 scaling: None,
70 locality: None,
71 redirect: None,
72 extra: IndexMap::new(),
73 jobs: None,
74 }
75}
76
77#[derive(clap::Parser, Debug)]
79pub struct CmdAppCreate {
80 #[clap(
86 long,
87 conflicts_with = "package",
88 conflicts_with = "use_local_manifest"
89 )]
90 pub template: Option<String>,
91
92 #[clap(
94 long,
95 conflicts_with = "template",
96 conflicts_with = "use_local_manifest"
97 )]
98 pub package: Option<String>,
99
100 #[clap(long, conflicts_with = "template", conflicts_with = "package")]
102 pub use_local_manifest: bool,
103
104 #[clap(long = "deploy")]
109 pub deploy_app: bool,
110
111 #[clap(long)]
113 pub no_validate: bool,
114
115 #[clap(long, default_value_t = !std::io::stdin().is_terminal())]
117 pub non_interactive: bool,
118
119 #[clap(long)]
121 pub offline: bool,
122
123 #[clap(long)]
125 pub owner: Option<String>,
126
127 #[clap(long = "name")]
129 pub app_name: Option<String>,
130
131 #[clap(long = "dir")]
133 pub app_dir_path: Option<PathBuf>,
134
135 #[clap(long)]
137 pub no_wait: bool,
138
139 #[clap(flatten)]
141 pub env: WasmerEnv,
142
143 #[clap(flatten)]
144 #[allow(missing_docs)]
145 pub fmt: ItemFormatOpts,
146
147 #[clap(long)]
149 pub new_package_name: Option<String>,
150
151 #[clap(long)]
153 pub quiet: bool,
154}
155
156impl CmdAppCreate {
157 #[inline]
158 fn get_app_config(&self, owner: &str, name: &str, package: &str) -> AppConfigV1 {
159 AppConfigV1 {
160 name: Some(String::from(name)),
161 owner: Some(String::from(owner)),
162 package: PackageSource::from_str(package).unwrap(),
163 app_id: None,
164 domains: None,
165 env: IndexMap::new(),
166 cli_args: None,
167 capabilities: None,
168 scheduled_tasks: None,
169 volumes: None,
170 health_checks: None,
171 debug: None,
172 scaling: None,
173 locality: None,
174 redirect: None,
175 extra: IndexMap::new(),
176 jobs: None,
177 }
178 }
179
180 async fn get_app_name(&self) -> anyhow::Result<String> {
181 if let Some(name) = &self.app_name {
182 return Ok(name.clone());
183 }
184
185 if self.non_interactive {
186 anyhow::bail!("No app name specified: use --name <app_name>");
188 }
189
190 let default_name = match &self.app_dir_path {
191 Some(path) => path
192 .file_name()
193 .and_then(|f| f.to_str())
194 .map(|s| s.to_owned()),
195 None => env::current_dir().ok().and_then(|dir| {
196 dir.file_name()
197 .and_then(|f| f.to_str())
198 .map(|s| s.to_owned())
199 }),
200 };
201
202 crate::utils::prompts::prompt_for_app_ident(
203 "What should be the name of the app?",
204 default_name.as_deref(),
205 )
206 }
207
208 async fn get_owner(&self, client: Option<&WasmerClient>) -> anyhow::Result<String> {
209 if let Some(owner) = &self.owner {
210 return Ok(owner.clone());
211 }
212
213 if self.non_interactive {
214 anyhow::bail!("No owner specified: use --owner <owner>");
216 }
217
218 let user = if let Some(client) = client {
219 Some(wasmer_backend_api::query::current_user_with_namespaces(client, None).await?)
220 } else {
221 None
222 };
223 crate::utils::prompts::prompt_for_namespace("Who should own this app?", None, user.as_ref())
224 }
225
226 async fn get_output_dir(&self, app_name: &str) -> anyhow::Result<PathBuf> {
227 let mut output_path = if let Some(path) = &self.app_dir_path {
228 path.clone()
229 } else {
230 PathBuf::from(".").canonicalize()?
231 };
232
233 if output_path.is_dir() && output_path.read_dir()?.next().is_some() {
234 if self.non_interactive {
235 if !self.quiet {
236 eprintln!("The current directory is not empty.");
237 eprintln!(
238 "Use the `--dir` flag to specify another directory, or remove files from the currently selected one."
239 )
240 }
241 anyhow::bail!("Stopping as the directory is not empty")
242 } else {
243 let theme = ColorfulTheme::default();
244 let raw: String = dialoguer::Input::with_theme(&theme)
245 .with_prompt("Select the directory to save the app in")
246 .with_initial_text(app_name)
247 .interact_text()
248 .context("could not read user input")?;
249 output_path = PathBuf::from_str(&raw)?
250 }
251 }
252
253 Ok(output_path)
254 }
255
256 async fn create_from_local_manifest(
257 &self,
258 owner: &str,
259 app_name: &str,
260 ) -> anyhow::Result<bool> {
261 if (!self.use_local_manifest && self.non_interactive)
262 || self.template.is_some()
263 || self.package.is_some()
264 {
265 return Ok(false);
266 }
267
268 let app_dir = match &self.app_dir_path {
269 Some(dir) => PathBuf::from(dir),
270 None => std::env::current_dir()?,
271 };
272
273 let (manifest_path, _) = if let Some(res) = load_package_manifest(&app_dir)? {
274 res
275 } else if self.use_local_manifest {
276 anyhow::bail!(
277 "The --use_local_manifest flag was passed, but path {} does not contain a valid package manifest.",
278 app_dir.display()
279 )
280 } else {
281 return Ok(false);
282 };
283
284 let ask_confirmation = || {
285 eprintln!(
286 "A package manifest was found in path {}.",
287 &manifest_path.display()
288 );
289 let theme = dialoguer::theme::ColorfulTheme::default();
290 Confirm::with_theme(&theme)
291 .with_prompt("Use it for the app?")
292 .interact()
293 };
294
295 if self.use_local_manifest || ask_confirmation()? {
296 let app_config = self.get_app_config(owner, app_name, ".");
297 write_app_config(&app_config, self.app_dir_path.clone()).await?;
298 self.try_deploy(owner, app_name, None, false, false).await?;
299 return Ok(true);
300 }
301
302 Ok(false)
303 }
304
305 async fn create_from_package(
306 &self,
307 client: Option<&WasmerClient>,
308 owner: &str,
309 app_name: &str,
310 ) -> anyhow::Result<bool> {
311 if self.template.is_some() {
312 return Ok(false);
313 }
314
315 let output_path = self.get_output_dir(app_name).await?;
316
317 if let Some(pkg) = &self.package {
318 let app_config = self.get_app_config(owner, app_name, pkg);
319 write_app_config(&app_config, Some(output_path.clone())).await?;
320 self.try_deploy(owner, app_name, Some(&output_path), false, false)
321 .await?;
322 return Ok(true);
323 } else if !self.non_interactive {
324 let (package_id, _) = crate::utils::prompts::prompt_for_package(
325 "Enter the name of the package",
326 Some("wasmer/hello"),
327 if client.is_some() {
328 Some(PackageCheckMode::MustExist)
329 } else {
330 None
331 },
332 client,
333 )
334 .await?;
335
336 let app_config = self.get_app_config(owner, app_name, &package_id.to_string());
337 write_app_config(&app_config, Some(output_path.clone())).await?;
338 self.try_deploy(owner, app_name, Some(&output_path), false, false)
339 .await?;
340 return Ok(true);
341 } else {
342 eprintln!(
343 "{}: the app creation process did not produce any local change.",
344 "Warning".bold().yellow()
345 );
346 }
347
348 Ok(false)
349 }
350
351 fn persist_in_cache<S: serde::Serialize>(path: &Path, data: &S) -> Result<(), anyhow::Error> {
352 if let Some(parent) = path.parent() {
353 std::fs::create_dir_all(parent).context("could not create cache dir")?;
354 }
355
356 let data = serde_json::to_vec(data)?;
357
358 std::fs::write(path, data)?;
359 tracing::trace!(path=%path.display(), "persisted app template cache");
360
361 Ok(())
362 }
363
364 async fn fetch_templates_cached(
368 client: &WasmerClient,
369 cache_dir: &Path,
370 language: &str,
371 ) -> Result<Vec<AppTemplate>, anyhow::Error> {
372 const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 60);
373 const MAX_COUNT: usize = 100;
374 let cache_filename = format!("app_templates_{language}.json");
375
376 let cache_path = cache_dir.join(cache_filename);
377
378 let cached_items = match Self::load_cached::<Vec<AppTemplate>>(&cache_path) {
379 Ok((items, age)) => {
380 if age <= MAX_CACHE_AGE {
381 return Ok(items);
382 }
383 items
384 }
385 Err(e) => {
386 tracing::trace!(error = &*e, "could not load templates from local cache");
387 Vec::new()
388 }
389 };
390
391 let stream = wasmer_backend_api::query::fetch_all_app_templates_from_language(
396 client,
397 10,
398 Some(wasmer_backend_api::types::AppTemplatesSortBy::Newest),
399 language.to_string(),
400 );
401
402 futures_util::pin_mut!(stream);
403
404 let first_page = match stream.try_next().await? {
405 Some(items) => items,
406 None => return Ok(Vec::new()),
407 };
408
409 if let (Some(a), Some(b)) = (cached_items.first(), first_page.first()) {
410 if a == b {
411 return Ok(cached_items);
413 }
414 }
415
416 let mut items = first_page;
417 while let Some(next) = stream.try_next().await? {
418 items.extend(next);
419
420 if items.len() >= MAX_COUNT {
421 break;
422 }
423 }
424
425 if let Err(err) = Self::persist_in_cache(&cache_path, &items) {
427 tracing::trace!(error = &*err, "could not persist template cache");
428 }
429
430 Ok(items)
435 }
436
437 fn load_cached<D: serde::de::DeserializeOwned>(
441 path: &Path,
442 ) -> Result<(D, std::time::Duration), anyhow::Error> {
443 let modified = path.metadata()?.modified()?;
444 let age = modified.elapsed()?;
445
446 let data = std::fs::read_to_string(path)?;
447 match serde_json::from_str::<D>(data.as_str()) {
448 Ok(v) => Ok((v, age)),
449 Err(err) => {
450 std::fs::remove_file(path).ok();
451 Err(err).context("could not deserialize cached file")
452 }
453 }
454 }
455
456 async fn fetch_template_languages_cached(
457 client: &WasmerClient,
458 cache_dir: &Path,
459 ) -> anyhow::Result<Vec<TemplateLanguage>> {
460 const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 60);
461 const MAX_COUNT: usize = 100;
462 const CACHE_FILENAME: &str = "app_languages.json";
463
464 let cache_path = cache_dir.join(CACHE_FILENAME);
465
466 let cached_items = match Self::load_cached::<Vec<TemplateLanguage>>(&cache_path) {
467 Ok((items, age)) => {
468 if age <= MAX_CACHE_AGE {
469 return Ok(items);
470 }
471 items
472 }
473 Err(e) => {
474 tracing::trace!(error = &*e, "could not load templates from local cache");
475 Vec::new()
476 }
477 };
478 let mut stream = Box::pin(wasmer_backend_api::query::fetch_all_app_template_languages(
483 client, None,
484 ));
485
486 let first_page = match stream.try_next().await? {
487 Some(items) => items,
488 None => return Ok(Vec::new()),
489 };
490
491 if let (Some(a), Some(b)) = (cached_items.first(), first_page.first()) {
492 if a == b {
493 return Ok(cached_items);
495 }
496 }
497
498 let mut items = first_page;
499 while let Some(next) = stream.try_next().await? {
500 items.extend(next);
501
502 if items.len() >= MAX_COUNT {
503 break;
504 }
505 }
506
507 if let Err(err) = Self::persist_in_cache(&cache_path, &items) {
509 tracing::trace!(error = &*err, "could not persist template cache");
510 }
511
512 Ok(items)
517 }
518
519 async fn get_template_url(&self, client: &WasmerClient) -> anyhow::Result<url::Url> {
521 let mut url = if let Some(template) = &self.template {
522 if let Ok(url) = url::Url::parse(template) {
523 url
524 } else if let Some(template) =
525 wasmer_backend_api::query::fetch_app_template_from_slug(client, template.clone())
526 .await?
527 {
528 url::Url::parse(&template.repo_url)?
529 } else {
530 anyhow::bail!("Template '{template}' not found in the registry")
531 }
532 } else {
533 if self.non_interactive {
534 anyhow::bail!("No template selected")
535 }
536
537 let theme = ColorfulTheme::default();
538 let registry = self
539 .env
540 .registry_public_url()?
541 .host_str()
542 .unwrap_or("unknown_registry")
543 .replace('.', "_");
544 let cache_dir = self.env.cache_dir().join("templates").join(registry);
545
546 let languages = Self::fetch_template_languages_cached(client, &cache_dir).await?;
547
548 let items = languages.iter().map(|t| t.name.clone()).collect::<Vec<_>>();
549
550 let dialog = dialoguer::Select::with_theme(&theme)
553 .with_prompt(format!("Select a language ({} available)", items.len()))
554 .items(&items)
555 .max_length(10)
556 .clear(true)
557 .report(true)
558 .default(0);
559
560 let selection = dialog.interact()?;
561
562 let selected_language = languages
563 .get(selection)
564 .ok_or(anyhow::anyhow!("Invalid selection!"))?;
565
566 let templates =
567 Self::fetch_templates_cached(client, &cache_dir, &selected_language.slug).await?;
568
569 let items = templates
570 .iter()
571 .map(|t| {
572 format!(
573 "{} - {} {}",
574 t.name.bold(),
575 "demo:".bold().dimmed(),
576 t.demo_url.dimmed()
577 )
578 })
579 .collect::<Vec<_>>();
580
581 let dialog = dialoguer::Select::with_theme(&theme)
582 .with_prompt(format!("Select a template ({} available)", items.len()))
583 .items(&items)
584 .max_length(10)
585 .clear(true)
586 .report(false)
587 .default(0);
588
589 let selection = dialog.interact()?;
590
591 let selected_template = templates
592 .get(selection)
593 .ok_or(anyhow::anyhow!("Invalid selection!"))?;
594
595 if !self.quiet {
596 eprintln!(
597 "{} {} {} {} - {} {}",
598 "✔".green().bold(),
599 "Selected template".bold(),
600 "·".dimmed(),
601 selected_template.name.green().bold(),
602 "demo url".dimmed().bold(),
603 selected_template.demo_url.dimmed()
604 )
605 }
606
607 url::Url::parse(&selected_template.repo_url)?
608 };
609
610 let url = if url.path().contains("archive/refs/heads") || url.path().contains("/zipball/") {
611 url
612 } else {
613 let old_path = url.path();
614 url.set_path(&format!("{old_path}/zipball/main"));
615 url
616 };
617
618 Ok(url)
619 }
620
621 async fn create_from_template(
622 &self,
623 client: Option<&WasmerClient>,
624 owner: &str,
625 app_name: &str,
626 ) -> anyhow::Result<bool> {
627 let client = match client {
628 Some(client) => client,
629 None => anyhow::bail!("Cannot"),
630 };
631
632 let url = self.get_template_url(client).await?;
633
634 tracing::info!("Downloading template from url {url}");
635
636 let output_path = self.get_output_dir(app_name).await?;
637 let pb = indicatif::ProgressBar::new_spinner();
638
639 pb.enable_steady_tick(std::time::Duration::from_millis(500));
640 pb.set_style(
641 indicatif::ProgressStyle::with_template("{spinner:.magenta} {msg}")
642 .unwrap()
643 .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷"]),
644 );
645
646 pb.set_message("Downloading package..");
647
648 let response = reqwest::get(url).await?;
649 let bytes = response.bytes().await?;
650 pb.set_message("Unpacking the template..");
651
652 let cursor = Cursor::new(bytes);
653 let mut archive = zip::ZipArchive::new(cursor)?;
654
655 for entry in 0..archive.len() {
657 let mut entry = archive
658 .by_index(entry)
659 .context(format!("Getting the archive entry #{entry}"))?;
660
661 let path = entry.mangled_name();
662
663 let path: PathBuf = {
664 let mut components = path.components();
665 components.next();
666 components.collect()
667 };
668
669 if path.to_str().unwrap_or_default().contains(".github") {
670 continue;
671 }
672
673 let path = output_path.join(path);
674
675 if let Some(parent) = path.parent() {
676 if !parent.exists() {
677 std::fs::create_dir_all(parent)?;
678 }
679 }
680
681 if !path.exists() {
682 if entry.is_file() {
684 let mut outfile = std::fs::OpenOptions::new()
685 .create(true)
686 .truncate(true)
687 .write(true)
688 .open(&path)?;
689 std::io::copy(&mut entry, &mut outfile)?;
690 } else {
691 std::fs::create_dir(path)?;
692 }
693 }
694 }
695 pb.set_style(
696 indicatif::ProgressStyle::with_template(&format!("{} {{msg}}", "✔".green().bold()))
697 .unwrap(),
698 );
699 pb.finish_with_message(format!("{}", "Unpacked template".bold()));
700
701 pb.finish();
702
703 let app_yaml_path = output_path.join(AppConfigV1::CANONICAL_FILE_NAME);
704
705 if app_yaml_path.exists() && app_yaml_path.is_file() {
706 let contents = tokio::fs::read_to_string(&app_yaml_path).await?;
707 let mut raw_yaml: serde_yaml::Value = serde_yaml::from_str(&contents)?;
708
709 if let serde_yaml::Value::Mapping(m) = &mut raw_yaml {
710 m.insert("name".into(), app_name.into());
711 m.insert("owner".into(), owner.into());
712 m.shift_remove("domains");
713 m.shift_remove("app_id");
714 };
715
716 let raw_app = serde_yaml::to_string(&raw_yaml)?;
717
718 AppConfigV1::parse_yaml(&raw_app)?;
720
721 tokio::fs::write(&app_yaml_path, raw_app).await?;
722 } else {
723 let app_config = minimal_app_config(owner, app_name);
724 write_app_config(&app_config, Some(output_path.clone())).await?;
725 }
726
727 let build_md_path = output_path.join("BUILD.md");
728 if build_md_path.exists() {
729 let contents = tokio::fs::read_to_string(build_md_path).await?;
730 eprintln!(
731 "{}: {}
732{}",
733 "NOTE".bold(),
734 "The selected template has a `BUILD.md` file.
735This means there are likely additional build
736steps that you need to perform before deploying
737the app:\n"
738 .bold(),
739 contents
740 );
741 let bin_name = match std::env::args().nth(0) {
742 Some(n) => n,
743 None => String::from("wasmer"),
744 };
745 eprintln!(
746 "After taking the necessary steps to build your application, re-run `{}`",
747 format!("{bin_name} deploy").bold()
748 )
749 } else {
750 self.try_deploy(owner, app_name, Some(&output_path), false, false)
751 .await?;
752 }
753
754 Ok(true)
755 }
756
757 async fn try_deploy(
758 &self,
759 owner: &str,
760 app_name: &str,
761 path: Option<&Path>,
762 build_remote: bool,
763 skip_prompt: bool,
764 ) -> anyhow::Result<()> {
765 let interactive = !self.non_interactive;
766 let theme = dialoguer::theme::ColorfulTheme::default();
767
768 let mut should_deploy = self.deploy_app;
769
770 if skip_prompt {
771 should_deploy = true;
772 } else if !should_deploy && interactive {
773 should_deploy = Confirm::with_theme(&theme)
774 .with_prompt("Do you want to deploy the app now?")
775 .interact()?;
776 }
777
778 if should_deploy {
779 let cmd_deploy = CmdAppDeploy {
780 quiet: false,
781 env: self.env.clone(),
782 fmt: ItemFormatOpts {
783 format: self.fmt.format,
784 },
785 no_validate: false,
786 non_interactive: self.non_interactive,
787 publish_package: !build_remote,
788 dir: self.app_dir_path.clone(),
789 path: path.map(|v| v.to_path_buf()),
790 no_wait: self.no_wait,
791 no_default: false,
792 no_persist_id: false,
793 owner: Some(String::from(owner)),
794 app_name: Some(app_name.into()),
795 bump: false,
796 build_remote,
797 template: None,
798 package: None,
799 use_local_manifest: self.use_local_manifest,
800 ensure_app_config: true,
801 };
802 cmd_deploy.run_async().await?;
803 }
804
805 Ok(())
806 }
807}
808
809#[async_trait::async_trait]
810impl AsyncCliCommand for CmdAppCreate {
811 type Output = ();
812
813 async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
814 let client = if self.offline {
815 None
816 } else {
817 Some(
818 login_user(
819 &self.env,
820 !self.non_interactive,
821 "retrieve informations about the owner of the app",
822 )
823 .await?,
824 )
825 };
826
827 let owner = self.get_owner(client.as_ref()).await?;
829
830 let app_name = self.get_app_name().await?;
832
833 if !self.create_from_local_manifest(&owner, &app_name).await? {
834 if self.template.is_some() {
835 self.create_from_template(client.as_ref(), &owner, &app_name)
836 .await?;
837 } else if self.package.is_some() {
838 self.create_from_package(client.as_ref(), &owner, &app_name)
839 .await?;
840 } else if !self.non_interactive {
841 if self.offline {
842 eprintln!("Creating app from a package name running in offline mode");
843 self.create_from_package(client.as_ref(), &owner, &app_name)
844 .await?;
845 } else {
846 let theme = ColorfulTheme::default();
847 let working_dir = if let Some(dir) = &self.app_dir_path {
848 dir.clone()
849 } else {
850 std::env::current_dir()?
851 };
852
853 let remote_option_available = working_dir.is_dir()
854 && std::fs::read_dir(&working_dir)?.next().is_some()
855 && !working_dir.join(AppConfigV1::CANONICAL_FILE_NAME).exists()
856 && load_package_manifest(&working_dir)?.is_none();
857
858 let mut items = Vec::new();
859 let mut remote_idx = None;
860 if remote_option_available {
861 remote_idx = Some(items.len());
862 items.push(String::from(
863 "Deploy the current directory with a remote build",
864 ));
865 }
866 let template_idx = items.len();
867 items.push(String::from("Start with a template"));
868 let package_idx = items.len();
869 items.push(String::from("Choose an existing package"));
870
871 let choice = Select::with_theme(&theme)
872 .with_prompt("What would you like to deploy?")
873 .items(&items)
874 .default(0)
875 .interact()?;
876
877 if remote_idx.is_some() && Some(choice) == remote_idx {
878 let app_config = minimal_app_config(owner.as_str(), app_name.as_str());
879 write_app_config(&app_config, Some(working_dir.clone())).await?;
880 self.try_deploy(
881 owner.as_str(),
882 app_name.as_str(),
883 Some(&working_dir),
884 true,
885 true,
886 )
887 .await?;
888 return Ok(());
889 } else if choice == template_idx {
890 self.create_from_template(client.as_ref(), &owner, &app_name)
891 .await?
892 } else if choice == package_idx {
893 self.create_from_package(client.as_ref(), &owner, &app_name)
894 .await?
895 } else {
896 panic!("unhandled selection {choice}");
897 };
898 }
899 } else {
900 eprintln!("Warning: the creation process did not produce any result.");
901 }
902 }
903
904 Ok(())
905 }
906}