use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(
name = "umbral",
about = "umbral CLI. Scaffolds projects, plugins and commands \
(startproject/startapp/startplugin/startcommand) and runs project-free \
utilities (maskkeygen) directly; every other command (serve, migrate, \
makemigrations, worker, seed_data, …) is forwarded to \
`cargo run -- <command>` in the current project.",
disable_help_subcommand = true
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Startproject {
name: String,
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long, value_name = "PATH")]
local: Option<PathBuf>,
},
Startapp {
name: String,
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long, value_name = "PATH")]
local: Option<PathBuf>,
},
Startplugin {
name: String,
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long, value_name = "PATH")]
local: Option<PathBuf>,
},
Startcommand {
name: Option<String>,
#[arg(long = "in", value_name = "root|PLUGIN")]
target: Option<String>,
#[arg(long, default_value = ".")]
path: PathBuf,
},
#[command(external_subcommand)]
Forward(Vec<String>),
}
fn forward_to_project(args: &[String]) -> ExitCode {
let cwd = match std::env::current_dir() {
Ok(dir) => dir,
Err(e) => {
eprintln!("error: cannot read the current directory: {e}");
return ExitCode::FAILURE;
}
};
if !umbral_cli::in_cargo_project(&cwd) {
let cmd = args.first().map(String::as_str).unwrap_or("<command>");
eprintln!(
"error: `umbral {cmd}` must run inside an umbral project — no Cargo.toml in {} \
(or any parent).\n cd into your project directory, or create one with \
`umbral startproject <name>`.",
cwd.display()
);
return ExitCode::FAILURE;
}
let cargo_args = umbral_cli::cargo_run_forward_args(args);
match std::process::Command::new("cargo")
.args(&cargo_args)
.status()
{
Ok(status) => ExitCode::from(status.code().unwrap_or(1) as u8),
Err(e) => {
eprintln!("error: failed to run `cargo {}`: {e}", cargo_args.join(" "));
ExitCode::FAILURE
}
}
}
fn run_startcommand(
name: Option<String>,
target: Option<String>,
path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
use umbral::codegen::{Target as CommandTarget, prompt};
use umbral_cli::scaffold::scaffold_command;
let interactive = prompt::is_interactive();
let name = match name {
Some(n) => n,
None if interactive => prompt::ask_required("Command name (e.g. backfill_slugs): ")?,
None => {
return Err("a command name is required when stdin isn't a terminal: \
`umbral startcommand <NAME> --in root`"
.into());
}
};
let name = name.trim().to_string();
let target = match target {
Some(t) => CommandTarget::parse(&t),
None if interactive => prompt::ask_target(path)?,
None => {
return Err("`--in <root|PLUGIN>` is required when stdin isn't a terminal".into());
}
};
let report = scaffold_command(&name, &target, path)?;
println!("Created in `{}`:", report.root.display());
for f in &report.files {
println!(" {}", f.display());
}
println!();
match (&target, report.registered) {
(_, Some(false)) => {
println!("NOT registered yet — the steps below are the ones I could not do for you.")
}
(CommandTarget::Root, _) => println!(
"Registered `{name}` on the App builder (src/main.rs: `.commands(commands::all())`)."
),
(CommandTarget::Plugin(p), _) => println!(
"Registered `{name}` with the `{p}` plugin (src/lib.rs: `Plugin::commands()`)."
),
}
println!();
println!("Next steps:");
for step in &report.next_steps {
println!(" {step}");
}
Ok(())
}
fn print_report(r: &umbral_cli::scaffold::ScaffoldReport, name: &str, wants_dep: bool) {
println!("Created `{}`:", r.root.display());
for f in &r.files {
println!(" {}", f.display());
}
println!();
if wants_dep {
match r.cargo_toml_registered {
Some(true) => {
println!("Registered `{name} = {{ path = \"plugins/{name}\" }}` in Cargo.toml.")
}
Some(false) => println!("Cargo.toml already lists `{name}` — no duplicate added."),
None => println!(
"Note: could not find a Cargo.toml to update. \
Add `{name} = {{ path = \"plugins/{name}\" }}` manually."
),
}
println!();
}
println!("Next steps:");
for step in &r.next_steps {
println!(" {step}");
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
if let Command::Forward(args) = &cli.command {
if let Some(result) = umbral_cli::try_run_standalone(args) {
return match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
};
}
return forward_to_project(args);
}
let result: Result<(), Box<dyn std::error::Error>> = match cli.command {
Command::Startproject { name, path, local } => {
umbral_cli::scaffold::scaffold_project(&name, &path, local.as_deref())
.map(|r| print_report(&r, &name, false))
.map_err(Into::into)
}
Command::Startapp { name, path, local } => {
umbral_cli::scaffold::scaffold_app(&name, &path, local.as_deref())
.map(|r| print_report(&r, &name, true))
.map_err(Into::into)
}
Command::Startplugin { name, path, local } => {
umbral_cli::scaffold::scaffold_plugin(&name, &path, local.as_deref())
.map(|r| print_report(&r, &name, true))
.map_err(Into::into)
}
Command::Startcommand { name, target, path } => run_startcommand(name, target, &path),
Command::Forward(_) => unreachable!("Forward is dispatched before this match"),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("error: {err}");
ExitCode::FAILURE
}
}
}