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