mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Migrate Cursor-managed skills into Codex (#36361)
## What changed - Discover and import home-level Cursor skills from both `skills` and `skills-cursor`. - Keep repository-level Cursor migration scoped to `skills`. - Deduplicate skill names when reporting migration candidates from multiple source directories. ## Testing - Add coverage for detecting and importing user and Cursor-managed skills. - Verify repository detection continues to use the `skills` directory. GitOrigin-RevId: 7b5e96a0d9c5ca14c61230e336728f2e4930a3b0
This commit is contained in:
committed by
copyberry
parent
385fe95ce1
commit
da2c7ca8d1
@@ -189,22 +189,33 @@ impl ExternalAgentConfigService {
|
||||
);
|
||||
}
|
||||
|
||||
let source_skills = repo_root.map_or_else(
|
||||
|| self.external_agent_home.join("skills"),
|
||||
|repo_root| repo_root.join(self.source.config_dir()).join("skills"),
|
||||
);
|
||||
let source_skills = self
|
||||
.source
|
||||
.skills_dir_names(scope)
|
||||
.iter()
|
||||
.map(|directory| source_external_agent_dir.join(*directory))
|
||||
.filter(|path| path.is_dir())
|
||||
.collect::<Vec<_>>();
|
||||
let target_skills = repo_root.map_or_else(
|
||||
|| self.home_target_skills_dir(),
|
||||
|repo_root| repo_root.join(".agents").join("skills"),
|
||||
);
|
||||
let skill_names = missing_subdirectory_names(&source_skills, &target_skills)?;
|
||||
let mut skill_names = Vec::new();
|
||||
for source_skills_dir in &source_skills {
|
||||
skill_names.extend(missing_subdirectory_names(
|
||||
source_skills_dir,
|
||||
&target_skills,
|
||||
)?);
|
||||
}
|
||||
skill_names.sort();
|
||||
skill_names.dedup();
|
||||
let skills_count = skill_names.len();
|
||||
if skills_count > 0 {
|
||||
items.push(ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
||||
description: format!(
|
||||
"Migrate skills from {} to {}",
|
||||
source_skills.display(),
|
||||
display_source_paths(&source_skills),
|
||||
target_skills.display()
|
||||
),
|
||||
cwd: cwd.clone(),
|
||||
|
||||
@@ -71,6 +71,14 @@ impl ExternalAgentSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn skills_dir_names(self, scope: &MigrationScope) -> &'static [&'static str] {
|
||||
match (self, scope) {
|
||||
// skills-cursor is Cursor-managed and only exists under the home config.
|
||||
(Self::Cur, MigrationScope::Home) => &["skills", "skills-cursor"],
|
||||
_ => &["skills"],
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn supports_memory(self) -> bool {
|
||||
match self {
|
||||
Self::Cla => true,
|
||||
|
||||
@@ -682,37 +682,45 @@ impl ExternalAgentConfigService {
|
||||
let Some(scope) = MigrationScope::from_cwd(cwd)? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let (source_skills, target_skills) = match scope {
|
||||
let source_skills_dir_names = self.source.skills_dir_names(&scope);
|
||||
let (source_config_dir, target_skills) = match scope {
|
||||
MigrationScope::Home => (
|
||||
self.external_agent_home.join("skills"),
|
||||
self.external_agent_home.clone(),
|
||||
self.home_target_skills_dir(),
|
||||
),
|
||||
MigrationScope::Repository { root } => (
|
||||
root.join(self.source.config_dir()).join("skills"),
|
||||
root.join(self.source.config_dir()),
|
||||
root.join(".agents").join("skills"),
|
||||
),
|
||||
};
|
||||
if !source_skills.is_dir() {
|
||||
let source_skills = source_skills_dir_names
|
||||
.iter()
|
||||
.map(|directory| source_config_dir.join(*directory))
|
||||
.filter(|path| path.is_dir())
|
||||
.collect::<Vec<_>>();
|
||||
if source_skills.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
fs::create_dir_all(&target_skills)?;
|
||||
let mut copied_names = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(&source_skills)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
if !file_type.is_dir() {
|
||||
continue;
|
||||
}
|
||||
for source_skills_dir in source_skills {
|
||||
for entry in fs::read_dir(&source_skills_dir)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
if !file_type.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let target = target_skills.join(entry.file_name());
|
||||
if target.exists() {
|
||||
continue;
|
||||
}
|
||||
let target = target_skills.join(entry.file_name());
|
||||
if target.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
copy_dir_recursive(&entry.path(), &target, self.source.rewrite_profile())?;
|
||||
copied_names.push(entry.file_name().to_string_lossy().to_string());
|
||||
copy_dir_recursive(&entry.path(), &target, self.source.rewrite_profile())?;
|
||||
copied_names.push(entry.file_name().to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(copied_names)
|
||||
|
||||
@@ -69,6 +69,91 @@ async fn detect_home_lists_config_skills_and_agents_md() {
|
||||
assert_eq!(items, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn detect_cursor_home_lists_user_and_managed_skills() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let external_agent_home = root.path().join(".cursor");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let user_skills = external_agent_home.join("skills");
|
||||
let managed_skills = external_agent_home.join("skills-cursor");
|
||||
let target_skills = codex_home
|
||||
.parent()
|
||||
.map(|parent| parent.join(".agents").join("skills"))
|
||||
.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
|
||||
fs::create_dir_all(user_skills.join("user-skill")).expect("create user skill");
|
||||
fs::create_dir_all(managed_skills.join("managed-skill")).expect("create managed skill");
|
||||
let mut service = service_for_paths(external_agent_home, codex_home);
|
||||
service.source = ExternalAgentSource::Cur;
|
||||
|
||||
let items = service
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
include_memory: false,
|
||||
cwds: None,
|
||||
})
|
||||
.await
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
||||
description: format!(
|
||||
"Migrate skills from {}, {} to {}",
|
||||
user_skills.display(),
|
||||
managed_skills.display(),
|
||||
target_skills.display()
|
||||
),
|
||||
cwd: None,
|
||||
details: Some(MigrationDetails {
|
||||
skills: named_migrations(vec![
|
||||
"managed-skill".to_string(),
|
||||
"user-skill".to_string()
|
||||
]),
|
||||
..Default::default()
|
||||
}),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn detect_cursor_repo_lists_skills_from_skills() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let repo_root = root.path().join("repo");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let source_skills = repo_root.join(".cursor").join("skills");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
|
||||
fs::create_dir_all(source_skills.join("skill-a")).expect("create repo skill");
|
||||
let mut service = service_for_paths(root.path().join(".cursor"), codex_home);
|
||||
service.source = ExternalAgentSource::Cur;
|
||||
|
||||
let items = service
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: false,
|
||||
include_memory: false,
|
||||
cwds: Some(vec![repo_root.clone()]),
|
||||
})
|
||||
.await
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
||||
description: format!(
|
||||
"Migrate skills from {} to {}",
|
||||
source_skills.display(),
|
||||
repo_root.join(".agents").join("skills").display()
|
||||
),
|
||||
cwd: Some(repo_root),
|
||||
details: Some(MigrationDetails {
|
||||
skills: named_migrations(vec!["skill-a".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn detect_home_lists_recent_sessions() {
|
||||
let (root, external_agent_home, codex_home) = fixture_paths();
|
||||
|
||||
@@ -710,3 +710,43 @@ fn import_skills_returns_only_new_skill_directory_names() {
|
||||
|
||||
assert_eq!(copied_names, vec!["skill-b".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_cursor_skills_reads_user_and_managed_directories() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let external_agent_home = root.path().join(".cursor");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let user_skill = external_agent_home.join("skills").join("user-skill");
|
||||
let managed_skill = external_agent_home
|
||||
.join("skills-cursor")
|
||||
.join("managed-skill");
|
||||
let target_skills = codex_home
|
||||
.parent()
|
||||
.map(|parent| parent.join(".agents").join("skills"))
|
||||
.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
|
||||
fs::create_dir_all(&user_skill).expect("create user skill");
|
||||
fs::create_dir_all(&managed_skill).expect("create managed skill");
|
||||
fs::write(user_skill.join("SKILL.md"), "# Imported user skill").expect("write user skill");
|
||||
fs::write(managed_skill.join("SKILL.md"), "# Imported managed skill")
|
||||
.expect("write managed skill");
|
||||
let mut service = service_for_paths(external_agent_home, codex_home);
|
||||
service.source = ExternalAgentSource::Cur;
|
||||
|
||||
let mut copied_names = service.import_skills(/*cwd*/ None).expect("import skills");
|
||||
copied_names.sort();
|
||||
|
||||
assert_eq!(
|
||||
copied_names,
|
||||
vec!["managed-skill".to_string(), "user-skill".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(target_skills.join("user-skill").join("SKILL.md"))
|
||||
.expect("read user skill"),
|
||||
"# Imported user skill"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(target_skills.join("managed-skill").join("SKILL.md"))
|
||||
.expect("read managed skill"),
|
||||
"# Imported managed skill"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user