use clap::{CommandFactory, Parser, Subcommand};
use crate::engine::tasks::TaskManager;
use crate::errors::AxumApcoreError;
use crate::output::AxumRegistryWriter;
use crate::scanner::get_scanner;
use clap_complete::Shell;
#[derive(Parser, Debug)]
#[command(name = "axum-apcore", version, about)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Scan {
#[arg(long, default_value = "native")]
source: String,
#[arg(long, default_value = "registry")]
output: String,
#[arg(long)]
dir: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
include: Option<String>,
#[arg(long)]
exclude: Option<String>,
#[arg(long)]
verify: bool,
},
Serve {
#[arg(long)]
transport: Option<String>,
#[arg(long)]
host: Option<String>,
#[arg(long)]
port: Option<u16>,
#[arg(long)]
explorer: bool,
#[arg(long, env = "APCORE_JWT_SECRET")]
jwt_secret: Option<String>,
#[arg(long, default_value = "auto")]
approval: String,
#[arg(long)]
tags: Option<String>,
#[arg(long)]
prefix: Option<String>,
},
Export {
#[arg(long, default_value = "openai-tools")]
format: String,
#[arg(long)]
strict: bool,
#[arg(long)]
embed_annotations: bool,
#[arg(long)]
tags: Option<String>,
#[arg(long)]
prefix: Option<String>,
},
Tasks {
#[command(subcommand)]
action: TaskCommands,
},
List {
#[arg(long)]
tag: Vec<String>,
#[arg(long)]
format: Option<String>,
},
Describe {
module_id: String,
#[arg(long)]
format: Option<String>,
},
Completion {
shell: String,
},
Man {
command: Option<String>,
},
Init {
#[command(subcommand)]
action: InitCommands,
},
}
#[derive(Subcommand, Debug)]
pub enum InitCommands {
Module {
module_id: String,
#[arg(long, default_value = "convention")]
style: String,
#[arg(long)]
dir: Option<String>,
#[arg(long)]
description: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum TaskCommands {
List {
#[arg(long)]
status: Option<String>,
},
Cancel {
task_id: String,
},
Cleanup {
#[arg(long, default_value = "3600")]
max_age: u64,
},
}
pub fn parse_cli() -> Cli {
Cli::parse()
}
pub async fn run(cli: Cli) -> Result<(), AxumApcoreError> {
match cli.command {
Commands::Scan {
source,
output,
dir,
dry_run,
include,
exclude,
verify,
} => run_scan(source, output, dir, dry_run, include, exclude, verify).await,
Commands::Serve {
transport,
host,
port,
tags,
prefix,
..
} => run_serve(transport, host, port, tags, prefix).await,
Commands::Export {
format,
strict,
embed_annotations,
tags,
prefix,
} => run_export(format, strict, embed_annotations, tags, prefix),
Commands::Tasks { action } => run_tasks(action),
Commands::List { tag, format } => run_list(tag, format),
Commands::Describe { module_id, format } => run_describe(module_id, format),
Commands::Completion { shell } => run_completion(shell),
Commands::Man { command } => run_man(command),
Commands::Init { action } => run_init(action),
}
}
async fn run_scan(
source: String,
output: String,
dir: Option<String>,
dry_run: bool,
include: Option<String>,
exclude: Option<String>,
verify: bool,
) -> Result<(), AxumApcoreError> {
let scanner = get_scanner(&source)?;
let router = axum::Router::new();
let include_ref = include.as_deref();
let exclude_ref = exclude.as_deref();
let modules = scanner.scan(&router, include_ref, exclude_ref).await?;
println!("Scanned {} modules via '{}' scanner", modules.len(), source);
for m in &modules {
println!(" - {} ({})", m.module_id, m.description);
}
if dry_run {
println!("Dry run — no output written.");
return Ok(());
}
match output.as_str() {
"registry" => {
let writer = AxumRegistryWriter::new();
let mut registry = apcore::Registry::new();
let results = writer.write(&modules, &mut registry, false, verify);
let ok_count = results.iter().filter(|r| r.verified).count();
println!("Registered {}/{} modules", ok_count, results.len());
}
"yaml" => {
let out_dir = dir.unwrap_or_else(|| "apcore_modules".into());
let yaml_writer = apcore_toolkit::YAMLWriter;
let results = yaml_writer
.write(&modules, &out_dir, false, verify, None)
.map_err(|e| AxumApcoreError::Config(format!("YAML write failed: {e}")))?;
let ok_count = results.iter().filter(|r| r.verified).count();
println!(
"Wrote {}/{} YAML bindings to '{}'",
ok_count,
results.len(),
out_dir
);
}
other => {
return Err(AxumApcoreError::Config(format!(
"Unknown output format: '{}'. Available: registry, yaml",
other
)));
}
}
Ok(())
}
async fn run_serve(
transport: Option<String>,
host: Option<String>,
port: Option<u16>,
tags: Option<String>,
prefix: Option<String>,
) -> Result<(), AxumApcoreError> {
#[cfg(not(feature = "mcp"))]
{
let _ = (transport, host, port, tags, prefix);
return Err(AxumApcoreError::Config(
"MCP feature not enabled. Build with --features mcp".into(),
));
}
#[cfg(feature = "mcp")]
{
let settings = crate::config::get_apcore_settings();
let transport_str = transport.as_deref().unwrap_or(&settings.serve_transport);
let host_str = host.as_deref().unwrap_or(&settings.serve_host);
let port_val = port.unwrap_or(settings.serve_port);
let parsed_tags = tags.map(|t| parse_tags(&t));
println!(
"Starting MCP server on {}://{}:{}",
transport_str, host_str, port_val
);
let config = apcore_mcp::ServeConfig {
name: settings.server_name.clone(),
transport: transport_str.to_string(),
host: host_str.to_string(),
port: port_val,
tags: parsed_tags,
prefix,
..apcore_mcp::ServeConfig::default()
};
let backend = apcore_mcp::BackendSource::ExtensionsDir(settings.module_dir.clone());
apcore_mcp::serve(backend, config)
.map_err(|e| AxumApcoreError::Config(format!("MCP server error: {e}")))?;
Ok(())
}
}
fn run_export(
format: String,
strict: bool,
embed_annotations: bool,
tags: Option<String>,
prefix: Option<String>,
) -> Result<(), AxumApcoreError> {
#[cfg(not(feature = "mcp"))]
{
let _ = (format, strict, embed_annotations, tags, prefix);
return Err(AxumApcoreError::Config(
"MCP feature not enabled. Build with --features mcp".into(),
));
}
#[cfg(feature = "mcp")]
{
if format != "openai-tools" {
return Err(AxumApcoreError::Config(format!(
"Unknown export format: '{}'. Available: openai-tools",
format
)));
}
let settings = crate::config::get_apcore_settings();
let parsed_tags = tags.map(|t| parse_tags(&t));
let config = apcore_mcp::OpenAIToolsConfig {
embed_annotations,
strict,
tags: parsed_tags,
prefix,
};
let backend = apcore_mcp::BackendSource::ExtensionsDir(settings.module_dir.clone());
let tools = apcore_mcp::to_openai_tools(backend, config)
.map_err(|e| AxumApcoreError::Config(format!("Export failed: {e}")))?;
let json = serde_json::to_string_pretty(&tools).map_err(AxumApcoreError::Json)?;
println!("{}", json);
println!("Exported {} OpenAI tool definitions", tools.len());
Ok(())
}
}
fn run_tasks(action: TaskCommands) -> Result<(), AxumApcoreError> {
let settings = crate::config::get_apcore_settings();
let task_manager = TaskManager::from_settings(settings);
match action {
TaskCommands::List { status } => {
let tasks = task_manager.list_tasks(status.as_deref());
if tasks.is_empty() {
println!("No tasks found.");
} else {
let header = format!(
"{:<36} {:<20} {:<10} {}",
"TASK ID", "MODULE", "STATUS", "CREATED"
);
println!("{header}");
for task in &tasks {
println!(
"{:<36} {:<20} {:<10} {}",
task.task_id, task.module_id, task.status, task.created_at
);
}
println!("\nTotal: {} tasks", tasks.len());
}
}
TaskCommands::Cancel { task_id } => {
if task_manager.cancel(&task_id) {
println!("Task '{}' cancelled.", task_id);
} else {
println!("Task '{}' not found or not running.", task_id);
}
}
TaskCommands::Cleanup { max_age } => {
let removed = task_manager.cleanup(max_age);
println!("Cleaned up {} tasks (older than {}s).", removed, max_age);
}
}
Ok(())
}
fn build_registry_provider() -> apcore_cli::ApCoreRegistryProvider {
let registry_arc = crate::engine::registry::get_registry();
let registry = registry_arc.lock().expect("registry lock poisoned");
let fresh = apcore::Registry::new();
for name in registry.list(None, None, None) {
if let Ok(Some(descriptor)) = registry.get_definition(&name) {
let fm = apcore::decorator::FunctionModule::new::<_, ()>(
descriptor.annotations.clone().unwrap_or_default(),
descriptor.input_schema.clone(),
descriptor.output_schema.clone(),
|inputs: serde_json::Value,
_ctx: &apcore::Context<serde_json::Value>|
-> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<serde_json::Value, apcore::ModuleError>,
> + Send
+ '_,
>,
> { Box::pin(async move { Ok(inputs) }) },
);
let _ = fresh.register(&name, Box::new(fm), descriptor.clone());
}
}
apcore_cli::ApCoreRegistryProvider::new(fresh)
}
fn run_list(tags: Vec<String>, format: Option<String>) -> Result<(), AxumApcoreError> {
let provider = build_registry_provider();
let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
let opts = apcore_cli::ListOptions {
tags: &tag_refs,
explicit_format: format.as_deref(),
..apcore_cli::ListOptions::default()
};
let output = apcore_cli::cmd_list_enhanced(&provider, &opts)
.map_err(|e| AxumApcoreError::Config(format!("List error: {e}")))?;
println!("{output}");
Ok(())
}
fn run_describe(module_id: String, format: Option<String>) -> Result<(), AxumApcoreError> {
let provider = build_registry_provider();
let output = apcore_cli::cmd_describe(&provider, &module_id, format.as_deref())
.map_err(|e| AxumApcoreError::Config(format!("Describe error: {e}")))?;
println!("{output}");
Ok(())
}
fn run_completion(shell: String) -> Result<(), AxumApcoreError> {
let shell_val: Shell = shell
.parse()
.map_err(|e| AxumApcoreError::Config(format!("Unknown shell '{shell}': {e}")))?;
let mut cmd = Cli::command();
let output = apcore_cli::cmd_completion(shell_val, "axum-apcore", &mut cmd);
print!("{output}");
Ok(())
}
fn run_man(command: Option<String>) -> Result<(), AxumApcoreError> {
let root_cmd = Cli::command();
match command {
Some(cmd_name) => {
let output = apcore_cli::cmd_man(
&cmd_name,
&root_cmd,
"axum-apcore",
env!("CARGO_PKG_VERSION"),
)
.map_err(|e| AxumApcoreError::Config(format!("Man error: {e}")))?;
print!("{output}");
}
None => {
let output = apcore_cli::build_program_man_page(
&root_cmd,
"axum-apcore",
env!("CARGO_PKG_VERSION"),
None,
None,
);
print!("{output}");
}
}
Ok(())
}
fn run_init(action: InitCommands) -> Result<(), AxumApcoreError> {
match action {
InitCommands::Module {
module_id,
style,
dir,
description,
} => {
let mut args = vec![
"init".to_string(),
"module".to_string(),
module_id,
"--style".to_string(),
style,
];
if let Some(d) = dir {
args.push("--dir".to_string());
args.push(d);
}
if let Some(desc) = description {
args.push("--description".to_string());
args.push(desc);
}
let cmd = apcore_cli::init_command();
let matches = cmd
.try_get_matches_from(args)
.map_err(|e| AxumApcoreError::Config(format!("Init error: {e}")))?;
apcore_cli::handle_init(&matches);
Ok(())
}
}
}
fn parse_tags(tags: &str) -> Vec<String> {
tags.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_cli_parse_scan() {
let cli = Cli::try_parse_from(["axum-apcore", "scan"]).unwrap();
assert!(matches!(cli.command, Commands::Scan { .. }));
}
#[test]
fn test_cli_parse_serve() {
let cli = Cli::try_parse_from(["axum-apcore", "serve"]).unwrap();
assert!(matches!(cli.command, Commands::Serve { .. }));
}
#[test]
fn test_cli_parse_export() {
let cli = Cli::try_parse_from(["axum-apcore", "export"]).unwrap();
assert!(matches!(cli.command, Commands::Export { .. }));
}
#[test]
fn test_cli_parse_tasks_list() {
let cli = Cli::try_parse_from(["axum-apcore", "tasks", "list"]).unwrap();
assert!(matches!(
cli.command,
Commands::Tasks {
action: TaskCommands::List { .. }
}
));
}
#[test]
fn test_cli_parse_tasks_cancel() {
let cli = Cli::try_parse_from(["axum-apcore", "tasks", "cancel", "task-123"]).unwrap();
assert!(matches!(
cli.command,
Commands::Tasks {
action: TaskCommands::Cancel { .. }
}
));
}
#[test]
fn test_cli_verify() {
Cli::command().debug_assert();
}
#[test]
fn test_parse_tags() {
assert_eq!(parse_tags("a, b, c"), vec!["a", "b", "c"]);
assert_eq!(parse_tags("single"), vec!["single"]);
assert_eq!(parse_tags(""), Vec::<String>::new());
assert_eq!(parse_tags("a,,b"), vec!["a", "b"]);
}
#[test]
fn test_cli_parse_scan_with_options() {
let cli = Cli::try_parse_from([
"axum-apcore",
"scan",
"--source",
"openapi",
"--output",
"yaml",
"--dry-run",
"--include",
"users",
"--verify",
])
.unwrap();
match cli.command {
Commands::Scan {
source,
output,
dry_run,
include,
verify,
..
} => {
assert_eq!(source, "openapi");
assert_eq!(output, "yaml");
assert!(dry_run);
assert_eq!(include.unwrap(), "users");
assert!(verify);
}
_ => panic!("Expected Scan command"),
}
}
#[test]
fn test_cli_parse_serve_with_options() {
let cli = Cli::try_parse_from([
"axum-apcore",
"serve",
"--transport",
"sse",
"--host",
"0.0.0.0",
"--port",
"8080",
"--tags",
"users,tasks",
])
.unwrap();
match cli.command {
Commands::Serve {
transport,
host,
port,
tags,
..
} => {
assert_eq!(transport.unwrap(), "sse");
assert_eq!(host.unwrap(), "0.0.0.0");
assert_eq!(port.unwrap(), 8080);
assert_eq!(tags.unwrap(), "users,tasks");
}
_ => panic!("Expected Serve command"),
}
}
#[test]
fn test_cli_parse_list() {
let cli = Cli::try_parse_from(["axum-apcore", "list"]).unwrap();
assert!(matches!(cli.command, Commands::List { .. }));
}
#[test]
fn test_cli_parse_list_with_tags() {
let cli = Cli::try_parse_from(["axum-apcore", "list", "--tag", "users", "--tag", "admin"])
.unwrap();
match cli.command {
Commands::List { tag, .. } => {
assert_eq!(tag, vec!["users", "admin"]);
}
_ => panic!("Expected List command"),
}
}
#[test]
fn test_cli_parse_describe() {
let cli = Cli::try_parse_from(["axum-apcore", "describe", "users.get_user.get"]).unwrap();
match cli.command {
Commands::Describe { module_id, .. } => {
assert_eq!(module_id, "users.get_user.get");
}
_ => panic!("Expected Describe command"),
}
}
#[test]
fn test_cli_parse_completion() {
let cli = Cli::try_parse_from(["axum-apcore", "completion", "bash"]).unwrap();
match cli.command {
Commands::Completion { shell } => {
assert_eq!(shell, "bash");
}
_ => panic!("Expected Completion command"),
}
}
#[test]
fn test_cli_parse_man() {
let cli = Cli::try_parse_from(["axum-apcore", "man"]).unwrap();
assert!(matches!(cli.command, Commands::Man { .. }));
}
#[test]
fn test_build_registry_provider_empty() {
let provider = build_registry_provider();
let modules = apcore_cli::discovery::RegistryProvider::list(&provider);
let _ = modules;
}
#[test]
fn test_run_list_empty_registry() {
let result = run_list(vec![], Some("json".into()));
assert!(result.is_ok());
}
#[test]
fn test_run_completion_bash() {
let result = run_completion("bash".into());
assert!(result.is_ok());
}
#[test]
fn test_run_completion_invalid_shell() {
let result = run_completion("invalid-shell".into());
assert!(result.is_err());
}
#[test]
fn test_run_man_program_page() {
let result = run_man(None);
assert!(result.is_ok());
}
#[test]
fn test_run_man_unknown_command() {
let result = run_man(Some("nonexistent-command".into()));
assert!(result.is_err());
}
#[test]
fn test_cli_parse_init_module() {
let cli = Cli::try_parse_from([
"axum-apcore",
"init",
"module",
"users.get_user",
"--style",
"binding",
])
.unwrap();
match cli.command {
Commands::Init {
action:
InitCommands::Module {
module_id, style, ..
},
} => {
assert_eq!(module_id, "users.get_user");
assert_eq!(style, "binding");
}
_ => panic!("Expected Init Module command"),
}
}
}