Files
codex/codex-rs/core-plugins/src/command_migration/plugin.rs
T
charlesgong-openai 2cd6ed7509 Migrate plugin commands into skills on install (#33411)
## What changed

- Convert supported plugin command Markdown into generated skills when a plugin is installed.
- Read command sources from the manifest's `commands` field, falling back to the plugin's `commands/` directory.
- Load generated command skills alongside native plugin skills while preferring native skills with the same name.
- Skip commands with unsupported templates, missing descriptions, name collisions, or generated skills larger than 4 KB, and keep migration failures from blocking plugin installation.

## Testing

- Cover default and manifest-configured command paths, stale generated skill cleanup, size limits, native-skill precedence, invalid manifest fields, and non-fatal migration errors.

GitOrigin-RevId: 67d910048544b160b191066569bca69b2b951ebc
2026-07-15 19:51:37 +00:00

62 lines
2.1 KiB
Rust

use super::CommandDescriptionMode;
use super::CommandMigrationProfile;
use super::CommandSkillSizeLimit;
use super::CommandSource;
use super::RewriteProfile;
use super::import_command_sources;
use super::supported_command_sources;
use super::unique_command_sources;
use crate::manifest::load_plugin_command_paths;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::fs;
use std::io;
use std::path::Path;
const PLUGIN_COMMANDS_DIR: &str = "commands";
const PLUGIN_METADATA_DIR: &str = ".codex-plugin";
const MIGRATED_COMMAND_SKILLS_DIR: &str = "migrated-command-skills";
const MAX_MIGRATED_COMMAND_SKILL_BYTES: usize = 4_000;
const PLUGIN_REWRITE_PROFILE: RewriteProfile = RewriteProfile::new("AGENTS.md", &[]);
const PLUGIN_MIGRATION_PROFILE: CommandMigrationProfile = CommandMigrationProfile::new(
PLUGIN_REWRITE_PROFILE,
CommandDescriptionMode::RequireFrontmatter,
);
pub(crate) fn migrate_plugin_commands(plugin_root: &Path) -> io::Result<()> {
let target_skills = plugin_root
.join(PLUGIN_METADATA_DIR)
.join(MIGRATED_COMMAND_SKILLS_DIR);
if target_skills.is_dir() {
fs::remove_dir_all(&target_skills)?;
} else if target_skills.exists() {
fs::remove_file(&target_skills)?;
}
import_command_sources(
plugin_command_sources(plugin_root)?,
&target_skills,
PLUGIN_MIGRATION_PROFILE,
CommandSkillSizeLimit::MaxBytes(MAX_MIGRATED_COMMAND_SKILL_BYTES),
)?;
Ok(())
}
pub(crate) fn migrated_command_skills_root(plugin_root: &AbsolutePathBuf) -> AbsolutePathBuf {
plugin_root
.join(PLUGIN_METADATA_DIR)
.join(MIGRATED_COMMAND_SKILLS_DIR)
}
fn plugin_command_sources(plugin_root: &Path) -> io::Result<Vec<CommandSource>> {
let command_paths = load_plugin_command_paths(plugin_root)?
.unwrap_or_else(|| vec![plugin_root.join(PLUGIN_COMMANDS_DIR)]);
let mut sources = Vec::new();
for command_path in command_paths {
sources.extend(supported_command_sources(
&command_path,
CommandDescriptionMode::RequireFrontmatter,
)?);
}
Ok(unique_command_sources(sources))
}