-
Notifications
You must be signed in to change notification settings - Fork 182
fix: match installed skills by source repo, not just name #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -63,7 +63,10 @@ pub async fn install_from_github(spec: &str, target_dir: &Path) -> Result<Vec<St | |||||||||||||||||||||||||||
| drop(file); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Extract and install | ||||||||||||||||||||||||||||
| let installed = extract_and_install(&zip_path, target_dir, skill_path.as_deref()).await?; | ||||||||||||||||||||||||||||
| let source_repo = format!("{owner}/{repo}"); | ||||||||||||||||||||||||||||
| let installed = | ||||||||||||||||||||||||||||
| extract_and_install(&zip_path, target_dir, skill_path.as_deref(), Some(&source_repo)) | ||||||||||||||||||||||||||||
| .await?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| tracing::info!( | ||||||||||||||||||||||||||||
| installed = ?installed, | ||||||||||||||||||||||||||||
|
|
@@ -84,7 +87,7 @@ pub async fn install_from_file(skill_file: &Path, target_dir: &Path) -> Result<V | |||||||||||||||||||||||||||
| "installing skill from file" | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let installed = extract_and_install(skill_file, target_dir, None).await?; | ||||||||||||||||||||||||||||
| let installed = extract_and_install(skill_file, target_dir, None, None).await?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| tracing::info!( | ||||||||||||||||||||||||||||
| installed = ?installed, | ||||||||||||||||||||||||||||
|
|
@@ -102,6 +105,7 @@ async fn extract_and_install( | |||||||||||||||||||||||||||
| zip_path: &Path, | ||||||||||||||||||||||||||||
| target_dir: &Path, | ||||||||||||||||||||||||||||
| skill_path: Option<&str>, | ||||||||||||||||||||||||||||
| source_repo: Option<&str>, | ||||||||||||||||||||||||||||
| ) -> Result<Vec<String>> { | ||||||||||||||||||||||||||||
| let file = std::fs::File::open(zip_path).context("failed to open zip file")?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -176,6 +180,23 @@ async fn extract_and_install( | |||||||||||||||||||||||||||
| // Copy skill directory | ||||||||||||||||||||||||||||
| copy_dir_recursive(&skill_dir, &target_skill_dir).await?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Write source_repo into SKILL.md frontmatter so we can track provenance | ||||||||||||||||||||||||||||
| if let Some(repo) = source_repo { | ||||||||||||||||||||||||||||
| let skill_md = target_skill_dir.join("SKILL.md"); | ||||||||||||||||||||||||||||
| if skill_md.exists() { | ||||||||||||||||||||||||||||
| if let Ok(content) = fs::read_to_string(&skill_md).await { | ||||||||||||||||||||||||||||
| let patched = inject_source_repo(&content, repo); | ||||||||||||||||||||||||||||
| if let Err(error) = fs::write(&skill_md, patched).await { | ||||||||||||||||||||||||||||
| tracing::warn!( | ||||||||||||||||||||||||||||
| skill = %skill_name, | ||||||||||||||||||||||||||||
| %error, | ||||||||||||||||||||||||||||
| "failed to write source_repo to SKILL.md" | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+184
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent discard of The 🛡️ Proposed fix- if let Ok(content) = fs::read_to_string(&skill_md).await {
- let patched = inject_source_repo(&content, repo);
- if let Err(error) = fs::write(&skill_md, patched).await {
- tracing::warn!(
- skill = %skill_name,
- %error,
- "failed to write source_repo to SKILL.md"
- );
- }
- }
+ match fs::read_to_string(&skill_md).await {
+ Ok(content) => {
+ let patched = inject_source_repo(&content, repo);
+ if let Err(error) = fs::write(&skill_md, patched).await {
+ tracing::warn!(
+ skill = %skill_name,
+ %error,
+ "failed to write source_repo to SKILL.md"
+ );
+ }
+ }
+ Err(error) => {
+ tracing::warn!(
+ skill = %skill_name,
+ %error,
+ "failed to read SKILL.md for source_repo injection"
+ );
+ }
+ }As per coding guidelines: "Don't silently discard errors; no 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| installed.push(skill_name.to_string()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| tracing::debug!( | ||||||||||||||||||||||||||||
|
|
@@ -188,6 +209,41 @@ async fn extract_and_install( | |||||||||||||||||||||||||||
| Ok(installed) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Inject or update `source_repo` in SKILL.md frontmatter. | ||||||||||||||||||||||||||||
| fn inject_source_repo(content: &str, repo: &str) -> String { | ||||||||||||||||||||||||||||
| let trimmed = content.trim_start(); | ||||||||||||||||||||||||||||
| let line = format!("source_repo: {repo}"); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if !trimmed.starts_with("---") { | ||||||||||||||||||||||||||||
| // No frontmatter — add one | ||||||||||||||||||||||||||||
| return format!("---\n{line}\n---\n{content}"); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let after_opening = &trimmed[3..]; | ||||||||||||||||||||||||||||
| if let Some(end_pos) = after_opening.find("\n---") { | ||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||
| let fm_block = &after_opening[..end_pos]; | ||||||||||||||||||||||||||||
| let body = &after_opening[end_pos + 4..]; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Remove any existing source_repo line | ||||||||||||||||||||||||||||
| let filtered: Vec<&str> = fm_block | ||||||||||||||||||||||||||||
| .lines() | ||||||||||||||||||||||||||||
| .filter(|l| { | ||||||||||||||||||||||||||||
| !l.trim_start() | ||||||||||||||||||||||||||||
| .starts_with("source_repo:") | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let mut new_fm = filtered.join("\n"); | ||||||||||||||||||||||||||||
| new_fm.push('\n'); | ||||||||||||||||||||||||||||
| new_fm.push_str(&line); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| format!("---{new_fm}\n---{body}") | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| // Malformed frontmatter, prepend | ||||||||||||||||||||||||||||
| format!("---\n{line}\n---\n{content}") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Parse a GitHub spec: `owner/repo` or `owner/repo/skill-name` | ||||||||||||||||||||||||||||
| fn parse_github_spec(spec: &str) -> Result<(String, String, Option<String>)> { | ||||||||||||||||||||||||||||
| let parts: Vec<&str> = spec.split('/').collect(); | ||||||||||||||||||||||||||||
|
|
@@ -308,4 +364,55 @@ mod tests { | |||||||||||||||||||||||||||
| assert!(parse_github_spec("invalid").is_err()); | ||||||||||||||||||||||||||||
| assert!(parse_github_spec("too/many/slashes/here").is_err()); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||
| fn test_inject_source_repo_into_existing_frontmatter() { | ||||||||||||||||||||||||||||
| let content = "---\nname: weather\ndescription: Get weather\n---\n\n# Weather\n"; | ||||||||||||||||||||||||||||
| let result = inject_source_repo(content, "anthropics/skills"); | ||||||||||||||||||||||||||||
| assert!(result.contains("source_repo: anthropics/skills")); | ||||||||||||||||||||||||||||
| assert!(result.contains("name: weather")); | ||||||||||||||||||||||||||||
| assert!(result.contains("# Weather")); | ||||||||||||||||||||||||||||
| // source_repo should be inside the frontmatter delimiters | ||||||||||||||||||||||||||||
| let after_first = result.splitn(2, "---").nth(1).unwrap(); | ||||||||||||||||||||||||||||
| let fm = after_first.splitn(2, "\n---").next().unwrap(); | ||||||||||||||||||||||||||||
| assert!(fm.contains("source_repo: anthropics/skills")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||
| fn test_inject_source_repo_no_frontmatter() { | ||||||||||||||||||||||||||||
| let content = "# Just markdown\n\nNo frontmatter here."; | ||||||||||||||||||||||||||||
| let result = inject_source_repo(content, "owner/repo"); | ||||||||||||||||||||||||||||
| assert!(result.starts_with("---\nsource_repo: owner/repo\n---\n")); | ||||||||||||||||||||||||||||
| assert!(result.contains("# Just markdown")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||
| fn test_inject_source_repo_updates_existing() { | ||||||||||||||||||||||||||||
| let content = "---\nname: weather\nsource_repo: old/repo\ndescription: foo\n---\n\nBody\n"; | ||||||||||||||||||||||||||||
| let result = inject_source_repo(content, "new/repo"); | ||||||||||||||||||||||||||||
| assert!(result.contains("source_repo: new/repo")); | ||||||||||||||||||||||||||||
| assert!(!result.contains("old/repo")); | ||||||||||||||||||||||||||||
| // Should only have one source_repo line | ||||||||||||||||||||||||||||
| assert_eq!(result.matches("source_repo:").count(), 1); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||
| fn test_inject_source_repo_malformed_frontmatter() { | ||||||||||||||||||||||||||||
| let content = "---\nname: broken\nno closing delimiter"; | ||||||||||||||||||||||||||||
| let result = inject_source_repo(content, "owner/repo"); | ||||||||||||||||||||||||||||
| // Falls back to prepending new frontmatter | ||||||||||||||||||||||||||||
| assert!(result.starts_with("---\nsource_repo: owner/repo\n---\n")); | ||||||||||||||||||||||||||||
| assert!(result.contains("no closing delimiter")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||
| fn test_inject_source_repo_roundtrip_with_parse() { | ||||||||||||||||||||||||||||
| use crate::skills::parse_frontmatter; | ||||||||||||||||||||||||||||
| let content = "---\nname: weather\ndescription: Get weather\n---\n\n# Weather\n"; | ||||||||||||||||||||||||||||
| let patched = inject_source_repo(content, "anthropics/skills"); | ||||||||||||||||||||||||||||
| let (fm, body) = parse_frontmatter(&patched).unwrap(); | ||||||||||||||||||||||||||||
| assert_eq!(fm.get("source_repo").unwrap(), &"anthropics/skills".to_string()); | ||||||||||||||||||||||||||||
| assert_eq!(fm.get("name").unwrap(), &"weather".to_string()); | ||||||||||||||||||||||||||||
| assert!(body.contains("# Weather")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
source_repoends up embedded into YAML frontmatter, it might be worth ensuring it can’t contain newlines (e.g. ifspecis weird) so we don’t accidentally corruptSKILL.md.