1use aether_project::{AgentEntry, Settings};
2use crossterm::style::Stylize;
3use std::fs;
4
5use crate::agent::ListArgs;
6use crate::error::CliError;
7
8pub fn run_list(args: ListArgs) -> Result<(), CliError> {
9 let project_root = args.path.canonicalize().unwrap_or(args.path);
10 let settings_path = project_root.join(".aether/settings.json");
11
12 let content = match fs::read_to_string(&settings_path) {
13 Ok(c) => c,
14 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
15 println!("No agents found. Run `aether agent new` to create one.");
16 return Ok(());
17 }
18 Err(e) => return Err(CliError::IoError(e)),
19 };
20
21 let settings: Settings =
22 serde_json::from_str(&content).map_err(|e| CliError::AgentError(format!("Failed to parse settings: {e}")))?;
23
24 if settings.agents.is_empty() {
25 println!("No agents found. Run `aether agent new` to create one.");
26 return Ok(());
27 }
28
29 let mut sorted: Vec<&AgentEntry> = settings.agents.iter().collect();
30 sorted.sort_by(|a, b| a.name.cmp(&b.name));
31
32 for (i, agent) in sorted.iter().enumerate() {
33 if i > 0 {
34 println!();
35 }
36 print_agent(agent, &settings);
37 }
38
39 Ok(())
40}
41
42fn print_agent(agent: &AgentEntry, settings: &Settings) {
43 println!("{}", agent.name.as_str().bold().cyan());
44 println!(" {} {}", "model:".dim(), agent.model);
45
46 let reasoning = agent.reasoning_effort.as_ref().map_or("none".to_string(), std::string::ToString::to_string);
47 println!(" {} {reasoning}", "reasoning:".dim());
48
49 println!(" {} {}", "description:".dim(), agent.description);
50
51 let mut surfaces = Vec::new();
52 if agent.user_invocable {
53 surfaces.push("user");
54 }
55 if agent.agent_invocable {
56 surfaces.push("agent");
57 }
58 println!(" {} {}", "invocable:".dim(), surfaces.join(", "));
59
60 let effective_prompts = if agent.prompts.is_empty() { &settings.prompts } else { &agent.prompts };
61 if !effective_prompts.is_empty() {
62 println!(
63 " {} {}",
64 "prompts:".dim(),
65 effective_prompts.iter().map(std::string::String::as_str).collect::<Vec<_>>().join(", ")
66 );
67 }
68
69 let effective_mcp = if agent.mcp_servers.is_empty() { &settings.mcp_servers } else { &agent.mcp_servers };
70 if !effective_mcp.is_empty() {
71 println!(
72 " {} {}",
73 "mcp servers:".dim(),
74 effective_mcp.iter().map(std::string::String::as_str).collect::<Vec<_>>().join(", ")
75 );
76 }
77
78 if !agent.tools.allow.is_empty() {
79 println!(" {} {}", "tools allow:".dim(), agent.tools.allow.join(", "));
80 }
81 if !agent.tools.deny.is_empty() {
82 println!(" {} {}", "tools deny:".dim(), agent.tools.deny.join(", "));
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::agent::new_agent_wizard::{DraftAgentEntry, scaffold};
90
91 #[test]
92 fn list_empty_project() {
93 let dir = tempfile::tempdir().unwrap();
94 let args = super::super::ListArgs { path: dir.path().to_path_buf() };
95 run_list(args).unwrap();
96 }
97
98 #[test]
99 fn list_project_with_agents() {
100 let dir = tempfile::tempdir().unwrap();
101 scaffold(dir.path(), &test_draft()).unwrap();
102 let args = super::super::ListArgs { path: dir.path().to_path_buf() };
103 run_list(args).unwrap();
104 }
105
106 fn test_draft() -> DraftAgentEntry {
107 use crate::agent::new_agent_wizard::build_system_md;
108
109 let mut draft = DraftAgentEntry {
110 entry: AgentEntry {
111 name: "Coder".to_string(),
112 description: "A coding agent".to_string(),
113 user_invocable: true,
114 agent_invocable: true,
115 model: "anthropic:claude-sonnet-4-5".to_string(),
116 prompts: vec!["AGENTS.md".to_string()],
117 mcp_servers: vec!["coding".to_string()],
118 ..AgentEntry::default()
119 },
120 system_md_content: String::new(),
121 system_md_edited: false,
122 workspace_mcp_configs: vec![],
123 };
124 draft.system_md_content = build_system_md(&draft);
125 draft
126 }
127}