use clap::{Parser, Subcommand};
use goblin_engine::{Engine, EngineConfig, EnginePool, Result};
use std::path::PathBuf;
use tracing::{info, error};
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
#[derive(Parser)]
#[command(name = "goblin")]
#[command(about = "A workflow engine for executing scripts in a planned sequence")]
#[command(version)]
struct Cli {
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
#[arg(short, long, value_name = "DIR")]
scripts_dir: Option<PathBuf>,
#[arg(short, long, value_name = "DIR")]
plans_dir: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
#[arg(long, value_name = "SIZE")]
pool_size: Option<usize>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init {
#[arg(value_name = "DIR")]
directory: Option<PathBuf>,
},
Scripts,
Plans,
RunScript {
script: String,
args: Vec<String>,
},
RunPlan {
plan: String,
#[arg(short, long)]
input: Option<String>,
},
Validate,
Stats,
Config,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let log_level = if cli.verbose { "debug" } else { "info" };
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(log_level));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_span_events(FmtSpan::CLOSE)
.init();
let mut config = load_config(&cli).await?;
if let Some(scripts_dir) = cli.scripts_dir {
config.scripts_dir = Some(scripts_dir);
}
if let Some(plans_dir) = cli.plans_dir {
config.plans_dir = Some(plans_dir);
}
let mut engine = Engine::new();
if let Some(scripts_dir) = &config.scripts_dir {
engine = engine.with_scripts_dir(scripts_dir.clone());
}
if config.scripts_dir.is_some() {
match engine.auto_discover_scripts() {
Ok(count) => info!("Discovered {} scripts", count),
Err(e) => error!("Failed to discover scripts: {}", e),
}
}
if let Some(plans_dir) = &config.plans_dir {
load_plans(&engine, plans_dir).await?;
}
let pool = if let Some(pool_size) = cli.pool_size {
let size = if pool_size == 0 { 8 } else { pool_size };
info!("Creating engine pool with {} instances", size);
let pool = EnginePool::with_config(size, config.scripts_dir.clone()).await?;
if let Some(plans_dir) = &config.plans_dir {
load_plans_on_pool(&pool, plans_dir).await?;
}
Some(pool)
} else {
None
};
match cli.command {
Commands::Init { directory } => {
let target_dir = directory.unwrap_or_else(|| PathBuf::from("."));
init_project(target_dir).await?;
}
Commands::Scripts => {
list_scripts(&engine).await?;
}
Commands::Plans => {
list_plans(&engine).await?;
}
Commands::RunScript { script, args } => {
if let Some(pool) = &pool {
run_script_with_pool(pool, &script, args).await?;
} else {
run_script(&engine, &script, args).await?;
}
}
Commands::RunPlan { plan, input } => {
if let Some(pool) = &pool {
run_plan_with_pool(pool, &plan, input).await?;
} else {
run_plan(&engine, &plan, input).await?;
}
}
Commands::Validate => {
validate_engine(&engine).await?;
}
Commands::Stats => {
if let Some(pool) = &pool {
show_pool_stats(pool).await?;
} else {
show_stats(&engine).await?;
}
}
Commands::Config => {
generate_sample_config().await?;
}
}
Ok(())
}
async fn load_config(cli: &Cli) -> Result<EngineConfig> {
if let Some(config_path) = &cli.config {
info!("Loading configuration from: {}", config_path.display());
EngineConfig::from_file(config_path)
} else {
let default_config = PathBuf::from("goblin.toml");
if default_config.exists() {
info!("Loading configuration from: {}", default_config.display());
EngineConfig::from_file(default_config)
} else {
info!("Using default configuration");
Ok(EngineConfig::default())
}
}
}
async fn load_plans(engine: &Engine, plans_dir: &PathBuf) -> Result<()> {
if !plans_dir.exists() {
error!("Plans directory does not exist: {}", plans_dir.display());
return Ok(());
}
let mut loaded = 0;
for entry in std::fs::read_dir(plans_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "toml") {
match engine.load_plan(path.clone()) {
Ok(_) => {
info!("Loaded plan: {}", path.file_stem().unwrap().to_string_lossy());
loaded += 1;
}
Err(e) => {
error!("Failed to load plan from {}: {}", path.display(), e);
}
}
}
}
info!("Loaded {} plans", loaded);
Ok(())
}
async fn init_project(target_dir: PathBuf) -> Result<()> {
info!("Initializing goblin project in: {}", target_dir.display());
std::fs::create_dir_all(&target_dir)?;
let scripts_dir = target_dir.join("scripts");
let plans_dir = target_dir.join("plans");
std::fs::create_dir_all(&scripts_dir)?;
std::fs::create_dir_all(&plans_dir)?;
let config_path = target_dir.join("goblin.toml");
if !config_path.exists() {
let sample_config = EngineConfig::sample_config();
std::fs::write(&config_path, sample_config)?;
info!("Created configuration file: {}", config_path.display());
}
let example_script_dir = scripts_dir.join("example");
std::fs::create_dir_all(&example_script_dir)?;
let goblin_toml_content = r#"name = "example"
command = "echo 'Hello from Goblin!'"
timeout = 30
test_command = "echo true"
require_test = false
"#;
let goblin_toml_path = example_script_dir.join("goblin.toml");
if !goblin_toml_path.exists() {
std::fs::write(&goblin_toml_path, goblin_toml_content)?;
info!("Created example script: {}", goblin_toml_path.display());
}
let example_plan_content = r#"name = "example_plan"
[[steps]]
name = "greeting"
function = "example"
inputs = ["default_input"]
"#;
let example_plan_path = plans_dir.join("example.toml");
if !example_plan_path.exists() {
std::fs::write(&example_plan_path, example_plan_content)?;
info!("Created example plan: {}", example_plan_path.display());
}
println!("✅ Goblin project initialized successfully!");
println!("📁 Scripts directory: {}", scripts_dir.display());
println!("📋 Plans directory: {}", plans_dir.display());
println!("⚙️ Configuration: {}", config_path.display());
println!();
println!("Try running:");
println!(" goblin scripts");
println!(" goblin run-plan example_plan --input 'World'");
Ok(())
}
async fn list_scripts(engine: &Engine) -> Result<()> {
let scripts = engine.list_scripts();
if scripts.is_empty() {
println!("No scripts found. Make sure your scripts directory is configured and contains script subdirectories with goblin.toml files.");
return Ok(());
}
println!("Available scripts:");
for script_name in scripts {
if let Some(script) = engine.get_script(&script_name) {
println!(" 📜 {} - {}", script_name, script.command);
if script.has_test() {
println!(" 🧪 Test: {}", script.get_test_command().unwrap_or(""));
}
}
}
Ok(())
}
async fn list_plans(engine: &Engine) -> Result<()> {
let plans = engine.list_plans();
if plans.is_empty() {
println!("No plans found. Make sure your plans directory is configured and contains TOML plan files.");
return Ok(());
}
println!("Available plans:");
for plan_name in plans {
if let Some(plan) = engine.get_plan(&plan_name) {
println!(" 📋 {} ({} steps)", plan_name, plan.steps.len());
for step in &plan.steps {
println!(" 🔹 {} -> {}", step.name, step.function);
}
}
}
Ok(())
}
async fn run_script(engine: &Engine, script_name: &str, args: Vec<String>) -> Result<()> {
info!("Executing script: {} with args: {:?}", script_name, args);
let result = engine.execute_script(script_name, args).await?;
println!("✅ Script '{}' completed successfully", script_name);
println!("⏱️ Duration: {:?}", result.duration);
if !result.stdout.is_empty() {
println!("📤 Output:");
println!("{}", result.stdout);
}
if !result.stderr.is_empty() {
println!("⚠️ Stderr:");
println!("{}", result.stderr);
}
Ok(())
}
async fn run_plan(engine: &Engine, plan_name: &str, input: Option<String>) -> Result<()> {
info!("Executing plan: {} with input: {:?}", plan_name, input);
let context = engine.execute_plan(plan_name, input).await?;
println!("✅ Plan '{}' completed successfully", plan_name);
println!("🆔 Execution ID: {}", context.id);
println!("⏱️ Duration: {:?}", context.elapsed());
println!("📊 Step Results:");
for (step_name, result) in &context.results {
if step_name != "default_input" {
println!(" 🔹 {}: {}", step_name, result);
}
}
Ok(())
}
async fn validate_engine(engine: &Engine) -> Result<()> {
info!("Validating engine configuration...");
match engine.validate_all_plans() {
Ok(_) => {
println!("✅ All plans are valid");
}
Err(e) => {
println!("❌ Validation failed: {}", e);
return Err(e);
}
}
let (scripts_count, plans_count) = engine.get_stats();
println!("📊 Validation complete:");
println!(" 📜 Scripts: {}", scripts_count);
println!(" 📋 Plans: {}", plans_count);
Ok(())
}
async fn show_stats(engine: &Engine) -> Result<()> {
let (scripts_count, plans_count) = engine.get_stats();
println!("📊 Engine Statistics:");
println!(" 📜 Loaded Scripts: {}", scripts_count);
println!(" 📋 Loaded Plans: {}", plans_count);
if scripts_count > 0 {
println!("\n📜 Scripts:");
for script_name in engine.list_scripts() {
if let Some(script) = engine.get_script(&script_name) {
println!(" • {} (timeout: {:?})", script_name, script.timeout);
}
}
}
if plans_count > 0 {
println!("\n📋 Plans:");
for plan_name in engine.list_plans() {
if let Some(plan) = engine.get_plan(&plan_name) {
println!(" • {} ({} steps)", plan_name, plan.steps.len());
}
}
}
Ok(())
}
async fn load_plans_on_pool(pool: &EnginePool, plans_dir: &PathBuf) -> Result<()> {
if !plans_dir.exists() {
error!("Plans directory does not exist: {}", plans_dir.display());
return Ok(());
}
let mut loaded = 0;
for entry in std::fs::read_dir(plans_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "toml") {
match pool.load_plan_on_all(path.clone()).await {
Ok(_) => {
info!("Loaded plan on all instances: {}", path.file_stem().unwrap().to_string_lossy());
loaded += 1;
}
Err(e) => {
error!("Failed to load plan from {}: {}", path.display(), e);
}
}
}
}
info!("Loaded {} plans on all pool instances", loaded);
Ok(())
}
async fn run_script_with_pool(pool: &EnginePool, script_name: &str, args: Vec<String>) -> Result<()> {
info!("Executing script with pool: {} with args: {:?}", script_name, args);
let engine_guard = pool.acquire().await?;
let result = engine_guard.execute_script(script_name, args).await?;
println!("✅ Script '{}' completed successfully (using pool)", script_name);
println!("⏱️ Duration: {:?}", result.duration);
if !result.stdout.is_empty() {
println!("📤 Output:");
println!("{}", result.stdout);
}
if !result.stderr.is_empty() {
println!("⚠️ Stderr:");
println!("{}", result.stderr);
}
Ok(())
}
async fn run_plan_with_pool(pool: &EnginePool, plan_name: &str, input: Option<String>) -> Result<()> {
info!("Executing plan with pool: {} with input: {:?}", plan_name, input);
let mut engine_guard = pool.acquire().await?;
let context = engine_guard.execute_plan_with_reset(plan_name, input).await?;
println!("✅ Plan '{}' completed successfully (using pool)", plan_name);
println!("🆔 Execution ID: {}", context.id);
println!("⏱️ Duration: {:?}", context.elapsed());
println!("📊 Step Results:");
for (step_name, result) in &context.results {
if step_name != "default_input" {
println!(" 🔹 {}: {}", step_name, result);
}
}
Ok(())
}
async fn show_pool_stats(pool: &EnginePool) -> Result<()> {
let pool_stats = pool.get_pool_stats();
println!("📊 Engine Pool Statistics:");
println!(" 🏊 Total Instances: {}", pool_stats.total_instances);
println!(" ✅ Available Instances: {}", pool_stats.available_instances);
println!(" ⚡ Busy Instances: {}", pool_stats.busy_instances);
if let Some(engine_guard) = pool.try_acquire()? {
let (scripts_count, plans_count) = engine_guard.get_stats();
println!("\n📊 Per-Instance Statistics:");
println!(" 📜 Scripts per instance: {}", scripts_count);
println!(" 📋 Plans per instance: {}", plans_count);
if scripts_count > 0 {
println!("\n📜 Scripts:");
for script_name in engine_guard.list_scripts() {
if let Some(script) = engine_guard.get_script(&script_name) {
println!(" • {} (timeout: {:?})", script_name, script.timeout);
}
}
}
if plans_count > 0 {
println!("\n📋 Plans:");
for plan_name in engine_guard.list_plans() {
if let Some(plan) = engine_guard.get_plan(&plan_name) {
println!(" • {} ({} steps)", plan_name, plan.steps.len());
}
}
}
} else {
println!("⚠️ All instances are busy, couldn't get detailed stats");
}
Ok(())
}
async fn generate_sample_config() -> Result<()> {
println!("{}", EngineConfig::sample_config());
Ok(())
}