#![warn(missing_docs)]
use reinhardt_admin_cli::migrate_v2;
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use colored::Colorize;
use reinhardt_commands::{
BaseCommand, CommandContext, CommandResult, PluginDisableCommand, PluginEnableCommand,
PluginInfoCommand, PluginInstallCommand, PluginListCommand, PluginRemoveCommand,
PluginSearchCommand, PluginUpdateCommand, StartAppCommand, StartProjectCommand,
};
use std::process;
#[derive(Clone, Debug, ValueEnum)]
enum TemplateType {
Rest,
Pages,
}
enum ResolvedProjectType {
Pages,
Rest,
}
fn resolve_project_type(
template: Option<TemplateType>,
with_pages: bool,
with_rest: bool,
) -> ResolvedProjectType {
match (template, with_pages, with_rest) {
(Some(TemplateType::Pages), _, _) | (_, true, _) => ResolvedProjectType::Pages,
(Some(TemplateType::Rest), _, _) | (_, _, true) => ResolvedProjectType::Rest,
_ => unreachable!(),
}
}
#[derive(Parser)]
#[command(name = "reinhardt-admin")]
#[command(about = "Reinhardt project administration utility", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, action = clap::ArgAction::Count)]
verbosity: u8,
}
#[derive(Subcommand)]
enum Commands {
#[command(group(
clap::ArgGroup::new("project_type")
.required(true)
.args(["template", "with_pages", "with_rest"])
))]
Startproject {
#[arg(value_name = "PROJECT_NAME")]
name: String,
#[arg(value_name = "DIRECTORY")]
directory: Option<String>,
#[arg(long, value_name = "TYPE", value_enum, group = "project_type")]
template: Option<TemplateType>,
#[arg(long, group = "project_type")]
with_pages: bool,
#[arg(long, group = "project_type")]
with_rest: bool,
#[arg(long, value_name = "DIR")]
template_dir: Option<String>,
},
#[command(group(
clap::ArgGroup::new("app_type")
.required(true)
.args(["template", "with_pages", "with_rest"])
))]
Startapp {
#[arg(value_name = "APP_NAME")]
name: String,
#[arg(value_name = "DIRECTORY")]
directory: Option<String>,
#[arg(long, value_name = "TYPE", value_enum, group = "app_type")]
template: Option<TemplateType>,
#[arg(long, group = "app_type")]
with_pages: bool,
#[arg(long, group = "app_type")]
with_rest: bool,
#[arg(long, value_name = "DIR")]
template_dir: Option<String>,
},
Plugin {
#[command(subcommand)]
subcommand: PluginCommands,
},
Fmt {
#[arg(value_name = "PATH")]
path: PathBuf,
#[arg(long)]
check: bool,
#[arg(long, default_value = "true", action = clap::ArgAction::Set)]
with_rustfmt: bool,
#[arg(long, value_name = "PATH")]
config_path: Option<PathBuf>,
#[arg(long, value_name = "EDITION")]
edition: Option<String>,
#[arg(long, value_name = "EDITION")]
style_edition: Option<String>,
#[arg(long, value_name = "OPTIONS")]
config: Option<String>,
#[arg(long, value_name = "WHEN", default_value = "auto")]
color: String,
#[arg(long)]
backup: bool,
},
FmtAll {
#[arg(long)]
check: bool,
#[arg(long, value_name = "PATH")]
config_path: Option<PathBuf>,
#[arg(long, value_name = "EDITION")]
edition: Option<String>,
#[arg(long, value_name = "EDITION")]
style_edition: Option<String>,
#[arg(long, value_name = "OPTIONS")]
config: Option<String>,
#[arg(long, value_name = "WHEN", default_value = "auto")]
color: String,
#[arg(long)]
backup: bool,
},
MigrateManoucheV2(migrate_v2::MigrateV2Args),
}
#[derive(Subcommand)]
enum PluginCommands {
List {
#[arg(short, long)]
verbose: bool,
#[arg(long)]
enabled: bool,
#[arg(long)]
disabled: bool,
#[arg(long)]
project_root: Option<String>,
},
Info {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
remote: bool,
#[arg(long)]
project_root: Option<String>,
},
Install {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
version: Option<String>,
#[arg(short, long)]
yes: bool,
#[arg(long)]
project_root: Option<String>,
},
Remove {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
purge: bool,
#[arg(short, long)]
yes: bool,
#[arg(long)]
project_root: Option<String>,
},
Enable {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
project_root: Option<String>,
},
Disable {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
project_root: Option<String>,
},
Search {
#[arg(value_name = "QUERY")]
query: String,
#[arg(long, default_value = "10")]
limit: u64,
},
Update {
#[arg(value_name = "NAME")]
name: Option<String>,
#[arg(long)]
all: bool,
#[arg(short, long)]
yes: bool,
#[arg(long)]
project_root: Option<String>,
},
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let result = match cli.command {
Commands::Startproject {
name,
directory,
template,
with_pages,
with_rest,
template_dir,
} => {
run_startproject(
name,
directory,
template,
with_pages,
with_rest,
template_dir,
cli.verbosity,
)
.await
}
Commands::Startapp {
name,
directory,
template,
with_pages,
with_rest,
template_dir,
} => {
run_startapp(
name,
directory,
template,
with_pages,
with_rest,
template_dir,
cli.verbosity,
)
.await
}
Commands::Plugin { subcommand } => run_plugin(subcommand, cli.verbosity).await,
Commands::Fmt {
path,
check,
with_rustfmt,
config_path,
edition,
style_edition,
config,
color,
backup,
} => run_fmt(
path,
check,
with_rustfmt,
config_path,
edition,
style_edition,
config,
color,
backup,
cli.verbosity,
),
Commands::FmtAll {
check,
config_path,
edition,
style_edition,
config,
color,
backup,
} => run_fmt_all(
check,
config_path,
edition,
style_edition,
config,
color,
backup,
cli.verbosity,
),
Commands::MigrateManoucheV2(args) => {
if let Err(e) = migrate_v2::run(args) {
eprintln!("{}", format!("error: {e}").red());
process::exit(1);
}
Ok(())
}
};
if let Err(e) = result {
eprintln!("Error: {}", e);
process::exit(1);
}
}
async fn run_startproject(
name: String,
directory: Option<String>,
template: Option<TemplateType>,
with_pages: bool,
with_rest: bool,
template_dir: Option<String>,
verbosity: u8,
) -> CommandResult<()> {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(name);
if let Some(dir) = directory {
ctx.add_arg(dir);
}
match resolve_project_type(template, with_pages, with_rest) {
ResolvedProjectType::Pages => ctx.set_option("with-pages".to_string(), "true".to_string()),
ResolvedProjectType::Rest => ctx.set_option("restful".to_string(), "true".to_string()),
}
if let Some(td) = template_dir {
ctx.set_option("template-dir".to_string(), td);
}
let cmd = StartProjectCommand;
cmd.execute(&ctx).await
}
async fn run_startapp(
name: String,
directory: Option<String>,
template: Option<TemplateType>,
with_pages: bool,
with_rest: bool,
template_dir: Option<String>,
verbosity: u8,
) -> CommandResult<()> {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(name);
if let Some(dir) = directory {
ctx.add_arg(dir);
}
match resolve_project_type(template, with_pages, with_rest) {
ResolvedProjectType::Pages => ctx.set_option("with-pages".to_string(), "true".to_string()),
ResolvedProjectType::Rest => ctx.set_option("restful".to_string(), "true".to_string()),
}
if let Some(td) = template_dir {
ctx.set_option("template-dir".to_string(), td);
}
let cmd = StartAppCommand;
cmd.execute(&ctx).await
}
async fn run_plugin(subcommand: PluginCommands, verbosity: u8) -> CommandResult<()> {
match subcommand {
PluginCommands::List {
verbose,
enabled,
disabled,
project_root,
} => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
if verbose {
ctx.set_option("verbose".to_string(), "true".to_string());
}
if enabled {
ctx.set_option("enabled".to_string(), "true".to_string());
}
if disabled {
ctx.set_option("disabled".to_string(), "true".to_string());
}
if let Some(root) = project_root {
ctx.set_option("project-root".to_string(), root);
}
PluginListCommand.execute(&ctx).await
}
PluginCommands::Info {
name,
remote,
project_root,
} => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(name);
if remote {
ctx.set_option("remote".to_string(), "true".to_string());
}
if let Some(root) = project_root {
ctx.set_option("project-root".to_string(), root);
}
PluginInfoCommand.execute(&ctx).await
}
PluginCommands::Install {
name,
version,
yes,
project_root,
} => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(name);
if let Some(v) = version {
ctx.set_option("version".to_string(), v);
}
if yes {
ctx.set_option("yes".to_string(), "true".to_string());
}
if let Some(root) = project_root {
ctx.set_option("project-root".to_string(), root);
}
PluginInstallCommand.execute(&ctx).await
}
PluginCommands::Remove {
name,
purge,
yes,
project_root,
} => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(name);
if purge {
ctx.set_option("purge".to_string(), "true".to_string());
}
if yes {
ctx.set_option("yes".to_string(), "true".to_string());
}
if let Some(root) = project_root {
ctx.set_option("project-root".to_string(), root);
}
PluginRemoveCommand.execute(&ctx).await
}
PluginCommands::Enable { name, project_root } => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(name);
if let Some(root) = project_root {
ctx.set_option("project-root".to_string(), root);
}
PluginEnableCommand.execute(&ctx).await
}
PluginCommands::Disable { name, project_root } => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(name);
if let Some(root) = project_root {
ctx.set_option("project-root".to_string(), root);
}
PluginDisableCommand.execute(&ctx).await
}
PluginCommands::Search { query, limit } => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
ctx.add_arg(query);
ctx.set_option("limit".to_string(), limit.to_string());
PluginSearchCommand.execute(&ctx).await
}
PluginCommands::Update {
name,
all,
yes,
project_root,
} => {
let mut ctx = CommandContext::default();
ctx.set_verbosity(verbosity);
if let Some(n) = name {
ctx.add_arg(n);
}
if all {
ctx.set_option("all".to_string(), "true".to_string());
}
if yes {
ctx.set_option("yes".to_string(), "true".to_string());
}
if let Some(root) = project_root {
ctx.set_option("project-root".to_string(), root);
}
PluginUpdateCommand.execute(&ctx).await
}
}
}
#[allow(clippy::too_many_arguments)] fn run_fmt(
path: PathBuf,
check: bool,
with_rustfmt: bool,
config_path: Option<PathBuf>,
edition: Option<String>,
style_edition: Option<String>,
config: Option<String>,
color: String,
backup: bool,
verbosity: u8,
) -> CommandResult<()> {
let mut command = formatter_delegate_command(verbosity);
command.arg("fmt").arg(path);
push_flag(&mut command, "--check", check);
if !with_rustfmt {
command.arg("--with-rustfmt=false");
}
push_optional_path(&mut command, "--config-path", config_path.as_ref());
push_optional_str(&mut command, "--edition", edition.as_deref());
push_optional_str(&mut command, "--style-edition", style_edition.as_deref());
push_optional_str(&mut command, "--config", config.as_deref());
push_optional_str(&mut command, "--color", Some(&color));
push_flag(&mut command, "--backup", backup);
run_formatter_delegate(command)
}
#[allow(clippy::too_many_arguments)] fn run_fmt_all(
check: bool,
config_path: Option<PathBuf>,
edition: Option<String>,
style_edition: Option<String>,
config: Option<String>,
color: String,
backup: bool,
verbosity: u8,
) -> CommandResult<()> {
let mut command = formatter_delegate_command(verbosity);
command.arg("fmt-all");
push_flag(&mut command, "--check", check);
push_optional_path(&mut command, "--config-path", config_path.as_ref());
push_optional_str(&mut command, "--edition", edition.as_deref());
push_optional_str(&mut command, "--style-edition", style_edition.as_deref());
push_optional_str(&mut command, "--config", config.as_deref());
push_optional_str(&mut command, "--color", Some(&color));
push_flag(&mut command, "--backup", backup);
run_formatter_delegate(command)
}
fn formatter_delegate_command(verbosity: u8) -> process::Command {
let mut command = process::Command::new("reinhardt-formatter");
for _ in 0..verbosity {
command.arg("-v");
}
command
}
fn push_flag(command: &mut process::Command, flag: &str, enabled: bool) {
if enabled {
command.arg(flag);
}
}
fn push_optional_path(command: &mut process::Command, flag: &str, value: Option<&PathBuf>) {
if let Some(value) = value {
command.arg(flag).arg(value);
}
}
fn push_optional_str(command: &mut process::Command, flag: &str, value: Option<&str>) {
if let Some(value) = value {
command.arg(flag).arg(value);
}
}
fn run_formatter_delegate(mut command: process::Command) -> CommandResult<()> {
let status = command.status().map_err(|err| {
reinhardt_commands::CommandError::ExecutionError(format!(
"failed to run reinhardt-formatter: {err}. Install or build the formatter binary to use fmt commands."
))
})?;
if status.success() {
Ok(())
} else {
Err(reinhardt_commands::CommandError::ExecutionError(format!(
"reinhardt-formatter exited with status {status}"
)))
}
}
#[cfg(test)]
mod resolve_project_type_tests {
use super::*;
#[test]
fn with_pages_bool_resolves_to_pages() {
assert!(matches!(
resolve_project_type(None, true, false),
ResolvedProjectType::Pages
));
}
#[test]
fn with_rest_bool_resolves_to_rest() {
assert!(matches!(
resolve_project_type(None, false, true),
ResolvedProjectType::Rest
));
}
#[test]
fn template_pages_resolves_to_pages() {
assert!(matches!(
resolve_project_type(Some(TemplateType::Pages), false, false),
ResolvedProjectType::Pages
));
}
#[test]
fn template_rest_resolves_to_rest() {
assert!(matches!(
resolve_project_type(Some(TemplateType::Rest), false, false),
ResolvedProjectType::Rest
));
}
#[test]
fn template_pages_with_bool_false_resolves_to_pages() {
assert!(matches!(
resolve_project_type(Some(TemplateType::Pages), false, false),
ResolvedProjectType::Pages
));
}
#[test]
fn template_rest_with_bool_false_resolves_to_rest() {
assert!(matches!(
resolve_project_type(Some(TemplateType::Rest), false, false),
ResolvedProjectType::Rest
));
}
}
#[cfg(test)]
mod arg_group_tests {
use super::*;
use clap::error::ErrorKind;
fn try_parse(args: &[&str]) -> Result<Cli, clap::Error> {
Cli::try_parse_from(args)
}
#[test]
fn startproject_with_pages_flag_accepted() {
assert!(
try_parse(&["reinhardt-admin", "startproject", "myproj", "--with-pages"]).is_ok(),
"--with-pages should be accepted"
);
}
#[test]
fn startproject_with_rest_flag_accepted() {
assert!(
try_parse(&["reinhardt-admin", "startproject", "myproj", "--with-rest"]).is_ok(),
"--with-rest should be accepted"
);
}
#[test]
fn startproject_template_pages_accepted() {
assert!(
try_parse(&[
"reinhardt-admin",
"startproject",
"myproj",
"--template",
"pages"
])
.is_ok(),
"--template pages should be accepted"
);
}
#[test]
fn startproject_template_rest_accepted() {
assert!(
try_parse(&[
"reinhardt-admin",
"startproject",
"myproj",
"--template",
"rest"
])
.is_ok(),
"--template rest should be accepted"
);
}
#[test]
fn startproject_missing_type_is_error() {
let result = try_parse(&["reinhardt-admin", "startproject", "myproj"]);
assert!(result.is_err(), "expected Err when type flag omitted");
assert_eq!(
result.err().unwrap().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn startproject_duplicate_flags_are_error() {
assert!(
try_parse(&[
"reinhardt-admin",
"startproject",
"myproj",
"--with-pages",
"--with-rest",
])
.is_err(),
"duplicate type flags should be rejected"
);
}
#[test]
fn startproject_template_and_alias_together_are_error() {
assert!(
try_parse(&[
"reinhardt-admin",
"startproject",
"myproj",
"--template",
"pages",
"--with-pages",
])
.is_err(),
"--template + --with-pages should be rejected"
);
}
#[test]
fn startapp_with_pages_flag_accepted() {
assert!(
try_parse(&["reinhardt-admin", "startapp", "myapp", "--with-pages"]).is_ok(),
"--with-pages should be accepted for startapp"
);
}
#[test]
fn startapp_missing_type_is_error() {
let result = try_parse(&["reinhardt-admin", "startapp", "myapp"]);
assert!(result.is_err(), "expected Err when type flag omitted");
assert_eq!(
result.err().unwrap().kind(),
ErrorKind::MissingRequiredArgument
);
}
}