1use crate::exceptions::AicoError;
2use crate::fs::atomic_write_text;
3use crate::models::{AddonInfo, AddonSource};
4use crate::session::find_session_file;
5use crate::trust::is_project_trusted;
6use std::collections::HashMap;
7use std::env;
8use std::fs;
9use std::os::unix::fs::PermissionsExt;
10use std::path::{Path, PathBuf};
11use std::process::{Command, Stdio};
12
13const PROJECT_ADDONS_DIR: &str = ".aico/addons";
14
15macro_rules! bundle_addon {
17 ($name:expr, $path:expr) => {
18 ($name, include_bytes!($path) as &'static [u8])
19 };
20}
21
22fn get_cache_dir() -> PathBuf {
23 let dir = crate::utils::get_app_cache_dir().join("bundled_addons");
24 let _ = fs::create_dir_all(&dir);
25 dir
26}
27
28fn extract_bundled_addon(name: &str, content: &[u8]) -> Result<PathBuf, AicoError> {
29 let cache_dir = get_cache_dir();
30 let target = cache_dir.join(name);
31
32 if target.exists()
34 && let Ok(existing) = fs::read(&target)
35 && existing == content
36 {
37 return Ok(target);
38 }
39
40 atomic_write_text(&target, &String::from_utf8_lossy(content))?;
41
42 let mut perms = fs::metadata(&target)?.permissions();
43 perms.set_mode(0o755);
44 fs::set_permissions(&target, perms)?;
45
46 Ok(target)
47}
48
49fn run_usage(path: &Path) -> String {
50 let output = Command::new(path)
51 .arg("--usage")
52 .stdout(Stdio::piped())
53 .stderr(Stdio::null())
54 .output();
55
56 match output {
57 Ok(out) if out.status.success() => {
58 let s = String::from_utf8_lossy(&out.stdout);
59 s.lines().next().unwrap_or("").trim().to_string()
60 }
61 _ => String::new(),
62 }
63}
64
65pub fn discover_addons() -> Vec<AddonInfo> {
66 let mut candidates = Vec::new();
67
68 if let Some(session_path) = find_session_file() {
70 let root = session_path.parent().unwrap_or(Path::new("."));
71 let addon_dir = root.join(PROJECT_ADDONS_DIR);
72 if addon_dir.is_dir() {
73 if is_project_trusted(root) {
74 if let Ok(entries) = fs::read_dir(addon_dir) {
75 for entry in entries.flatten() {
76 let path = entry.path();
77 if is_executable_file(&path) {
78 candidates.push((path, AddonSource::Project));
79 }
80 }
81 }
82 } else {
83 eprintln!("[WARN] Project addons found but ignored. Run 'aico trust' to enable.");
84 }
85 }
86 }
87
88 let user_addon_dir = crate::utils::get_app_config_dir().join("addons");
90 if let Ok(entries) = fs::read_dir(user_addon_dir) {
91 for entry in entries.flatten() {
92 let path = entry.path();
93 if is_executable_file(&path) {
94 candidates.push((path, AddonSource::User));
95 }
96 }
97 }
98
99 let bundled = vec![
101 bundle_addon!("commit", "../.aico/addons/commit"),
102 bundle_addon!("manage-context", "../.aico/addons/manage-context"),
103 bundle_addon!("refine", "../.aico/addons/refine"),
104 bundle_addon!("summarize", "../.aico/addons/summarize"),
105 ];
106
107 for (name, content) in bundled {
108 if let Ok(path) = extract_bundled_addon(name, content) {
109 candidates.push((path, AddonSource::Bundled));
110 }
111 }
112
113 let mut results = HashMap::<String, AddonInfo>::new();
115
116 for (path, source) in candidates {
117 let name = path
118 .file_name()
119 .and_then(|n| n.to_str())
120 .unwrap_or("")
121 .to_string();
122 if name.is_empty() {
123 continue;
124 }
125
126 let help_text = run_usage(&path);
127 let info = AddonInfo {
128 name: name.clone(),
129 path,
130 help_text,
131 source,
132 };
133
134 if let Some(existing) = results.get(&name) {
136 if info.source < existing.source {
137 results.insert(name, info);
138 }
139 } else {
140 results.insert(name, info);
141 }
142 }
143
144 let mut list: Vec<AddonInfo> = results.into_values().collect();
145 list.sort_by(|a, b| a.name.cmp(&b.name));
146 list
147}
148
149fn is_executable_file(path: &Path) -> bool {
150 if let Ok(meta) = fs::metadata(path) {
151 if !meta.is_file() {
152 return false;
153 }
154
155 #[cfg(unix)]
156 {
157 use std::os::unix::fs::PermissionsExt;
158 return meta.permissions().mode() & 0o111 != 0;
159 }
160 }
161 false
162}
163
164pub fn execute_addon(addon: &AddonInfo, args: Vec<String>) -> Result<(), AicoError> {
165 use std::os::unix::process::CommandExt;
166
167 let mut cmd = Command::new(&addon.path);
168 cmd.args(args);
169
170 if let Some(session_file) = find_session_file()
171 && let Ok(abs) = fs::canonicalize(session_file)
172 {
173 cmd.env("AICO_SESSION_FILE", abs);
174 }
175
176 if let Ok(current_exe) = env::current_exe()
178 && let Some(bin_dir) = current_exe.parent()
179 {
180 let existing_path = env::var_os("PATH");
181
182 let mut paths = match existing_path {
183 Some(p) => env::split_paths(&p).collect::<Vec<_>>(),
184 None => Vec::new(),
185 };
186
187 paths.insert(0, bin_dir.to_path_buf());
188
189 if let Ok(new_path) = env::join_paths(paths) {
190 cmd.env("PATH", new_path);
191 }
192 }
193
194 let err = cmd.exec();
196 Err(AicoError::Io(err))
197}