use agileplus_trace_validator::validate_intent_graph;
use anyhow::{Context, Result};
use clap::Args;
use serde_json;
use std::io::{self, Read, Write};
pub mod generate;
pub mod slug;
pub use generate::generate_intent_graph;
#[derive(Debug, Args)]
#[command(name = "intent")]
pub struct IntentCommand {
#[arg(short, long)]
prompt: Option<String>,
#[arg(short, long)]
output: Option<String>,
#[arg(long, default_value = "agileplus-cli")]
source_system: String,
#[arg(long, default_value = "agileplus-cli")]
agent_id: String,
#[arg(long, default_value = "true")]
pretty: bool,
#[arg(long, default_value = "true")]
validate: bool,
}
impl IntentCommand {
pub fn run(&self) -> Result<()> {
let prompt = match &self.prompt {
Some(p) => p.clone(),
None => {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
buffer.trim().to_string()
}
};
if prompt.is_empty() {
return Err(anyhow::anyhow!(
"Prompt is empty. Provide --prompt or pipe text to stdin."
));
}
let graph = generate_intent_graph(&prompt, &self.source_system, &self.agent_id)?;
if self.validate {
if let Err(errs) = validate_intent_graph(&graph) {
let messages: Vec<String> = errs.iter().map(|e| e.to_string()).collect();
return Err(anyhow::anyhow!(
"Generated graph failed validation: {}",
messages.join("; ")
));
}
}
let json = if self.pretty {
serde_json::to_string_pretty(&graph)?
} else {
serde_json::to_string(&graph)?
};
match &self.output {
Some(path) => {
std::fs::write(path, &json)
.with_context(|| format!("Failed to write to {}", path))?;
eprintln!("Intent graph written to {}", path);
}
None => {
io::stdout().write_all(json.as_bytes())?;
io::stdout().write_all(b"\n")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::generate::generate_intent_graph;
use super::slug::{decompose_features, extract_title, slugify};
use agileplus_domain::intent_graph::NodeType;
#[test]
fn test_generate_intent_graph() {
let graph = generate_intent_graph(
"Build an OAuth2 authentication system with login and session management.",
"test",
"test-agent",
)
.unwrap();
assert!(!graph.nodes.is_empty());
assert!(!graph.edges.is_empty());
assert!(graph.metadata.version == "1.0.0");
let intent_count = graph
.nodes
.iter()
.filter(|n| n.node_type == NodeType::Intent)
.count();
let plan_count = graph
.nodes
.iter()
.filter(|n| n.node_type == NodeType::Plan)
.count();
let feature_count = graph
.nodes
.iter()
.filter(|n| n.node_type == NodeType::Feature)
.count();
assert_eq!(intent_count, 1);
assert_eq!(plan_count, 1);
assert!(feature_count >= 1);
}
#[test]
fn test_extract_title() {
assert_eq!(extract_title("Hello world"), "Hello world");
}
#[test]
fn test_decompose_features() {
let feats = decompose_features("Build an OAuth2 system with database and cache");
assert!(feats.iter().any(|f| f.contains("oauth2") || f.contains("OAuth2")));
assert!(feats.iter().any(|f| f.contains("database") || f.contains("Database")));
assert!(feats.iter().any(|f| f.contains("cache") || f.contains("Cache")));
}
#[test]
fn test_slugify() {
assert_eq!(slugify("Hello World"), "hello-world");
}
}