mod choice;
mod generate;
mod git;
mod input;
mod manifest;
mod name;
mod options;
mod plan;
mod publish;
#[cfg(test)]
mod temp_dir;
mod wizard;
use std::path::{Path, PathBuf};
use clap::Args;
use console::style;
use self::{
git::GitInit,
input::{ChoiceArgs, Input},
name::PackageName,
options::DatabaseSetup,
};
use crate::common::prompt;
#[derive(Args)]
pub struct NewCommand {
path: Option<PathBuf>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
no_interactive: bool,
#[arg(long)]
no_git: bool,
#[command(flatten)]
choices: ChoiceArgs,
}
impl NewCommand {
pub fn run(self) {
if let Err(error) = self.run_inner() {
if error != prompt::CANCELLED {
cliclack::outro_cancel(error).ok();
}
std::process::exit(1);
}
}
fn run_inner(self) -> Result<(), String> {
let Self {
path,
name,
no_interactive,
no_git,
choices,
} = self;
let interactive = choices.preset().is_none() && !no_interactive && prompt::is_interactive();
let named = name.as_deref().map(PackageName::new).transpose()?;
let path = match path {
Some(path) => {
check_destination(&path, named.is_some())?;
path
}
None if interactive => wizard::destination(named.is_some())?,
None => {
return Err("missing destination: pass the directory to create".to_string());
}
};
let package = match &named {
Some(name) => name.clone(),
None => PackageName::from_path(&path)?,
};
let mut input = Input::new(choices);
if interactive {
wizard::ask(&mut input)?;
}
let resolution = input.resolve()?;
let plan = generate::generate(&package, &resolution.options)?;
publish::publish(&plan, &path)?;
let created = if path == Path::new(package.as_str()) {
format!("Created {}", style(&package).bold())
} else {
format!("Created {} in {}", style(&package).bold(), path.display())
};
cliclack::log::success(created).ok();
for note in &resolution.notes {
cliclack::log::info(note).ok();
}
if !no_git {
match git::init(&path) {
Ok(GitInit::Created) => cliclack::log::step("Initialized a Git repository"),
Ok(GitInit::Existing) => {
cliclack::log::step("Added to the existing Git repository")
}
Err(error) => cliclack::log::warning(format!(
"Failed to initialize a Git repository: {error}"
)),
}
.ok();
}
let quoted = shell_quote(&path);
let mut steps = vec![format!("cd {quoted}")];
if let DatabaseSetup::Toasty { .. } = resolution.options.database
&& resolution.options.example
{
steps.push("cargo run -- toasty migration generate".to_string());
steps.push("cargo run -- toasty migration apply".to_string());
}
steps.push("topcoat dev".to_string());
cliclack::outro_note("Next steps", steps.join("\n")).ok();
Ok(())
}
}
fn check_destination(path: &Path, named: bool) -> Result<(), String> {
if path.as_os_str().is_empty() {
return Err("the destination cannot be empty".to_string());
}
if !named {
PackageName::from_path(path)?;
}
if path.exists() {
return Err(format!("{} already exists", path.display()));
}
Ok(())
}
fn shell_quote(path: &Path) -> String {
let path = path.to_string_lossy();
let plain = !path.is_empty()
&& path
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"_-./".contains(&byte));
if plain {
path.into_owned()
} else {
format!("'{}'", path.replace('\'', r"'\''"))
}
}