Skip to main content

fastskill_core/core/
version_bump.rs

1//! Semantic version bumping for skills
2
3use crate::core::service::ServiceError;
4use semver::Version;
5use std::path::Path;
6
7/// Version bump type
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum BumpType {
10    Major,
11    Minor,
12    Patch,
13}
14
15/// Parse a version string into a SemVer version
16pub fn parse_version(version: &str) -> Result<Version, ServiceError> {
17    // Remove 'v' prefix if present
18    let version_str = version.strip_prefix('v').unwrap_or(version);
19
20    Version::parse(version_str)
21        .map_err(|e| ServiceError::Custom(format!("Invalid version '{}': {}", version, e)))
22}
23
24/// Bump a version based on bump type
25pub fn bump_version(version: &Version, bump_type: BumpType) -> Version {
26    match bump_type {
27        BumpType::Major => Version {
28            major: version.major + 1,
29            minor: 0,
30            patch: 0,
31            pre: semver::Prerelease::EMPTY,
32            build: semver::BuildMetadata::EMPTY,
33        },
34        BumpType::Minor => Version {
35            major: version.major,
36            minor: version.minor + 1,
37            patch: 0,
38            pre: semver::Prerelease::EMPTY,
39            build: semver::BuildMetadata::EMPTY,
40        },
41        BumpType::Patch => Version {
42            major: version.major,
43            minor: version.minor,
44            patch: version.patch + 1,
45            pre: semver::Prerelease::EMPTY,
46            build: semver::BuildMetadata::EMPTY,
47        },
48    }
49}
50
51/// Update skill-project.toml with new version
52pub fn update_skill_version(skill_path: &Path, new_version: &str) -> Result<(), ServiceError> {
53    let skill_project_path = skill_path.join("skill-project.toml");
54
55    if !skill_project_path.exists() {
56        // Create new skill-project.toml with [metadata] section
57        #[derive(serde::Serialize)]
58        struct SkillProjectToml {
59            metadata: SkillProjectMetadata,
60        }
61
62        #[derive(serde::Serialize)]
63        struct SkillProjectMetadata {
64            version: String,
65        }
66
67        let new_skill_project = SkillProjectToml {
68            metadata: SkillProjectMetadata {
69                version: new_version.to_string(),
70            },
71        };
72        let content = toml::to_string_pretty(&new_skill_project).map_err(|e| {
73            ServiceError::Custom(format!("Failed to serialize skill-project.toml: {}", e))
74        })?;
75        std::fs::write(&skill_project_path, content).map_err(ServiceError::Io)?;
76        return Ok(());
77    }
78
79    let content = std::fs::read_to_string(&skill_project_path).map_err(ServiceError::Io)?;
80
81    // Parse the TOML structure with [metadata] section
82    #[derive(serde::Deserialize, serde::Serialize)]
83    struct SkillProjectToml {
84        #[serde(default)]
85        metadata: Option<SkillProjectMetadata>,
86        #[serde(default)]
87        dependencies: Option<toml::Value>,
88    }
89
90    #[derive(serde::Deserialize, serde::Serialize)]
91    struct SkillProjectMetadata {
92        version: String,
93        #[serde(skip_serializing_if = "Option::is_none")]
94        name: Option<String>,
95        #[serde(skip_serializing_if = "Option::is_none")]
96        description: Option<String>,
97        #[serde(skip_serializing_if = "Option::is_none")]
98        author: Option<String>,
99        #[serde(skip_serializing_if = "Option::is_none")]
100        tags: Option<Vec<String>>,
101        #[serde(skip_serializing_if = "Option::is_none")]
102        capabilities: Option<Vec<String>>,
103        #[serde(skip_serializing_if = "Option::is_none")]
104        download_url: Option<String>,
105    }
106
107    if let Ok(mut skill_project) = toml::from_str::<SkillProjectToml>(&content) {
108        if let Some(ref mut metadata) = skill_project.metadata {
109            metadata.version = new_version.to_string();
110        } else {
111            skill_project.metadata = Some(SkillProjectMetadata {
112                version: new_version.to_string(),
113                name: None,
114                description: None,
115                author: None,
116                tags: None,
117                capabilities: None,
118                download_url: None,
119            });
120        }
121        let content = toml::to_string_pretty(&skill_project).map_err(|e| {
122            ServiceError::Custom(format!("Failed to serialize skill-project.toml: {}", e))
123        })?;
124        std::fs::write(&skill_project_path, content).map_err(ServiceError::Io)?;
125        Ok(())
126    } else {
127        // If parsing fails, create new structure with [metadata] section
128        let new_skill_project = SkillProjectToml {
129            metadata: Some(SkillProjectMetadata {
130                version: new_version.to_string(),
131                name: None,
132                description: None,
133                author: None,
134                tags: None,
135                capabilities: None,
136                download_url: None,
137            }),
138            dependencies: None,
139        };
140        let content = toml::to_string_pretty(&new_skill_project).map_err(|e| {
141            ServiceError::Custom(format!("Failed to serialize skill-project.toml: {}", e))
142        })?;
143        std::fs::write(&skill_project_path, content).map_err(ServiceError::Io)?;
144        Ok(())
145    }
146}
147
148/// Get current version from skill-project.toml
149pub fn get_current_version(skill_path: &Path) -> Result<Option<String>, ServiceError> {
150    let skill_project_path = skill_path.join("skill-project.toml");
151
152    if !skill_project_path.exists() {
153        return Ok(None);
154    }
155
156    let content = std::fs::read_to_string(&skill_project_path).map_err(ServiceError::Io)?;
157
158    #[derive(serde::Deserialize)]
159    struct SkillProjectToml {
160        metadata: Option<SkillProjectMetadata>,
161    }
162
163    #[derive(serde::Deserialize)]
164    struct SkillProjectMetadata {
165        version: Option<String>,
166    }
167
168    if let Ok(skill_project) = toml::from_str::<SkillProjectToml>(&content) {
169        if let Some(metadata) = skill_project.metadata {
170            if let Some(version) = metadata.version {
171                if !version.is_empty() {
172                    return Ok(Some(version));
173                }
174            }
175        }
176    }
177
178    Ok(None)
179}