use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(
name = "umbral",
about = "umbral CLI. Scaffolds projects (startproject/startapp/startplugin) 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>,
},
#[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 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 = match cli.command {
Command::Startproject { name, path, local } => {
umbral_cli::scaffold::scaffold_project(&name, &path, local.as_deref()).map(|r| {
println!("Created `{}`:", r.root.display());
for f in &r.files {
println!(" {}", f.display());
}
println!();
println!("Next steps:");
for step in &r.next_steps {
println!(" {step}");
}
})
}
Command::Startapp { name, path, local } => {
umbral_cli::scaffold::scaffold_app(&name, &path, local.as_deref()).map(|r| {
println!("Created `{}`:", r.root.display());
for f in &r.files {
println!(" {}", f.display());
}
println!();
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 step:");
for step in &r.next_steps {
println!(" {step}");
}
})
}
Command::Startplugin { name, path, local } => {
umbral_cli::scaffold::scaffold_plugin(&name, &path, local.as_deref()).map(|r| {
println!("Created `{}`:", r.root.display());
for f in &r.files {
println!(" {}", f.display());
}
println!();
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}");
}
})
}
Command::Forward(_) => unreachable!("Forward is dispatched before this match"),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("error: {err}");
ExitCode::FAILURE
}
}
}