Skip to main content

skm_cli/commands/
validate.rs

1//! Validate command: check SKILL.md files.
2
3use clap::Args;
4use std::path::PathBuf;
5
6use skm_core::SkillParser;
7
8#[derive(Args)]
9pub struct ValidateArgs {
10    /// Paths to validate (directories or SKILL.md files)
11    paths: Vec<PathBuf>,
12
13    /// Strict mode (fail on warnings)
14    #[arg(long)]
15    strict: bool,
16
17    /// Output format
18    #[arg(long, default_value = "text")]
19    format: String,
20}
21
22pub async fn validate(args: ValidateArgs) -> anyhow::Result<()> {
23    let parser = if args.strict {
24        SkillParser::strict()
25    } else {
26        SkillParser::new()
27    };
28
29    let mut total = 0;
30    let mut passed = 0;
31    let mut errors = Vec::new();
32
33    for path in &args.paths {
34        let skill_files = find_skill_files(path);
35
36        for file in skill_files {
37            total += 1;
38
39            match parser.parse_file(&file) {
40                Ok(skill) => {
41                    passed += 1;
42                    if args.format == "text" {
43                        println!("✓ {}: {} ({})", file.display(), skill.name, skill.description);
44                    }
45                }
46                Err(e) => {
47                    errors.push((file.clone(), e.to_string()));
48                    if args.format == "text" {
49                        eprintln!("✗ {}: {}", file.display(), e);
50                    }
51                }
52            }
53        }
54    }
55
56    if args.format == "json" {
57        let result = serde_json::json!({
58            "total": total,
59            "passed": passed,
60            "failed": total - passed,
61            "errors": errors.iter().map(|(p, e)| {
62                serde_json::json!({
63                    "file": p.display().to_string(),
64                    "error": e
65                })
66            }).collect::<Vec<_>>()
67        });
68        println!("{}", serde_json::to_string_pretty(&result)?);
69    } else {
70        println!("\nValidation complete: {}/{} passed", passed, total);
71    }
72
73    if passed < total {
74        std::process::exit(1);
75    }
76
77    Ok(())
78}
79
80fn find_skill_files(path: &PathBuf) -> Vec<PathBuf> {
81    let mut files = Vec::new();
82
83    if path.is_file() && path.file_name().map(|n| n == "SKILL.md").unwrap_or(false) {
84        files.push(path.clone());
85    } else if path.is_dir() {
86        // Recursively find SKILL.md files
87        if let Ok(entries) = std::fs::read_dir(path) {
88            for entry in entries.flatten() {
89                let entry_path = entry.path();
90                if entry_path.is_dir() {
91                    files.extend(find_skill_files(&entry_path));
92                } else if entry_path.file_name().map(|n| n == "SKILL.md").unwrap_or(false) {
93                    files.push(entry_path);
94                }
95            }
96        }
97    }
98
99    files
100}