use crate::config::{ensure_dir, global_source_store};
use crate::error::{Result, SkillcError};
use crate::verbose;
use std::fs;
pub struct InitOptions {
pub name: Option<String>,
pub global: bool,
}
pub fn init(options: InitOptions) -> Result<String> {
match options.name {
None => init_project(),
Some(name) => init_skill(&name, options.global),
}
}
fn init_project() -> Result<String> {
let cwd = std::env::current_dir()
.map_err(|e| SkillcError::Internal(format!("Failed to get current directory: {}", e)))?;
let skillc_dir = cwd.join(".skillc");
let skills_dir = skillc_dir.join("skills");
verbose!("Creating project structure in {:?}", cwd);
ensure_dir(&skillc_dir)
.map_err(|e| SkillcError::Internal(format!("Failed to create .skillc/: {}", e)))?;
ensure_dir(&skills_dir)
.map_err(|e| SkillcError::Internal(format!("Failed to create .skillc/skills/: {}", e)))?;
verbose!("Created .skillc/ and .skillc/skills/");
Ok(format!("Initialized skillc project in {}", cwd.display()))
}
fn init_skill(name: &str, global: bool) -> Result<String> {
let target_dir = if global {
global_source_store()?.join(name)
} else {
let cwd = std::env::current_dir().map_err(|e| {
SkillcError::Internal(format!("Failed to get current directory: {}", e))
})?;
let skillc_dir = cwd.join(".skillc");
let skills_dir = skillc_dir.join("skills");
if !skillc_dir.exists() {
verbose!("Creating .skillc/ for project-local skill");
ensure_dir(&skillc_dir)
.map_err(|e| SkillcError::Internal(format!("Failed to create .skillc/: {}", e)))?;
}
if !skills_dir.exists() {
ensure_dir(&skills_dir).map_err(|e| {
SkillcError::Internal(format!("Failed to create .skillc/skills/: {}", e))
})?;
}
skills_dir.join(name)
};
let skill_md = target_dir.join("SKILL.md");
verbose!("Creating skill '{}' at {:?}", name, target_dir);
if skill_md.exists() {
return Err(SkillcError::SkillAlreadyExists(name.to_string()));
}
ensure_dir(&target_dir)
.map_err(|e| SkillcError::Internal(format!("Failed to create skill directory: {}", e)))?;
let title_cased = title_case(name);
let content = format!(
r#"---
name: {}
description: "TODO: Add skill description"
---
# {}
"#,
name, title_cased
);
fs::write(&skill_md, content)
.map_err(|e| SkillcError::Internal(format!("Failed to write SKILL.md: {}", e)))?;
verbose!("Created SKILL.md");
let location = if global { "global" } else { "project" };
Ok(format!(
"Created {} skill '{}' at {}",
location,
name,
target_dir.display()
))
}
fn title_case(s: &str) -> String {
s.split(['-', '_'])
.filter(|word| !word.is_empty())
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_title_case() {
assert_eq!(title_case("cuda"), "Cuda");
assert_eq!(title_case("my-skill"), "My Skill");
assert_eq!(title_case("my_skill"), "My Skill");
assert_eq!(title_case("my-cool_skill"), "My Cool Skill");
assert_eq!(title_case("CAPS"), "CAPS");
}
#[test]
fn test_title_case_edge_cases() {
assert_eq!(title_case(""), "");
assert_eq!(title_case("-"), "");
assert_eq!(title_case("--"), "");
assert_eq!(title_case("a"), "A");
assert_eq!(title_case("a-b-c"), "A B C");
}
}