1use crate::errors::{CoreError, CoreResult};
8use ito_templates::{
9 commands_files, get_adapter_file, get_command_file, get_skill_file, skills_files,
10};
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone)]
14pub struct FileManifest {
16 pub source: String,
18 pub dest: PathBuf,
20 pub asset_type: AssetType,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum AssetType {
27 Skill,
29 Adapter,
31 Command,
33}
34
35fn ito_skills_manifests(skills_dir: &Path) -> Vec<FileManifest> {
41 let mut manifests = Vec::new();
42
43 for file in skills_files() {
45 let rel_path = file.relative_path;
46 let parts: Vec<&str> = rel_path.split('/').collect();
48 if parts.is_empty() {
49 continue;
50 }
51 let skill_name = parts[0];
52
53 let dest_skill_name = if skill_name.starts_with("ito") {
55 skill_name.to_string()
56 } else {
57 format!("ito-{}", skill_name)
58 };
59
60 let rest = if parts.len() > 1 {
61 parts[1..].join("/")
62 } else {
63 rel_path.to_string()
64 };
65 let dest = skills_dir.join(format!("{}/{}", dest_skill_name, rest));
66
67 manifests.push(FileManifest {
68 source: rel_path.to_string(),
69 dest,
70 asset_type: AssetType::Skill,
71 });
72 }
73
74 manifests
75}
76
77fn ito_commands_manifests(commands_dir: &Path) -> Vec<FileManifest> {
80 let mut manifests = Vec::new();
81
82 for file in commands_files() {
83 let rel_path = file.relative_path;
84 manifests.push(FileManifest {
85 source: rel_path.to_string(),
86 dest: commands_dir.join(rel_path),
87 asset_type: AssetType::Command,
88 });
89 }
90
91 manifests
92}
93
94pub fn opencode_manifests(config_dir: &Path) -> Vec<FileManifest> {
100 let mut out = Vec::new();
101
102 out.push(FileManifest {
103 source: "opencode/ito-skills.js".to_string(),
104 dest: config_dir.join("plugins").join("ito-skills.js"),
105 asset_type: AssetType::Adapter,
106 });
107
108 let skills_dir = config_dir.join("skills");
110 out.extend(ito_skills_manifests(&skills_dir));
111
112 let commands_dir = config_dir.join("commands");
114 out.extend(ito_commands_manifests(&commands_dir));
115
116 out
117}
118
119pub fn claude_manifests(project_root: &Path) -> Vec<FileManifest> {
121 let mut out = vec![
122 FileManifest {
123 source: "claude/session-start.sh".to_string(),
124 dest: project_root.join(".claude").join("session-start.sh"),
125 asset_type: AssetType::Adapter,
126 },
127 FileManifest {
128 source: "claude/hooks/ito-audit.sh".to_string(),
129 dest: project_root
130 .join(".claude")
131 .join("hooks")
132 .join("ito-audit.sh"),
133 asset_type: AssetType::Adapter,
134 },
135 ];
136
137 let skills_dir = project_root.join(".claude").join("skills");
139 out.extend(ito_skills_manifests(&skills_dir));
140
141 let commands_dir = project_root.join(".claude").join("commands");
143 out.extend(ito_commands_manifests(&commands_dir));
144
145 out
146}
147
148pub fn codex_manifests(project_root: &Path) -> Vec<FileManifest> {
150 let mut out = vec![FileManifest {
151 source: "codex/ito-skills-bootstrap.md".to_string(),
152 dest: project_root
153 .join(".codex")
154 .join("instructions")
155 .join("ito-skills-bootstrap.md"),
156 asset_type: AssetType::Adapter,
157 }];
158
159 let skills_dir = project_root.join(".codex").join("skills");
161 out.extend(ito_skills_manifests(&skills_dir));
162
163 let commands_dir = project_root.join(".codex").join("prompts");
165 out.extend(ito_commands_manifests(&commands_dir));
166
167 out
168}
169
170pub fn github_manifests(project_root: &Path) -> Vec<FileManifest> {
172 let skills_dir = project_root.join(".github").join("skills");
174 let mut out = ito_skills_manifests(&skills_dir);
175
176 let prompts_dir = project_root.join(".github").join("prompts");
179 for file in commands_files() {
180 let rel_path = file.relative_path;
181 let dest_name = if let Some(stripped) = rel_path.strip_suffix(".md") {
183 format!("{stripped}.prompt.md")
184 } else {
185 rel_path.to_string()
186 };
187 out.push(FileManifest {
188 source: rel_path.to_string(),
189 dest: prompts_dir.join(dest_name),
190 asset_type: AssetType::Command,
191 });
192 }
193
194 out
195}
196
197pub fn install_manifests(
204 manifests: &[FileManifest],
205 worktree_ctx: Option<&ito_templates::project_templates::WorktreeTemplateContext>,
206) -> CoreResult<()> {
207 use ito_templates::project_templates::{WorktreeTemplateContext, render_project_template};
208
209 let default_ctx = WorktreeTemplateContext::default();
210 let ctx = worktree_ctx.unwrap_or(&default_ctx);
211
212 for manifest in manifests {
213 let raw_bytes = match manifest.asset_type {
214 AssetType::Skill => get_skill_file(&manifest.source).ok_or_else(|| {
215 CoreError::NotFound(format!(
216 "Skill file not found in embedded assets: {}",
217 manifest.source
218 ))
219 })?,
220 AssetType::Adapter => get_adapter_file(&manifest.source).ok_or_else(|| {
221 CoreError::NotFound(format!(
222 "Adapter file not found in embedded assets: {}",
223 manifest.source
224 ))
225 })?,
226 AssetType::Command => get_command_file(&manifest.source).ok_or_else(|| {
227 CoreError::NotFound(format!(
228 "Command file not found in embedded assets: {}",
229 manifest.source
230 ))
231 })?,
232 };
233
234 let mut should_render_skill = false;
238 if manifest.asset_type == AssetType::Skill {
239 for line in raw_bytes.split(|b| *b == b'\n') {
240 let Ok(line) = std::str::from_utf8(line) else {
241 continue;
242 };
243 if skill_line_uses_worktree_template_syntax(line) {
244 should_render_skill = true;
245 break;
246 }
247 }
248 }
249
250 let bytes = if should_render_skill {
251 render_project_template(raw_bytes, ctx).map_err(|e| {
252 CoreError::Validation(format!(
253 "Failed to render skill template {}: {}",
254 manifest.source, e
255 ))
256 })?
257 } else {
258 raw_bytes.to_vec()
259 };
260
261 if let Some(parent) = manifest.dest.parent() {
262 ito_common::io::create_dir_all_std(parent).map_err(|e| {
263 CoreError::io(format!("creating directory {}", parent.display()), e)
264 })?;
265 }
266 ito_common::io::write_std(&manifest.dest, &bytes)
267 .map_err(|e| CoreError::io(format!("writing {}", manifest.dest.display()), e))?;
268 }
269 Ok(())
270}
271
272fn skill_line_uses_worktree_template_syntax(line: &str) -> bool {
273 if line.contains("{%") {
274 return true;
275 }
276
277 const WORKTREE_VARS: &[&str] = &[
279 "{{ enabled",
280 "{{ strategy",
281 "{{ layout_dir_name",
282 "{{ integration_mode",
283 "{{ default_branch",
284 ];
285
286 for var in WORKTREE_VARS {
287 if line.contains(var) {
288 return true;
289 }
290 }
291 false
292}