plugin-creator

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.

codex-rs/skills/src/assets/samples/plugin-creator/SKILL.md

Download bundle ↓
main · 18d7ace11 bundle filesScanned 2026-09-14

scripts/create_basic_plugin.py

2,553 tokens · o200k_base · 11,731 bytes

Source excerpt starting at line 1.
#!/usr/bin/env python3"""Scaffold a plugin directory and optionally update marketplace.json.""" from __future__ import annotations import argparseimport jsonimport reimport sysfrom pathlib import Pathfrom typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) from identifier_validation import validate_marketplace_name  MAX_PLUGIN_NAME_LENGTH = 64DEFAULT_INSTALL_POLICY = "AVAILABLE"DEFAULT_AUTH_POLICY = "ON_INSTALL"DEFAULT_CATEGORY = "Productivity"DEFAULT_MARKETPLACE_NAME = "personal"VALID_INSTALL_POLICIES = {"NOT_AVAILABLE", "AVAILABLE", "INSTALLED_BY_DEFAULT"}VALID_AUTH_POLICIES = {"ON_INSTALL", "ON_USE"}DEFAULT_PLUGIN_PARENT = Path.home() / "plugins"DEFAULT_MARKETPLACE_PATH = Path.home() / ".agents" / "plugins" / "marketplace.json"  def normalize_plugin_name(plugin_name: str) -> str:    """Normalize a plugin name to lowercase hyphen-case."""    normalized = plugin_name.strip().lower()    normalized = re.sub(r"[^a-z0-9]+", "-", normalized)    normalized = normalized.strip("-")    normalized = re.sub(r"-{2,}", "-", normalized)    return normalized  def validate_plugin_name(plugin_name: str) -> None:    if not plugin_name:        raise ValueError("Plugin name must include at least one letter or digit.")    if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH:        raise ValueError(            f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). "            f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters."        )  def display_name_from_plugin_name(plugin_name: str) -> str:    return " ".join(part.capitalize() for part in re.split(r"[-_]+", plugin_name))  def build_plugin_json(    plugin_name: str, *, with_mcp: bool, with_apps: bool) -> dict[str, Any]:    display_name = display_name_from_plugin_name(plugin_name)    payload: dict[str, Any] = {        "name": plugin_name,        "version": "0.1.0",        "description": f"{display_name} plugin",        "author": {            "name": "Local developer",        },        "skills": "./skills/",        "interface": {            "displayName": display_name,            "shortDescription": f"Use {display_name} in Codex.",            "longDescription": f"{display_name} adds a local Codex plugin scaffold.",            "developerName": "Local developer",            "category": DEFAULT_CATEGORY,            "capabilities": [],            "defaultPrompt": f"Help me use {display_name}.",        },    }    if with_mcp:        payload["mcpServers"] = "./.mcp.json"    if with_apps:        payload["apps"] = "./.app.json"    return payload  def build_marketplace_entry(    plugin_name: str,    install_policy: str,    auth_policy: str,    category: str,) -> dict[str, Any]:    return {        "name": plugin_name,        "source": {            "source": "local",            "path": f"./plugins/{plugin_name}",        },        "policy": {            "installation": install_policy,            "authentication": auth_policy,        },        "category": category,    }  def load_json(path: Path) -> dict[str, Any]:    with path.open() as handle:        return json.load(handle)  def build_default_marketplace(marketplace_name: str) -> dict[str, Any]:    return {        "name": marketplace_name,        "interface": {            "displayName": display_name_from_plugin_name(marketplace_name),        },        "plugins": [],    }  def validate_marketplace_interface(payload: dict[str, Any]) -> None:    interface = payload.get("interface")    if interface is not None and not isinstance(interface, dict):        raise ValueError("marketplace.json field 'interface' must be an object.")  def load_validated_marketplace(    marketplace_path: Path,    marketplace_name: str | None,    plugin_name: str,    force: bool,) -> dict[str, Any]:    if marketplace_path.exists():        payload = load_json(marketplace_path)    else:        payload = build_default_marketplace(            marketplace_name or DEFAULT_MARKETPLACE_NAME        )     if not isinstance(payload, dict):        raise ValueError(f"{marketplace_path} must contain a JSON object.")     validate_marketplace_interface(payload)     existing_marketplace_name = payload.get("name")    if (        not isinstance(existing_marketplace_name, str)        or not existing_marketplace_name.strip()    ):        raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.")    validate_marketplace_name(existing_marketplace_name)     if marketplace_name is not None:        if existing_marketplace_name != marketplace_name:            raise ValueError(                f"{marketplace_path} already uses marketplace name "                f"'{existing_marketplace_name}'. Create a new marketplace file to use "                f"'{marketplace_name}' instead."            )     plugins = payload.setdefault("plugins", [])    if not isinstance(plugins, list):        raise ValueError(f"{marketplace_path} field 'plugins' must be an array.")    if not force and any(        isinstance(entry, dict) and entry.get("name") == plugin_name        for entry in plugins    ):        raise FileExistsError(            f"Marketplace entry '{plugin_name}' already exists in {marketplace_path}. "            "Use --force to overwrite that entry."        )     return payload  def update_marketplace_json(    marketplace_path: Path,    marketplace_name: str | None,    plugin_name: str,    install_policy: str,    auth_policy: str,    category: str,    force: bool,) -> None:    payload = load_validated_marketplace(        marketplace_path, marketplace_name, plugin_name, force    )    plugins = payload["plugins"]     new_entry = build_marketplace_entry(        plugin_name, install_policy, auth_policy, category    )     for index, entry in enumerate(plugins):        if isinstance(entry, dict) and entry.get("name") == plugin_name:            plugins[index] = new_entry            break    else:        plugins.append(new_entry)     write_json(marketplace_path, payload, force=True)  def write_json(path: Path, data: dict, force: bool) -> None:    if path.exists() and not force:        raise FileExistsError(f"{path} already exists. Use --force to overwrite.")    path.parent.mkdir(parents=True, exist_ok=True)    with path.open("w") as handle:        json.dump(data, handle, indent=2)        handle.write("\n")  def create_stub_file(path: Path, payload: dict, force: bool) -> None:    if path.exists() and not force:        return    path.parent.mkdir(parents=True, exist_ok=True)    with path.open("w") as handle:        json.dump(payload, handle, indent=2)        handle.write("\n")  def parse_args() -> argparse.Namespace:    parser = argparse.ArgumentParser(        description="Create a plugin skeleton with a validation-ready plugin.json."    )    parser.add_argument("plugin_name")    parser.add_argument(        "--path",        default=str(DEFAULT_PLUGIN_PARENT),        help=(            "Parent directory for plugin creation (defaults to <home>/plugins). "            "Pass an explicit repo path only when a repo/team plugin is intended."        ),    )    parser.add_argument(        "--with-skills", action="store_true", help="Create skills/ directory"    )    parser.add_argument(        "--with-hooks", action="store_true", help="Create hooks/ directory"    )    parser.add_argument(        "--with-scripts", action="store_true", help="Create scripts/ directory"    )    parser.add_argument(        "--with-assets", action="store_true", help="Create assets/ directory"    )    parser.add_argument(        "--with-mcp", action="store_true", help="Create .mcp.json placeholder"    )    parser.add_argument(        "--with-apps", action="store_true", help="Create .app.json placeholder"    )    parser.add_argument(        "--with-marketplace",        action="store_true",        help=(            "Create or update <home>/.agents/plugins/marketplace.json by default. "            "Marketplace entries always point to ./plugins/<plugin-name> relative to the "            "marketplace root."        ),    )    parser.add_argument(        "--marketplace-path",        default=str(DEFAULT_MARKETPLACE_PATH),        help=(            "Path to marketplace.json (defaults to <home>/.agents/plugins/marketplace.json). "            "Pass a repo-rooted marketplace path only when a repo/team plugin is intended."        ),    )    parser.add_argument(        "--marketplace-name",        help=(            "Marketplace name to seed into a new marketplace.json. Use this only when the default "            "'personal' marketplace name is already taken and you need a different new marketplace."        ),    )    parser.add_argument(        "--install-policy",        default=DEFAULT_INSTALL_POLICY,        choices=sorted(VALID_INSTALL_POLICIES),        help="Marketplace policy.installation value",    )    parser.add_argument(        "--auth-policy",        default=DEFAULT_AUTH_POLICY,        choices=sorted(VALID_AUTH_POLICIES),        help="Marketplace policy.authentication value",    )    parser.add_argument(        "--category",        default=DEFAULT_CATEGORY,        help="Marketplace category value",    )    parser.add_argument("--force", action="store_true", help="Overwrite existing files")    return parser.parse_args()  def main() -> None:    args = parse_args()    raw_plugin_name = args.plugin_name    plugin_name = normalize_plugin_name(raw_plugin_name)    if plugin_name != raw_plugin_name:        print(            f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'."        )    validate_plugin_name(plugin_name)    marketplace_name = None    if args.marketplace_name is not None:        marketplace_name = args.marketplace_name.strip()        validate_marketplace_name(marketplace_name)     if args.with_marketplace:        marketplace_path = Path(args.marketplace_path).expanduser().resolve()        load_validated_marketplace(            marketplace_path, marketplace_name, plugin_name, args.force        )     plugin_root = Path(args.path).expanduser().resolve() / plugin_name    plugin_root.mkdir(parents=True, exist_ok=True)     plugin_json_path = plugin_root / ".codex-plugin" / "plugin.json"    write_json(        plugin_json_path,        build_plugin_json(            plugin_name, with_mcp=args.with_mcp, with_apps=args.with_apps        ),        args.force,    )     optional_directories = {        "skills": args.with_skills,        "hooks": args.with_hooks,        "scripts": args.with_scripts,        "assets": args.with_assets,    }    for folder, enabled in optional_directories.items():        if enabled:            (plugin_root / folder).mkdir(parents=True, exist_ok=True)     if args.with_mcp:        create_stub_file(            plugin_root / ".mcp.json",            {"mcpServers": {}},            args.force,        )     if args.with_apps:        create_stub_file(            plugin_root / ".app.json",            {                "apps": {},            },            args.force,        )     if args.with_marketplace:        update_marketplace_json(            marketplace_path,            marketplace_name,            plugin_name,            args.install_policy,            args.auth_policy,            args.category,            args.force,        )     print(f"Created plugin scaffold: {plugin_root}")    print(f"plugin manifest: {plugin_json_path}")    if args.with_marketplace:        print(f"marketplace manifest: {marketplace_path}")  if __name__ == "__main__":    main() 
Referenced from SKILL.md