use anyhow::{Context, Result};
use std::process::Command;
#[derive(Debug)]
pub enum AngrealError {
NotInstalled,
NotInProject,
ExecutionFailed(String),
}
impl std::fmt::Display for AngrealError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AngrealError::NotInstalled => write!(
f,
"The 'angreal' command is not available. Please install angreal first."
),
AngrealError::NotInProject => write!(
f,
"Not in an angreal project. Please run this command from within an angreal project directory."
),
AngrealError::ExecutionFailed(msg) => write!(f, "Angreal execution failed: {}", msg),
}
}
}
impl std::error::Error for AngrealError {}
pub async fn get_angreal_tree(format: &str) -> Result<String> {
validate_format(format)?;
let args = match format {
"json" => vec!["--json".to_string()],
"human" => vec![],
_ => unreachable!("Format already validated"),
};
run_angreal_command("tree", &args).await
}
fn validate_format(format: &str) -> Result<()> {
match format {
"json" | "human" => Ok(()),
_ => anyhow::bail!("Invalid format '{}'. Must be 'json' or 'human'", format),
}
}
pub async fn check_angreal_available() -> Result<bool> {
match Command::new("angreal").arg("--version").output() {
Ok(output) => Ok(output.status.success()),
Err(_) => Ok(false),
}
}
pub async fn check_angreal_project_status() -> Result<String> {
let mut status_parts = Vec::new();
let mut command_tree = None;
let angreal_available = match Command::new("angreal").arg("--version").output() {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
status_parts.push(format!(" Angreal is installed: {}", version));
true
}
_ => {
status_parts.push(" Angreal is not installed or not available in PATH".to_string());
status_parts.push(" Install angreal first: pip install angreal".to_string());
false
}
};
let angreal_folder_exists = std::path::Path::new(".angreal").exists();
if angreal_folder_exists {
status_parts.push("✓ Found .angreal/ directory - this is an angreal project".to_string());
} else {
status_parts
.push("✗ No .angreal/ directory found - this is not an angreal project".to_string());
}
if angreal_available && angreal_folder_exists {
match Command::new("angreal").arg("tree").arg("--json").output() {
Ok(output) if output.status.success() => {
let tree_output = String::from_utf8_lossy(&output.stdout);
if tree_output.trim().is_empty() || tree_output.contains("No commands") {
status_parts.push(
" Project appears to be initialized but has no commands defined"
.to_string(),
);
status_parts
.push(" You may need to add tasks in the .angreal/ directory".to_string());
} else {
status_parts.push(
"✓ Project is properly initialized with available commands".to_string(),
);
command_tree = Some(tree_output.to_string());
}
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("No angreal.toml") || stderr.contains("not an angreal project") {
status_parts.push(
"✗ Project folder exists but may not be properly initialized".to_string(),
);
status_parts
.push(" Try running 'angreal init' to initialize the project".to_string());
} else {
status_parts.push(format!("âš Angreal tree command failed: {}", stderr.trim()));
}
}
Err(e) => {
status_parts.push(format!("âš Could not check project status: {}", e));
}
}
} else if !angreal_available && angreal_folder_exists {
status_parts.push(" Install angreal to work with this project".to_string());
} else if angreal_available && !angreal_folder_exists {
status_parts.push(" This directory is not an angreal project".to_string());
status_parts
.push(" To create an angreal project: angreal init <template-url>".to_string());
}
if let Ok(current_dir) = std::env::current_dir() {
status_parts.push(format!("\nCurrent directory: {}", current_dir.display()));
}
let mut result = status_parts.join("\n");
if let Some(tree) = command_tree {
result.push_str("\n\nAvailable Commands:\n");
result.push_str(&tree);
}
Ok(result)
}
pub async fn run_angreal_command(command: &str, args: &[String]) -> Result<String> {
validate_angreal_command(command)?;
let all_args = parse_command_and_args(command, args)?;
eprintln!("Executing: angreal {}", all_args.join(" "));
let output = Command::new("angreal")
.args(&all_args)
.output()
.context("Failed to execute angreal command")?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.trim().is_empty() {
Ok(stdout.to_string())
} else {
Ok(format!("{}\n\nStderr:\n{}", stdout, stderr))
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
if stderr.contains("command not found") || output.status.code() == Some(127) {
return Err(AngrealError::NotInstalled.into());
}
if stderr.contains("No angreal.toml") || stderr.contains("not an angreal project") {
return Err(AngrealError::NotInProject.into());
}
let error_output = if stdout.trim().is_empty() {
stderr.to_string()
} else {
format!("Output:\n{}\n\nError:\n{}", stdout, stderr)
};
Err(AngrealError::ExecutionFailed(error_output).into())
}
}
fn validate_angreal_command(command: &str) -> Result<()> {
let parts: Vec<&str> = command.split_whitespace().collect();
for part in parts {
if !part.chars().all(|c| {
c.is_alphanumeric()
|| c == '-'
|| c == '_'
|| c == '.' || c == '/' }) {
return Err(anyhow::anyhow!(
"Invalid command component '{}': contains disallowed characters",
part
));
}
if part.contains("&&") || part.contains("||") || part.contains(";") || part.contains("|") {
return Err(anyhow::anyhow!(
"Command injection attempt detected in '{}'",
part
));
}
}
Ok(())
}
fn parse_command_and_args(command: &str, args: &[String]) -> Result<Vec<String>> {
let mut all_args = Vec::new();
let command_parts: Vec<&str> = command.split_whitespace().collect();
all_args.extend(command_parts.iter().map(|s| s.to_string()));
all_args.extend(args.iter().cloned());
for arg in &all_args {
if arg.len() > 1000 {
return Err(anyhow::anyhow!("Argument too long: {}", arg.len()));
}
}
Ok(all_args)
}