use anyhow::Result;
use colored::Colorize;
use std::path::PathBuf;
use crate::bundle::SkillType;
use crate::config::Config;
use crate::source::Source;
use crate::target::Tool;
pub struct InstallRecord {
pub bundle_name: String,
pub source_display: String,
}
pub fn install_bundle(
config: &Config,
bundle_name: &str,
tool: &Tool,
target_dir: &PathBuf,
types: &[SkillType],
) -> Result<Vec<InstallRecord>> {
let (source, bundle) = config.find_bundle(bundle_name)?.ok_or_else(|| {
let mut available = vec![];
for src in config.sources() {
if let Ok(bundles) = src.list_bundles() {
for b in bundles {
available.push(b.name);
}
}
}
anyhow::anyhow!(
"Bundle not found: {}\nAvailable: {}",
bundle_name,
if available.is_empty() {
"(none)".to_string()
} else {
available.join(", ")
}
)
})?;
let source_display = source.display_path();
println!(
"Importing from {} to {}...",
bundle_name.cyan(),
tool.name()
);
let mut total_count = 0;
for skill_type in types {
let files = bundle.files_of_type(*skill_type);
if files.is_empty() {
continue;
}
let mut count = 0;
for file in files {
tool.write_file(target_dir, &bundle.name, file)?;
count += 1;
}
if count > 0 {
let dest_info = tool.dest_info(*skill_type, &bundle.name);
println!(
" {}: {} files -> {}",
skill_type.dir_name(),
count,
dest_info.dimmed()
);
total_count += count;
}
}
if total_count == 0 {
println!("{}", "No files to import.".yellow());
} else {
println!("{}", "Done!".green());
}
Ok(vec![InstallRecord {
bundle_name: bundle.name,
source_display,
}])
}
pub fn install_from_source(
source: &dyn Source,
tool: &Tool,
target_dir: &PathBuf,
types: &[SkillType],
) -> Result<Vec<InstallRecord>> {
let bundles = source.list_bundles()?;
if bundles.is_empty() {
println!("{}", "No bundles found in source.".yellow());
return Ok(vec![]);
}
let source_display = source.display_path();
println!(
"Installing {} bundle(s) from {} to {}...",
bundles.len(),
source_display.cyan(),
tool.name()
);
println!();
let mut total_files = 0;
let mut records = Vec::new();
for bundle in bundles {
let mut bundle_files = 0;
for skill_type in types {
let files = bundle.files_of_type(*skill_type);
for file in files {
tool.write_file(target_dir, &bundle.name, file)?;
bundle_files += 1;
}
}
if bundle_files > 0 {
println!(" {} {} file(s)", bundle.name.cyan(), bundle_files);
total_files += bundle_files;
records.push(InstallRecord {
bundle_name: bundle.name,
source_display: source_display.clone(),
});
}
}
if total_files == 0 {
println!("{}", "No files to import.".yellow());
} else {
println!();
println!("{} {} file(s) installed.", "Done!".green(), total_files);
}
Ok(records)
}
pub fn install_bundle_from_source(
source: &dyn Source,
bundle_name: &str,
tool: &Tool,
target_dir: &PathBuf,
types: &[SkillType],
) -> Result<Vec<InstallRecord>> {
let bundles = source.list_bundles()?;
let source_display = source.display_path();
let bundle = bundles.into_iter().find(|b| b.name == bundle_name).ok_or_else(|| {
anyhow::anyhow!(
"Bundle '{}' not found in source '{}'",
bundle_name,
source_display
)
})?;
println!(
"Importing from {} to {}...",
bundle_name.cyan(),
tool.name()
);
let mut total_count = 0;
for skill_type in types {
let files = bundle.files_of_type(*skill_type);
if files.is_empty() {
continue;
}
let mut count = 0;
for file in files {
tool.write_file(target_dir, &bundle.name, file)?;
count += 1;
}
if count > 0 {
let dest_info = tool.dest_info(*skill_type, &bundle.name);
println!(
" {}: {} files -> {}",
skill_type.dir_name(),
count,
dest_info.dimmed()
);
total_count += count;
}
}
if total_count == 0 {
println!("{}", "No files to import.".yellow());
} else {
println!("{}", "Done!".green());
}
Ok(vec![InstallRecord {
bundle_name: bundle.name,
source_display,
}])
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn setup_test_source() -> (tempfile::TempDir, PathBuf) {
let dir = tempdir().unwrap();
let source_path = dir.path().to_path_buf();
let bundle_dir = source_path.join("test-bundle");
let commands_dir = bundle_dir.join("commands");
fs::create_dir_all(&commands_dir).unwrap();
fs::write(commands_dir.join("commit.md"), "# Commit command").unwrap();
fs::write(commands_dir.join("debug.md"), "# Debug command").unwrap();
let agents_dir = bundle_dir.join("agents");
fs::create_dir_all(&agents_dir).unwrap();
fs::write(agents_dir.join("analyzer.md"), "# Analyzer agent").unwrap();
let skills_dir = bundle_dir.join("skills");
fs::create_dir_all(&skills_dir).unwrap();
fs::write(skills_dir.join("helper.md"), "# Helper skill").unwrap();
(dir, source_path)
}
#[test]
fn test_install_to_claude() {
let (_source_dir, source_path) = setup_test_source();
let target_dir = tempdir().unwrap();
let bundle = crate::bundle::Bundle::from_path(source_path.join("test-bundle")).unwrap();
for cmd in &bundle.commands {
Tool::Claude
.write_file(&target_dir.path().to_path_buf(), "test-bundle", cmd)
.unwrap();
}
assert!(target_dir
.path()
.join(".claude/commands/test-bundle/commit.md")
.exists());
assert!(target_dir
.path()
.join(".claude/commands/test-bundle/debug.md")
.exists());
}
#[test]
fn test_install_to_opencode() {
let (_source_dir, source_path) = setup_test_source();
let target_dir = tempdir().unwrap();
let bundle = crate::bundle::Bundle::from_path(source_path.join("test-bundle")).unwrap();
for skill in &bundle.skills {
Tool::OpenCode
.write_file(&target_dir.path().to_path_buf(), "test-bundle", skill)
.unwrap();
}
assert!(target_dir
.path()
.join(".opencode/skills/test-bundle-helper/SKILL.md")
.exists());
for cmd in &bundle.commands {
Tool::OpenCode
.write_file(&target_dir.path().to_path_buf(), "test-bundle", cmd)
.unwrap();
}
assert!(target_dir
.path()
.join(".opencode/commands/test-bundle-commit.md")
.exists());
}
#[test]
fn test_install_to_cursor() {
let (_source_dir, source_path) = setup_test_source();
let target_dir = tempdir().unwrap();
let bundle = crate::bundle::Bundle::from_path(source_path.join("test-bundle")).unwrap();
for skill in &bundle.skills {
Tool::Cursor
.write_file(&target_dir.path().to_path_buf(), "test-bundle", skill)
.unwrap();
}
assert!(target_dir
.path()
.join(".cursor/skills/test-bundle-helper/SKILL.md")
.exists());
for agent in &bundle.agents {
Tool::Cursor
.write_file(&target_dir.path().to_path_buf(), "test-bundle", agent)
.unwrap();
}
assert!(target_dir
.path()
.join(".cursor/agents/test-bundle-analyzer.md")
.exists());
for command in &bundle.commands {
Tool::Cursor
.write_file(&target_dir.path().to_path_buf(), "test-bundle", command)
.unwrap();
}
assert!(target_dir
.path()
.join(".cursor/commands/test-bundle-commit.md")
.exists());
}
}