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 path_clean::PathClean;
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}