mod authoring;
mod module;
mod plugin;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
#[command(
name = "lenso",
version,
about = "Create, develop, check, and package Lenso Plugins",
propagate_version = true
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Plugin {
#[command(subcommand)]
command: plugin::PluginCommand,
},
Module {
#[command(subcommand)]
command: ModuleCommand,
},
App {
#[command(subcommand)]
command: authoring::AppCommand,
},
#[command(hide = true)]
New(ModuleCreateArgs),
#[command(hide = true)]
Dev(ModuleDevArgs),
#[command(hide = true)]
Check(ModuleCheckArgs),
#[command(hide = true)]
Verify(ModuleVerifyArgs),
}
#[derive(Debug, Subcommand)]
enum ModuleCommand {
New(ModuleCreateArgs),
Dev(ModuleDevArgs),
Check(ModuleCheckArgs),
Verify(ModuleVerifyArgs),
}
#[derive(Debug, Args, Clone)]
struct ModuleCreateArgs {
module_id: String,
#[arg(long)]
repo_root: Option<std::path::PathBuf>,
#[arg(long, value_enum, default_value_t = ModuleRuntimeArg::Rust)]
runtime: ModuleRuntimeArg,
#[arg(long, value_enum, default_value_t = ModuleRecipeArg::Stateless)]
recipe: ModuleRecipeArg,
#[arg(long)]
dir: Option<std::path::PathBuf>,
#[arg(long)]
no_install: bool,
#[arg(long)]
capability: Option<String>,
#[arg(long)]
dry_run: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum ModuleRuntimeArg {
Rust,
Bun,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum ModuleRecipeArg {
Stateless,
Stateful,
WebConsole,
ManagedWork,
}
#[derive(Debug, Args, Clone)]
struct ModuleDevArgs {
#[arg(long)]
repo_root: Option<std::path::PathBuf>,
#[arg(long, default_value = "lenso.json")]
project: std::path::PathBuf,
#[arg(long = "bun-bin", default_value = "bun")]
bun_bin: String,
}
#[derive(Debug, Args, Clone)]
struct ModuleCheckArgs {
#[arg(long)]
repo_root: Option<std::path::PathBuf>,
#[arg(long, default_value = "lenso.json")]
project: std::path::PathBuf,
#[arg(long)]
json: bool,
#[arg(long)]
update_contracts: bool,
}
#[derive(Debug, Args, Clone)]
struct ModuleVerifyArgs {
#[arg(long)]
repo_root: Option<std::path::PathBuf>,
#[arg(long, default_value = "lenso.json")]
project: std::path::PathBuf,
#[arg(long = "module")]
module_key: Option<String>,
#[arg(long, default_value = "lenso.module.verify.json")]
manifest: std::path::PathBuf,
#[arg(long, default_value = ".lenso/module-verification.json")]
output: std::path::PathBuf,
#[arg(long)]
json: bool,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Plugin { command } => plugin::plugin(command).await?,
Command::Module { command } => run_module_command(command).await?,
Command::App { command } => authoring::app(command)?,
Command::New(args) => {
eprintln!("{}", compatibility_warning("new", "module new"));
create_module(args)?;
}
Command::Dev(args) => {
eprintln!("{}", compatibility_warning("dev", "module dev"));
dev_module(args).await?;
}
Command::Check(args) => {
eprintln!("{}", compatibility_warning("check", "module check"));
check_module(args)?;
}
Command::Verify(args) => {
eprintln!("{}", compatibility_warning("verify", "module verify"));
verify_module(args)?;
}
}
Ok(())
}
async fn run_module_command(command: ModuleCommand) -> anyhow::Result<()> {
match command {
ModuleCommand::New(args) => create_module(args),
ModuleCommand::Dev(args) => dev_module(args).await,
ModuleCommand::Check(args) => check_module(args),
ModuleCommand::Verify(args) => verify_module(args),
}
}
fn compatibility_warning(old: &str, new: &str) -> String {
format!("warning: `lenso {old}` is deprecated; use `lenso {new}`")
}
fn create_module(args: ModuleCreateArgs) -> anyhow::Result<()> {
module::create_module(&module::ModuleCreateOptions {
capability: args.capability,
dir: args.dir,
dry_run: args.dry_run,
module_id: args.module_id,
no_install: args.no_install,
repo_root: args.repo_root,
recipe: match args.recipe {
ModuleRecipeArg::Stateless => module::ModuleRecipe::Stateless,
ModuleRecipeArg::Stateful => module::ModuleRecipe::Stateful,
ModuleRecipeArg::WebConsole => module::ModuleRecipe::WebConsole,
ModuleRecipeArg::ManagedWork => module::ModuleRecipe::ManagedWork,
},
runtime: match args.runtime {
ModuleRuntimeArg::Rust => module::ModuleRuntime::Rust,
ModuleRuntimeArg::Bun => module::ModuleRuntime::Bun,
},
})
}
async fn dev_module(args: ModuleDevArgs) -> anyhow::Result<()> {
authoring::dev_module(args.repo_root.as_deref(), &args.project, &args.bun_bin).await
}
fn check_module(args: ModuleCheckArgs) -> anyhow::Result<()> {
authoring::check_module(&authoring::ModuleCheckOptions {
json: args.json,
project: args.project,
repo_root: args.repo_root,
update_contracts: args.update_contracts,
})
}
fn verify_module(args: ModuleVerifyArgs) -> anyhow::Result<()> {
authoring::verify_module(authoring::ModuleVerifyOptions {
json: args.json,
manifest: args.manifest,
module_key: args.module_key,
output: args.output,
project: args.project,
repo_root: args.repo_root,
})
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn command_tree_is_valid() {
Cli::command().debug_assert();
}
#[test]
fn normal_help_exposes_plugin_module_and_app_namespaces() {
let command = Cli::command();
let names = command
.get_subcommands()
.filter(|subcommand| !subcommand.is_hide_set())
.map(clap::Command::get_name)
.collect::<Vec<_>>();
assert_eq!(names, ["plugin", "module", "app"]);
let app = command
.get_subcommands()
.find(|subcommand| subcommand.get_name() == "app")
.expect("app command");
let app_names = app
.get_subcommands()
.map(clap::Command::get_name)
.collect::<Vec<_>>();
assert_eq!(app_names, ["add", "remove", "check", "resolve"]);
let plugin = command
.get_subcommands()
.find(|subcommand| subcommand.get_name() == "plugin")
.expect("plugin command");
let plugin_names = plugin
.get_subcommands()
.filter(|subcommand| !subcommand.is_hide_set())
.map(clap::Command::get_name)
.collect::<Vec<_>>();
assert_eq!(plugin_names, ["new", "dev", "check", "pack"]);
let module = command
.get_subcommands()
.find(|subcommand| subcommand.get_name() == "module")
.expect("module command");
let module_names = module
.get_subcommands()
.map(clap::Command::get_name)
.collect::<Vec<_>>();
assert_eq!(module_names, ["new", "dev", "check", "verify"]);
}
#[test]
fn deprecated_aliases_are_hidden_and_actionable() {
let command = Cli::command();
for name in ["new", "dev", "check", "verify"] {
assert!(
command
.get_subcommands()
.find(|subcommand| subcommand.get_name() == name)
.expect("compatibility command")
.is_hide_set()
);
}
assert_eq!(
compatibility_warning("check", "module check"),
"warning: `lenso check` is deprecated; use `lenso module check`"
);
}
}