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![FileManifest {
122 source: "claude/session-start.sh".to_string(),
123 dest: project_root.join(".claude").join("session-start.sh"),
124 asset_type: AssetType::Adapter,
125 }];
126
127 let skills_dir = project_root.join(".claude").join("skills");
129 out.extend(ito_skills_manifests(&skills_dir));
130
131 let commands_dir = project_root.join(".claude").join("commands");
133 out.extend(ito_commands_manifests(&commands_dir));
134
135 out
136}
137
138pub fn codex_manifests(project_root: &Path) -> Vec<FileManifest> {
140 let mut out = vec![FileManifest {
141 source: "codex/ito-skills-bootstrap.md".to_string(),
142 dest: project_root
143 .join(".codex")
144 .join("instructions")
145 .join("ito-skills-bootstrap.md"),
146 asset_type: AssetType::Adapter,
147 }];
148
149 let skills_dir = project_root.join(".codex").join("skills");
151 out.extend(ito_skills_manifests(&skills_dir));
152
153 let commands_dir = project_root.join(".codex").join("prompts");
155 out.extend(ito_commands_manifests(&commands_dir));
156
157 out
158}
159
160pub fn github_manifests(project_root: &Path) -> Vec<FileManifest> {
162 let skills_dir = project_root.join(".github").join("skills");
164 let mut out = ito_skills_manifests(&skills_dir);
165
166 let prompts_dir = project_root.join(".github").join("prompts");
169 for file in commands_files() {
170 let rel_path = file.relative_path;
171 let dest_name = if let Some(stripped) = rel_path.strip_suffix(".md") {
173 format!("{stripped}.prompt.md")
174 } else {
175 rel_path.to_string()
176 };
177 out.push(FileManifest {
178 source: rel_path.to_string(),
179 dest: prompts_dir.join(dest_name),
180 asset_type: AssetType::Command,
181 });
182 }
183
184 out
185}
186
187pub fn install_manifests(
194 manifests: &[FileManifest],
195 worktree_ctx: Option<&ito_templates::project_templates::WorktreeTemplateContext>,
196) -> CoreResult<()> {
197 use ito_templates::project_templates::{WorktreeTemplateContext, render_project_template};
198
199 let default_ctx = WorktreeTemplateContext::default();
200 let ctx = worktree_ctx.unwrap_or(&default_ctx);
201
202 for manifest in manifests {
203 let raw_bytes = match manifest.asset_type {
204 AssetType::Skill => get_skill_file(&manifest.source).ok_or_else(|| {
205 CoreError::NotFound(format!(
206 "Skill file not found in embedded assets: {}",
207 manifest.source
208 ))
209 })?,
210 AssetType::Adapter => get_adapter_file(&manifest.source).ok_or_else(|| {
211 CoreError::NotFound(format!(
212 "Adapter file not found in embedded assets: {}",
213 manifest.source
214 ))
215 })?,
216 AssetType::Command => get_command_file(&manifest.source).ok_or_else(|| {
217 CoreError::NotFound(format!(
218 "Command file not found in embedded assets: {}",
219 manifest.source
220 ))
221 })?,
222 };
223
224 let is_worktree_skill = manifest.source.starts_with("using-git-worktrees/");
229 let bytes = if manifest.asset_type == AssetType::Skill && is_worktree_skill {
230 render_project_template(raw_bytes, ctx).map_err(|e| {
231 CoreError::Validation(format!(
232 "Failed to render skill template {}: {}",
233 manifest.source, e
234 ))
235 })?
236 } else {
237 raw_bytes.to_vec()
238 };
239
240 if let Some(parent) = manifest.dest.parent() {
241 ito_common::io::create_dir_all_std(parent).map_err(|e| {
242 CoreError::io(format!("creating directory {}", parent.display()), e)
243 })?;
244 }
245 ito_common::io::write_std(&manifest.dest, &bytes)
246 .map_err(|e| CoreError::io(format!("writing {}", manifest.dest.display()), e))?;
247 }
248 Ok(())
249}