use crate::prelude::internal::*;
#[derive(Args)]
pub struct GtInitCommand {
#[arg(default_value = ".")]
path: GtpCwdRelativeOrAbsoluteStringPath,
}
pub fn init_command(args: &GtInitCommand) -> Result<()> {
let mut config = GtpConfig::default();
let name = configure_name(&mut config)?;
config.version = Some(Version::parse("0.1.0").into_diagnostic()?);
configure_targets(&mut config, &name)?;
let base_path: GtpCwdRelativePath = (&args.path).try_into()?;
let backend = GtbSystem::new(&args.path).wrap_err("Failed to create system backend")?;
let config_path: GtpConfigFilePath = base_path.join_str(GTCONFIG_FILE).into();
block_on(backend.save_config(&config_path, &config))?;
let src_path = base_path.join_relative_path(config.src.relative_path());
create_dir_all(src_path.as_str())
.map_err(|_| GtCliError::FailedCreateDir(src_path.as_str().into()))?;
for (file, content) in GUIDE_FILES {
write(src_path.join_str(file).as_str(), content)
.map_err(|_| GtCliError::FailedWrite(src_path.join_str(file).as_str().into()))?;
}
println!(
"{generated} project at {path:?}, run `{command}` to build the project",
generated = "Generated".green().bold(),
path = base_path.as_str(),
command = "gt build".yellow().bold()
);
Ok(())
}
const GUIDE_FILES: &[(&str, &str)] = &[
(
"guide.type",
include_str!("../../examples/guide/guide.type"),
),
(
"module.type",
include_str!("../../examples/guide/module.type"),
),
];
fn configure_name(config: &mut GtpConfig) -> Result<String> {
let cd_name = std::env::current_dir()
.map(|path| path.file_name().unwrap().to_string_lossy().to_string())
.unwrap_or_default();
let name = Text::new("Name your project:")
.with_default(&cd_name)
.with_validator(required!("Please provide the project name"))
.with_validator(min_length!(1, "Please provide the project name"))
.prompt()
.map_err(|_| GtCliError::FailedReadline("project name"))?;
config.name = Some(name.clone());
Ok(name)
}
fn configure_targets(config: &mut GtpConfig, name: &str) -> Result<()> {
let targets = MultiSelect::new(
"Choose the languages you want to target:",
Target::VARIANTS.to_vec(),
)
.with_validator(|targets: &[ListOption<&Target>]| {
Ok(if targets.is_empty() {
Validation::Invalid("Please select at least one language".into())
} else {
Validation::Valid
})
})
.prompt()
.map_err(|_| GtCliError::FailedReadline("targets"))?;
for target in targets {
match target {
Target::TypeScript => configure_ts(config, name)?,
Target::Python => configure_py(config, name)?,
Target::Rust => configure_rs(config, name)?,
}
}
Ok(())
}
fn configure_ts(config: &mut GtpConfig, name: &str) -> Result<()> {
let mut ts = TsConfig::default();
ts.common.enabled = true;
let default_name = name.to_kebab_case();
let name = Text::new("Name the TypeScript package:")
.with_default(&default_name)
.with_validator(required!("Please provide the TypeScript package name"))
.with_validator(min_length!(1, "Please provide the TypeScript package name"))
.with_validator(|name: &str| {
let re = Regex::new(r"^(@[a-z0-9][a-z0-9\-_\.]*\/)?[a-z0-9][a-z0-9\-_\.]*$")?;
Ok(if re.is_match(name) {
Validation::Valid
} else {
Validation::Invalid("Invalid package name".into())
})
})
.prompt()
.map_err(|_| GtCliError::FailedReadline("TypeScript package name"))?;
let manifest =
toml::map::Map::from_iter(vec![("name".into(), toml::Value::String(name.clone()))]);
ts.common.manifest = manifest;
config.ts = ts;
Ok(())
}
fn configure_py(config: &mut GtpConfig, name: &str) -> Result<()> {
let mut py = PyConfig::default();
py.common.enabled = true;
let default_name = name.to_kebab_case();
let name = Text::new("Name the Python package:")
.with_default(&default_name)
.with_validator(required!("Please provide the Python package name"))
.with_validator(min_length!(1, "Please provide the Python package name"))
.with_validator(|name: &str| {
let re = Regex::new(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")?;
Ok(if re.is_match(name) {
Validation::Valid
} else {
Validation::Invalid("Invalid package name".into())
})
})
.prompt()
.map_err(|_| GtCliError::FailedReadline("Python package name"))?;
py.lang.manager = PyPackageManager::Uv;
let project = toml::map::Map::from_iter(vec![("name".into(), toml::Value::String(name))]);
let manifest = toml::map::Map::from_iter(vec![("project".into(), toml::Value::Table(project))]);
py.common.manifest = manifest;
config.py = py;
Ok(())
}
fn configure_rs(config: &mut GtpConfig, name: &str) -> Result<()> {
let mut rs = RsConfig::default();
rs.common.enabled = true;
let default_name = name.to_snake_case();
let name = Text::new("Name the Rust crate:")
.with_default(&default_name)
.with_validator(required!("Please provide the Rust crate name"))
.with_validator(min_length!(1, "Please provide the Rust crate name"))
.with_validator(|name: &str| {
let re = Regex::new(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")?;
Ok(if re.is_match(name) {
Validation::Valid
} else {
Validation::Invalid("Invalid crate name".into())
})
})
.prompt()
.map_err(|_| GtCliError::FailedReadline("Rust package name"))?;
let package = toml::map::Map::from_iter(vec![
("name".into(), toml::Value::String(name.clone())),
(
"edition".into(),
toml::Value::String(RsConfigLang::DEFAULT_EDITION.into()),
),
]);
let manifest = toml::map::Map::from_iter(vec![("package".into(), toml::Value::Table(package))]);
rs.common.manifest = manifest;
config.rs = rs;
Ok(())
}
#[derive(Debug, Clone, Copy)]
enum Target {
TypeScript,
Python,
Rust,
}
impl Target {
const VARIANTS: &'static [Target] = &[Self::TypeScript, Self::Python, Self::Rust];
}
impl Display for Target {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{self:?}")
}
}