use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use clap::{Args, Subcommand};
use mlua_swarm::{
Compiler, LuaInProcessSpawnerFactory, OperatorSpawnerFactory, RustFnInProcessSpawnerFactory,
SpawnerRegistry, SubprocessProcessSpawnerFactory,
};
use mlua_swarm_cli::dsl;
#[derive(Debug, Args)]
pub struct BpArgs {
#[command(subcommand)]
cmd: BpCmd,
}
#[derive(Debug, Subcommand)]
enum BpCmd {
Build(BuildArgs),
}
#[derive(Debug, Args)]
struct BuildArgs {
script: PathBuf,
#[arg(short = 'o', long = "out")]
out: Option<PathBuf>,
#[arg(long)]
register: bool,
#[arg(long)]
server: Option<String>,
}
const DEFAULT_SERVER: &str = "127.0.0.1:7777";
pub async fn run(args: BpArgs) -> Result<()> {
match args.cmd {
BpCmd::Build(build_args) => run_build(build_args).await,
}
}
async fn run_build(args: BuildArgs) -> Result<()> {
let script = std::fs::read_to_string(&args.script)
.with_context(|| format!("reading {}", args.script.display()))?;
let bp_value = dsl::build_bp_from_script(&script)
.with_context(|| format!("building Blueprint from {}", args.script.display()))?;
compile_lint(&bp_value, &args.script)?;
let out_str = serde_json::to_string_pretty(&bp_value)?;
match &args.out {
Some(path) => {
std::fs::write(path, &out_str)
.with_context(|| format!("writing {}", path.display()))?;
}
None => println!("{out_str}"),
}
if args.register {
register(&bp_value, args.server.as_deref()).await?;
}
Ok(())
}
fn compile_lint(bp_value: &serde_json::Value, script_path: &Path) -> Result<()> {
let base = script_path.parent().unwrap_or_else(|| Path::new("."));
let default_kind = mlua_swarm::blueprint::loader::pre_read_default_agent_kind(bp_value);
let expanded = match mlua_swarm::expand_file_refs(bp_value.clone(), base, default_kind) {
Ok(v) => v,
Err(e) => {
eprintln!(
"compile lint: skipped — could not resolve $file/$agent_md refs relative to \
{} ({e}). Only the static DSL shape was validated; the server resolves these \
refs against its own --blueprint-ref-base at register time.",
base.display()
);
return Ok(());
}
};
let bp: mlua_swarm::Blueprint = serde_json::from_value(expanded).map_err(|e| {
anyhow!("compile lint: blueprint shape invalid after $agent_md expansion: {e}")
})?;
let registry = lint_registry(&bp);
Compiler::new(registry)
.compile(&bp)
.map_err(|e| anyhow!("compile lint FAILED: {e}"))?;
eprintln!(
"compile lint: OK ({} agent(s), {} operator(s) checked)",
bp.agents.len(),
bp.operators.len()
);
Ok(())
}
struct LintStubOperator;
#[async_trait::async_trait]
impl mlua_swarm::Operator for LintStubOperator {
async fn execute(
&self,
_ctx: &mlua_swarm::Ctx,
_system: Option<String>,
_prompt: serde_json::Value,
_worker: Option<mlua_swarm::WorkerBinding>,
_worker_token: mlua_swarm::CapToken,
) -> Result<mlua_swarm::WorkerResult, mlua_swarm::WorkerError> {
Ok(mlua_swarm::WorkerResult {
value: serde_json::Value::Null,
ok: true,
})
}
fn requires_worker_binding(&self) -> bool {
false
}
}
fn lint_registry(bp: &mlua_swarm::Blueprint) -> SpawnerRegistry {
let mut reg = SpawnerRegistry::new();
reg.register::<SubprocessProcessSpawnerFactory>(Arc::new(SubprocessProcessSpawnerFactory));
reg.register::<RustFnInProcessSpawnerFactory>(Arc::new(RustFnInProcessSpawnerFactory::new()));
reg.register::<LuaInProcessSpawnerFactory>(Arc::new(LuaInProcessSpawnerFactory::new()));
let op_factory = OperatorSpawnerFactory::new();
for op in &bp.operators {
op_factory.register_operator(op.name.clone(), Arc::new(LintStubOperator));
}
reg.register::<OperatorSpawnerFactory>(Arc::new(op_factory));
reg
}
async fn register(bp_value: &serde_json::Value, server: Option<&str>) -> Result<()> {
let server = server.unwrap_or(DEFAULT_SERVER);
let id = bp_value
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("register: Blueprint JSON has no top-level 'id' string field"))?;
let url = format!("http://{server}/v1/blueprints/{id}");
let client = reqwest::Client::new();
let resp = client
.post(&url)
.json(bp_value)
.send()
.await
.map_err(|e| anyhow!("register: request to {url} failed: {e}"))?;
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
if !status.is_success() {
return Err(anyhow!("register: {url} returned HTTP {status}: {body}"));
}
eprintln!("register: {url} -> HTTP {status}: {body}");
Ok(())
}