Compare commits

...

1 Commits

Author SHA1 Message Date
Matthew Zeng
7992cd07ee Support symlinked SKILL.md files 2026-01-16 13:04:25 -08:00
2 changed files with 73 additions and 13 deletions

View File

@@ -282,6 +282,10 @@ fn discover_skills_under_root(root: &Path, scope: SkillScope, outcome: &mut Skil
continue;
}
if metadata.is_file() && file_name == SKILLS_FILENAME {
record_skill_from_path(&path, scope, outcome);
}
continue;
}
@@ -300,19 +304,7 @@ fn discover_skills_under_root(root: &Path, scope: SkillScope, outcome: &mut Skil
}
if file_type.is_file() && file_name == SKILLS_FILENAME {
match parse_skill_file(&path, scope) {
Ok(skill) => {
outcome.skills.push(skill);
}
Err(err) => {
if scope != SkillScope::System {
outcome.errors.push(SkillError {
path,
message: err.to_string(),
});
}
}
}
record_skill_from_path(&path, scope, outcome);
}
}
}
@@ -326,6 +318,22 @@ fn discover_skills_under_root(root: &Path, scope: SkillScope, outcome: &mut Skil
}
}
fn record_skill_from_path(path: &Path, scope: SkillScope, outcome: &mut SkillLoadOutcome) {
match parse_skill_file(path, scope) {
Ok(skill) => {
outcome.skills.push(skill);
}
Err(err) => {
if scope != SkillScope::System {
outcome.errors.push(SkillError {
path: path.to_path_buf(),
message: err.to_string(),
});
}
}
}
}
fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, SkillParseError> {
let contents = fs::read_to_string(path).map_err(SkillParseError::Read)?;

View File

@@ -15,6 +15,7 @@ use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
fn write_skill(home: &Path, name: &str, description: &str, body: &str) -> std::path::PathBuf {
@@ -154,6 +155,57 @@ async fn skill_load_errors_surface_in_session_configured() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_skills_includes_symlinked_skill_md() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_pre_build_hook(|home| {
let external_dir = home.join("external-skills").join("demo-symlink");
fs::create_dir_all(&external_dir).unwrap();
let contents = "---\nname: demo-symlink\ndescription: demo skill\n---\n\nbody\n";
let external_skill_path = external_dir.join("SKILL.md");
fs::write(&external_skill_path, contents).unwrap();
let skill_dir = home.join("skills").join("demo-symlink");
fs::create_dir_all(&skill_dir).unwrap();
symlink(&external_skill_path, skill_dir.join("SKILL.md")).unwrap();
});
let test = builder.build(&server).await?;
test.codex
.submit(Op::ListSkills {
cwds: Vec::new(),
force_reload: true,
})
.await?;
let response =
core_test_support::wait_for_event_match(test.codex.as_ref(), |event| match event {
codex_core::protocol::EventMsg::ListSkillsResponse(response) => Some(response.clone()),
_ => None,
})
.await;
let cwd = test.cwd_path();
let (skills, errors) = response
.skills
.iter()
.find(|entry| entry.cwd.as_path() == cwd)
.map(|entry| (entry.skills.clone(), entry.errors.clone()))
.unwrap_or_default();
assert!(
errors.is_empty(),
"expected no load errors for symlinked skill, got {errors:?}"
);
assert!(
skills.iter().any(|skill| skill.name == "demo-symlink"),
"expected symlinked skill to be listed, got {skills:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_skills_includes_system_cache_entries() -> Result<()> {
skip_if_no_network!(Ok(()));