diff --git a/Cargo.lock b/Cargo.lock index 2f245d2d..d575510e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -552,6 +552,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", + "unicode-width", "windows-sys 0.59.0", ] @@ -693,6 +694,7 @@ dependencies = [ "cot-cli", "cot_codegen", "darling 0.21.0", + "dialoguer", "glob", "heck", "hex", @@ -966,6 +968,19 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "difflib" version = "0.4.0" @@ -2986,6 +3001,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -3865,6 +3886,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index 0d55f2d9..7b34fea9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ darling = "0.21" deadpool-redis = { version = "0.21", default-features = false } derive_builder = "0.20" derive_more = "2" +dialoguer = "0.11.0" digest = "0.10" email_address = "0.2.9" fake = "4" diff --git a/cot-cli/Cargo.toml b/cot-cli/Cargo.toml index 56d60a36..9d9b718c 100644 --- a/cot-cli/Cargo.toml +++ b/cot-cli/Cargo.toml @@ -31,6 +31,7 @@ clap-verbosity-flag = { workspace = true, features = ["tracing"] } darling.workspace = true cot.workspace = true cot_codegen = { workspace = true, features = ["symbol-resolver"] } +dialoguer.workspace = true glob.workspace = true heck.workspace = true hex.workspace = true diff --git a/cot-cli/src/args.rs b/cot-cli/src/args.rs index 9b09f4f6..0a703913 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -40,6 +40,9 @@ pub struct ProjectNewArgs { pub name: Option, #[command(flatten)] pub source: CotSourceArgs, + /// Skip interactive prompts and use defaults + #[arg(short, long)] + pub yes: bool, } #[derive(Debug, Subcommand)] diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index 07071553..bf429f6c 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -2,16 +2,48 @@ use std::path::PathBuf; use anyhow::Context; use clap::CommandFactory; +use dialoguer::Confirm; use crate::args::{ Cli, CompletionsArgs, ManpagesArgs, MigrationListArgs, MigrationMakeArgs, ProjectNewArgs, }; use crate::migration_generator::{MigrationGeneratorOptions, list_migrations, make_migrations}; use crate::new_project::{CotSource, new_project}; +use crate::utils::{StatusType, print_status_msg}; pub fn handle_new_project( - ProjectNewArgs { path, name, source }: ProjectNewArgs, + ProjectNewArgs { + path, + name, + source, + yes, + }: ProjectNewArgs, ) -> anyhow::Result<()> { + let path = path + .canonicalize() + .with_context(|| format!("unable to canonicalize path: {}", path.display()))?; + + if path.exists() && path.join("Cargo.toml").exists() { + if !yes { + let confirmed = Confirm::new() + .with_prompt(format!( + "Directory '{}' is not empty. Overwrite?", + path.display() + )) + .default(false) + .interact()?; + if !confirmed { + print_status_msg(StatusType::Error, "Aborted: directory not empty"); + std::process::exit(0); + } + } else { + print_status_msg( + StatusType::Warning, + &format!("Overwriting existing files in `{}`", path.display()), + ); + } + } + let project_name = match name { None => { let dir_name = path @@ -29,6 +61,10 @@ pub fn handle_new_project( } else { CotSource::PublishedCrate }; + print_status_msg( + StatusType::Creating, + &format!("Creating project '{}' in: {}", project_name, path.display()), + ); new_project(&path, &project_name, &cot_source).with_context(|| "unable to create project") } @@ -96,6 +132,7 @@ mod tests { use_git: false, cot_path: None, }, + yes: false, }; let result = handle_new_project(args); diff --git a/cot-cli/src/new_project.rs b/cot-cli/src/new_project.rs index 1241bc15..9cf75252 100644 --- a/cot-cli/src/new_project.rs +++ b/cot-cli/src/new_project.rs @@ -1,5 +1,6 @@ use std::path::Path; +use anyhow::Context; use heck::ToPascalCase; use rand::rngs::StdRng; use rand::{RngCore, SeedableRng}; @@ -60,8 +61,9 @@ pub fn new_project( &format!("Cot project `{project_name}`"), ); - if path.exists() { - anyhow::bail!("destination `{}` already exists", path.display()); + if !path.exists() { + std::fs::create_dir_all(path) + .with_context(|| format!("failed to create directory `{}`", path.display()))?; } let project_struct_name = format!("{}Project", project_name.to_pascal_case()); @@ -69,22 +71,28 @@ pub fn new_project( let cot_source = cot_source.as_cargo_toml_source(); let dev_secret_key = generate_secret_key(); - for (file_name, content) in PROJECT_FILES { + for (template_name, content) in PROJECT_FILES { // Cargo reads and parses all files that are named "Cargo.toml" in a repository, // so we need a different name so that it doesn't fail on build. - let file_name = file_name.replace(".template", ""); - - let file_path = path.join(file_name); - trace!("Writing file: {:?}", file_path); + let file_name = template_name.replace(".template", ""); + let file_path = path.join(&file_name); + + if file_path.exists() { + print_status_msg( + StatusType::Warning, + &format!("Overwriting `{}`", file_path.display()), + ); + } else { + trace!("Writing file: {:?}", file_path); + } - std::fs::create_dir_all( - file_path - .parent() - .expect("joined path should always have a parent"), - )?; + if let Some(dir) = file_path.parent() { + std::fs::create_dir_all(dir) + .with_context(|| format!("failed to create directory `{}`", dir.display()))?; + } std::fs::write( - file_path, + &file_path, content .replace("{{ project_name }}", project_name) .replace("{{ project_struct_name }}", &project_struct_name) diff --git a/cot-cli/tests/new_project.rs b/cot-cli/tests/new_project.rs index 9d272491..266b5560 100644 --- a/cot-cli/tests/new_project.rs +++ b/cot-cli/tests/new_project.rs @@ -18,6 +18,7 @@ fn new_project_compile_test() { &project_path, "my_project", &CotSource::Path(&cot_workspace_path), + false, ) .unwrap();