use clap::{Parser, Subcommand};
use kick_rs_cli::register::RegisterOutcome;
use kick_rs_cli::{add, generate, info, new};
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser)]
#[command(
name = "cargo-kick",
bin_name = "cargo kick",
version,
about = "Companion CLI for the kick-rs framework"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
New {
name: String,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long)]
force: bool,
},
#[command(alias = "g")]
Generate {
#[command(subcommand)]
kind: Generate,
},
Info {
#[arg(long)]
path: Option<PathBuf>,
#[arg(long, default_value = "kick-rs")]
dep_name: String,
},
Add {
feature: String,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long, default_value = "kick-rs")]
dep_name: String,
},
}
#[derive(Subcommand)]
enum Generate {
Module {
name: String,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
no_register: bool,
},
Service {
spec: String,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
no_register: bool,
},
Contributor {
spec: String,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
no_register: bool,
},
}
fn main() -> ExitCode {
let argv: Vec<String> = std::env::args()
.enumerate()
.filter(|(i, a)| !(*i == 1 && a == "kick"))
.map(|(_, a)| a)
.collect();
let cli = match Cli::try_parse_from(argv) {
Ok(c) => c,
Err(e) => {
let _ = e.print();
return ExitCode::from(if e.exit_code() == 0 { 0 } else { 2 });
}
};
match cli.command {
Command::New { name, path, force } => {
let args = new::NewArgs { name, path, force };
match new::run(&args) {
Ok(dest) => {
println!("✓ created kick-rs project at {}", dest.display());
println!(" next: cd {} && cargo run", dest.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
Command::Generate {
kind:
Generate::Module {
name,
path,
force,
no_register,
},
} => {
let args = generate::GenerateModuleArgs {
name: name.clone(),
project_root: path,
force,
auto_register: !no_register,
};
match generate::generate_module(&args) {
Ok(res) => {
println!("✓ generated module at {}", res.module_dir.display());
print_register_outcome(
&res.register,
"main.rs",
&format!(".module(modules::{name}::define())"),
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
Command::Generate {
kind:
Generate::Service {
spec,
path,
force,
no_register,
},
} => {
let args = generate::GenerateServiceArgs {
spec: spec.clone(),
project_root: path,
force,
auto_register: !no_register,
};
match generate::generate_service(&args) {
Ok(res) => {
let (module, service_snake) = spec.split_once('/').unwrap();
let pascal = generate::to_pascal_case(service_snake);
println!("✓ generated service at {}", res.file.display());
print_register_outcome(
&res.register,
&format!("src/modules/{module}/mod.rs"),
&format!(
"use {service_snake}::{pascal};\n ...\n .service::<{pascal}>()"
),
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
Command::Generate {
kind:
Generate::Contributor {
spec,
path,
force,
no_register,
},
} => {
let args = generate::GenerateContributorArgs {
spec: spec.clone(),
project_root: path,
force,
auto_register: !no_register,
};
match generate::generate_contributor(&args) {
Ok(res) => {
let (module, snake) = spec.split_once('/').unwrap();
let pascal = generate::to_pascal_case(snake);
println!("✓ generated contributor at {}", res.file.display());
print_register_outcome(
&res.register,
&format!("src/modules/{module}/mod.rs"),
&format!(
"use {snake}::{pascal};\n ...\n .contribute({pascal})"
),
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
Command::Info { path, dep_name } => {
let args = info::InfoArgs {
project_root: path,
dep_name,
};
match info::collect_info(&args) {
Ok(snapshot) => {
print!("{}", info::render_info(&snapshot));
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
Command::Add {
feature,
path,
dep_name,
} => {
if feature == "list" {
println!("kick-rs features that `cargo kick add` knows about:");
for (name, desc) in add::KNOWN_FEATURES {
println!(" {name:10} — {desc}");
}
return ExitCode::SUCCESS;
}
let args = add::AddArgs {
feature: feature.clone(),
project_root: path,
dep_name: dep_name.clone(),
};
match add::add_feature(&args) {
Ok(add::AddOutcome::Added) => {
println!("✓ added `{feature}` to {dep_name} features in Cargo.toml");
ExitCode::SUCCESS
}
Ok(add::AddOutcome::AlreadyEnabled) => {
println!("· `{feature}` already enabled on {dep_name}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
}
}
fn print_register_outcome(outcome: &RegisterOutcome, target_path: &str, manual_snippet: &str) {
match outcome {
RegisterOutcome::Inserted => {
println!(" ✓ registered in {target_path}");
}
RegisterOutcome::AlreadyRegistered => {
println!(" · {target_path} already had the registration — no edit needed");
}
RegisterOutcome::TargetMissing => {
println!(" ! {target_path} not found — add manually:");
println!(" {manual_snippet}");
}
RegisterOutcome::AnchorNotFound => {
println!(" ! could not find a known builder pattern in {target_path}; add manually:");
println!(" {manual_snippet}");
}
RegisterOutcome::Skipped => {
println!(" · skipped per --no-register — add manually:");
println!(" {manual_snippet}");
}
}
}