use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::commands::{self, NewTemplate};
use crate::error::CliResult;
use crate::naming::{Names, Transport};
const PROJECT_TAGLINE: &str = "Scalable Rust backend apps with native performance.";
const AFTER_HELP: &str = concat!(
"Documentation: ",
env!("CARGO_PKG_HOMEPAGE"),
"/cli/\n",
"Repository: ",
env!("CARGO_PKG_REPOSITORY"),
);
pub fn print_version() {
println!("NestRS {}", env!("CARGO_PKG_VERSION"));
}
pub fn print_about() {
println!("NestRS");
println!("Version: {}", env!("CARGO_PKG_VERSION"));
println!("Tagline: {PROJECT_TAGLINE}");
println!("Documentation: {}/cli/", env!("CARGO_PKG_HOMEPAGE"));
println!("Repository: {}", env!("CARGO_PKG_REPOSITORY"));
println!("License: {}", env!("CARGO_PKG_LICENSE"));
println!("Authors: Yoann Vanitou");
}
#[derive(Parser, Debug)]
#[command(
name = "nestrs",
about = PROJECT_TAGLINE,
long_about = "Scalable Rust backend apps with native performance.\n\n\
Scaffolds NestRS projects, features, transport adapters, and toolchain checks.",
disable_version_flag = true,
after_help = AFTER_HELP,
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
New {
name: String,
#[arg(long)]
standalone: bool,
#[arg(long, short = 'o', default_value = ".")]
output: PathBuf,
#[arg(long, value_enum)]
template: Option<NewTemplate>,
#[arg(long)]
check: bool,
#[arg(long)]
dry_run: bool,
},
Doctor {
#[arg(long, short = 'p')]
path: Option<PathBuf>,
},
Version,
About,
Update {
#[arg(long)]
from_path: bool,
#[arg(long, requires = "from_path")]
workspace: Option<PathBuf>,
},
#[command(subcommand, visible_aliases = ["g"])]
Generate(GenerateCommand),
}
#[derive(Args, Debug)]
pub struct GenTarget {
pub name: String,
#[arg(long, short = 'p')]
pub path: Option<PathBuf>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Subcommand, Debug)]
pub enum GenerateCommand {
Feature(GenTarget),
Resource(GenTarget),
Http(GenTarget),
Graphql(GenTarget),
Ws(GenTarget),
Queue(GenTarget),
Schedule(GenTarget),
Mcp(GenTarget),
}
pub fn run(cli: Cli) -> CliResult<()> {
match cli.command {
Command::New {
name,
standalone,
output,
template,
check,
dry_run,
} => {
let names = Names::parse(&name);
let opts = commands::NewOptions {
name,
output: output.clone(),
template,
standalone,
dry_run,
};
commands::run_new(opts.clone())?;
if check && !dry_run {
run_check(&opts, &names, standalone, &output)?;
}
Ok(())
}
Command::Doctor { path } => {
commands::run_doctor(commands::DoctorOptions { path })?;
Ok(())
}
Command::Version => {
print_version();
Ok(())
}
Command::About => {
print_about();
Ok(())
}
Command::Update {
from_path,
workspace,
} => commands::run_update(commands::UpdateOptions {
from_path,
path: workspace,
}),
Command::Generate(cmd) => run_generate(cmd),
}
}
fn run_generate(cmd: GenerateCommand) -> CliResult<()> {
use GenerateCommand::*;
match cmd {
Feature(t) => commands::run_feature(commands::FeatureOptions {
name: t.name,
path: t.path,
dry_run: t.dry_run,
}),
Resource(t) => commands::run_resource(commands::ResourceOptions {
name: t.name,
path: t.path,
dry_run: t.dry_run,
}),
Http(t) => adapter(Transport::Http, t),
Graphql(t) => adapter(Transport::Graphql, t),
Ws(t) => adapter(Transport::Ws, t),
Queue(t) => adapter(Transport::Queue, t),
Schedule(t) => adapter(Transport::Schedule, t),
Mcp(t) => adapter(Transport::Mcp, t),
}
}
fn adapter(transport: Transport, t: GenTarget) -> CliResult<()> {
commands::run_adapter(
transport,
commands::AdapterOptions {
name: t.name,
path: t.path,
dry_run: t.dry_run,
},
)
}
fn run_check(
opts: &commands::NewOptions,
names: &Names,
standalone: bool,
output: &std::path::Path,
) -> CliResult<()> {
let project = commands::project_dir_for_check(opts, names)?;
let ws_root = crate::context::NestrsWorkspace::discover(output)?;
if let Some(ws) = ws_root.filter(|_| !standalone) {
let status = std::process::Command::new("cargo")
.args(["check", "-p", &names.kebab])
.current_dir(&ws.root)
.status()
.map_err(crate::error::CliError::Io)?;
if !status.success() {
return Err(crate::error::CliError::Anyhow(anyhow::anyhow!(
"cargo check -p {} failed",
names.kebab
)));
}
} else {
commands::run_cargo_check(&project)?;
}
println!("cargo check passed.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn after_help_includes_docs_and_repo() {
assert!(AFTER_HELP.contains("/cli/"));
assert!(AFTER_HELP.contains("github.com"));
}
}