use crate::client;
use crate::token::{get_smb_token::get_smb_token, is_logged_in::is_logged_in};
use crate::{
account::login::process_login,
cli::CommandResult,
project::deploy_target::merge_project_with_frontend_app,
ui::{fail_message, succeed_message, succeed_symbol},
};
use anyhow::{anyhow, Result};
use console::style;
use dialoguer::console::Term;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
use smbcloud_model::frontend_app::{
AppType, DeployRepoCreate, FrontendApp, FrontendAppCreate, RepoKind,
};
use smbcloud_model::project::{DeploymentMethod, Project, ProjectCreate};
use smbcloud_model::runner::Runner;
use smbcloud_network::environment::Environment;
use smbcloud_networking_project::{
crud_deploy_repo_create::create_deploy_repo, crud_frontend_app_create::create_frontend_app,
crud_project_create::create_project,
};
use smbcloud_utils::config::Config as DeployConfig;
use spinners::Spinner;
struct RepoInput {
repository: String,
repo_kind: RepoKind,
runner: Runner,
}
struct AppInput {
name: String,
source_path: Option<String>,
runner: Runner,
}
pub async fn process_project_init(
env: Environment,
should_init_project: bool,
) -> Result<CommandResult> {
let is_logged_in = is_logged_in(env).await?;
if !is_logged_in {
let _ = process_login(env, Some(is_logged_in)).await;
}
let project_name = match Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Project name")
.interact()
{
Ok(project_name) => project_name,
Err(_) => {
return Err(anyhow!(fail_message("Invalid project name.")));
}
};
let description = match Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Description")
.interact()
{
Ok(description) => description,
Err(_) => {
return Err(anyhow!(fail_message("Invalid description")));
}
};
let repo_choices = [
"Single-app repo — one deployable app",
"Monorepo — several apps in one repo",
"Skip — add a repo later",
];
let repo_choice = match Select::with_theme(&ColorfulTheme::default())
.with_prompt("Add a repo to this project?")
.items(&repo_choices)
.default(0)
.interact_on_opt(&Term::stderr())
{
Ok(Some(index)) => index,
_ => {
return Err(anyhow!(fail_message("Invalid selection.")));
}
};
let (repo, apps) = match repo_choice {
0 => {
let repository = prompt_repository_name(&project_name)?;
let runner = prompt_runner()?;
(
Some(RepoInput {
repository,
repo_kind: RepoKind::SingleApp,
runner,
}),
Vec::new(),
)
}
1 => {
let repository = prompt_repository_name(&project_name)?;
let mut apps = Vec::new();
loop {
apps.push(prompt_monorepo_app()?);
let add_another = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Add another app?")
.default(false)
.interact()
.unwrap_or(false);
if !add_another {
break;
}
}
(
Some(RepoInput {
repository,
repo_kind: RepoKind::Monorepo,
runner: Runner::Monorepo,
}),
apps,
)
}
_ => (None, Vec::new()),
};
let spinner = Spinner::new(
spinners::Spinners::SimpleDotsScrolling,
style("Creating a project...").green().bold().to_string(),
);
let access_token = get_smb_token(env)?;
let project = match create_project(
env,
client(),
access_token.clone(),
ProjectCreate {
name: project_name.clone(),
description: description.clone(),
},
)
.await
{
Ok(project) => project,
Err(e) => {
println!("Error: {e:#?}");
return Err(anyhow!(fail_message("Failed to create project.")));
}
};
let mut first_app: Option<FrontendApp> = None;
let mut created_apps = 0;
if let Some(repo) = &repo {
let deploy_repo = match create_deploy_repo(
env,
client(),
access_token.clone(),
DeployRepoCreate {
project_id: project.id,
name: repo.repository.clone(),
repository: repo.repository.clone(),
repo_kind: repo.repo_kind,
runner: repo.runner,
deployment_method: deployment_method_for(repo.runner),
},
)
.await
{
Ok(deploy_repo) => deploy_repo,
Err(e) => {
println!("Error: {e:#?}");
return Err(anyhow!(fail_message(
"Project created, but failed to create its repo."
)));
}
};
if let Some(embedded_apps) = &deploy_repo.frontend_apps {
if let Some(embedded_app) = embedded_apps.first() {
created_apps += 1;
first_app = Some(embedded_app.clone());
}
}
for app in &apps {
match create_frontend_app(
env,
client(),
access_token.clone(),
FrontendAppCreate {
name: app.name.clone(),
project_id: project.id,
app_type: AppType::Web,
runner: app.runner,
deployment_method: deployment_method_for(app.runner),
repository: Some(repo.repository.clone()),
description: None,
deploy_repo_id: Some(deploy_repo.id),
source_path: app.source_path.clone(),
},
)
.await
{
Ok(frontend_app) => {
created_apps += 1;
if first_app.is_none() {
first_app = Some(frontend_app);
}
}
Err(e) => {
println!("Error: {e:#?}");
return Err(anyhow!(fail_message(&format!(
"Project created, but failed to create app {}.",
app.name
))));
}
}
}
}
if should_init_project {
write_smb_config(&project, first_app.as_ref())?;
}
let msg = match &repo {
Some(repo) if repo.repo_kind == RepoKind::Monorepo => format!(
"{project_name} has been created with monorepo {} and {created_apps} app(s).",
repo.repository
),
Some(repo) => {
let app_name = first_app
.as_ref()
.map(|frontend_app| frontend_app.name.clone())
.unwrap_or_else(|| repo.repository.clone());
format!(
"{project_name} has been created with repo {} and app {app_name}.",
repo.repository
)
}
None => format!("{project_name} has been created."),
};
Ok(CommandResult {
spinner,
symbol: succeed_symbol(),
msg: succeed_message(&msg),
})
}
fn prompt_repository_name(project_name: &str) -> Result<String> {
match Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Repository name")
.default(slugify(project_name))
.interact()
{
Ok(repository) => Ok(repository),
Err(_) => Err(anyhow!(fail_message("Invalid repository name."))),
}
}
fn prompt_runner() -> Result<Runner> {
let runners = vec![
Runner::NodeJs,
Runner::Static,
Runner::Ruby,
Runner::Swift,
Runner::Rust,
];
match Select::with_theme(&ColorfulTheme::default())
.with_prompt("Runner")
.items(&runners)
.default(0)
.interact_on_opt(&Term::stderr())
{
Ok(Some(index)) => Ok(runners[index]),
_ => Err(anyhow!(fail_message("Invalid runner."))),
}
}
fn prompt_monorepo_app() -> Result<AppInput> {
let name = match Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("App name")
.interact()
{
Ok(name) => name,
Err(_) => return Err(anyhow!(fail_message("Invalid app name."))),
};
let source_path = match Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Source path (relative to repo root)")
.default(format!("apps/{}", slugify(&name)))
.interact()
{
Ok(source_path) => source_path,
Err(_) => return Err(anyhow!(fail_message("Invalid source path."))),
};
let runner = prompt_runner()?;
Ok(AppInput {
name,
source_path: Some(source_path),
runner,
})
}
fn deployment_method_for(runner: Runner) -> DeploymentMethod {
match runner {
Runner::Static => DeploymentMethod::Rsync,
_ => DeploymentMethod::Git,
}
}
fn slugify(name: &str) -> String {
name.to_lowercase()
.replace(' ', "-")
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
fn write_smb_config(workspace_project: &Project, frontend_app: Option<&FrontendApp>) -> Result<()> {
let deploy_target = match frontend_app {
Some(frontend_app) => merge_project_with_frontend_app(workspace_project, frontend_app),
None => workspace_project.clone(),
};
let config = DeployConfig {
name: workspace_project.name.clone(),
description: workspace_project.description.clone(),
project: deploy_target,
projects: None,
};
std::fs::create_dir_all(".smb")?;
std::fs::write(".smb/config.toml", toml::to_string(&config)?)?;
Ok(())
}