diff --git a/README.md b/README.md index cfe26780..7d8fc566 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,15 @@ codex plugin marketplace upgrade socket After the marketplace is added or upgraded, restart Codex, open the plugin directory in the Codex GUI, choose the `Socket` marketplace, and install or enable the desired child plugins there. Manual local marketplace roots are useful for development, unpublished testing, and fallback work, but they are not the default user install or update path. +If you previously used the older copied-plugin or personal-local-marketplace install path, run the legacy cleanup helper after the Git-backed marketplace works: + +```bash +uv run scripts/cleanup_legacy_socket_installs.py +uv run scripts/cleanup_legacy_socket_installs.py --apply +``` + +The first command is a dry run. The `--apply` mode backs up affected files and copied plugin directories before removing only known legacy `socket` install artifacts. It does not delete Codex's installed plugin cache under `~/.codex/plugins/cache/`. + ## Usage Use `socket` when the task is about the superproject layer: @@ -126,6 +135,14 @@ Run the root validator locally with: uv run scripts/validate_socket_metadata.py ``` +To audit or remove old copied personal plugin payloads after moving to the Git-backed marketplace path, run: + +```bash +uv run scripts/cleanup_legacy_socket_installs.py +``` + +Add `--apply` only after reviewing the dry-run output. The helper backs up the personal marketplace file and copied plugin directories under `~/.codex/backups/socket-legacy-install-cleanup//` before changing anything. It reports stale non-`socket` plugin enablement entries in `~/.codex/config.toml`, but it does not rewrite that config file. + When a child repository or helper surface grows Python-backed validation beyond this root metadata check, add those checks as repo-local `uv` dev dependencies and document the exact `uv run ...` commands in that child repo instead of assuming a globally provisioned toolchain. ## Repo Structure diff --git a/docs/maintainers/plugin-packaging-strategy.md b/docs/maintainers/plugin-packaging-strategy.md index 01ae9188..48f04b0b 100644 --- a/docs/maintainers/plugin-packaging-strategy.md +++ b/docs/maintainers/plugin-packaging-strategy.md @@ -61,6 +61,15 @@ codex plugin marketplace add gaelic-ghost/SpeakSwiftlyServer `socket` can still list the same child as `./plugins/` from the superproject marketplace. Use explicit refs such as `gaelic-ghost/socket@vX.Y.Z` only for pinned reproducible installs. Manual local marketplace roots and copied payload directories are development, unpublished-testing, and fallback tools rather than the default user-facing path. +When a user has already migrated from an older copied-plugin or personal-local-marketplace install to the Git-backed marketplace, use the repo-owned cleanup helper instead of hand-editing home-directory files: + +```bash +uv run scripts/cleanup_legacy_socket_installs.py +uv run scripts/cleanup_legacy_socket_installs.py --apply +``` + +The helper's job is intentionally narrow. It removes known legacy `socket` entries from `~/.agents/plugins/marketplace.json` and copied personal payload directories such as `~/.codex/plugins/apple-dev-skills` after backing them up. It leaves Codex's installed cache under `~/.codex/plugins/cache/` alone, because that cache is Codex-owned install state for current marketplace entries. + ## Follow-up Decision Once several child repos have stable plugin packaging, decide whether `socket` needs: diff --git a/plugins/agent-plugin-skills/.codex-plugin/plugin.json b/plugins/agent-plugin-skills/.codex-plugin/plugin.json index c933dd75..d91f5d74 100644 --- a/plugins/agent-plugin-skills/.codex-plugin/plugin.json +++ b/plugins/agent-plugin-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-plugin-skills", - "version": "6.3.1", + "version": "6.3.2", "description": "Installable maintainer skills for skills-export repositories.", "author": { "name": "Gale", diff --git a/plugins/agent-plugin-skills/pyproject.toml b/plugins/agent-plugin-skills/pyproject.toml index f4d98e67..58f1dd5f 100644 --- a/plugins/agent-plugin-skills/pyproject.toml +++ b/plugins/agent-plugin-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-plugin-skills-maintenance" -version = "6.3.1" +version = "6.3.2" description = "Maintainer-only Python tooling baseline for agent-plugin-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/agent-plugin-skills/uv.lock b/plugins/agent-plugin-skills/uv.lock index c98741fa..5b19adeb 100644 --- a/plugins/agent-plugin-skills/uv.lock +++ b/plugins/agent-plugin-skills/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "agent-plugin-skills-maintenance" -version = "6.3.1" +version = "6.3.2" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/apple-dev-skills/.agents/plugins/marketplace.json b/plugins/apple-dev-skills/.agents/plugins/marketplace.json index 4d02ece9..69390f55 100644 --- a/plugins/apple-dev-skills/.agents/plugins/marketplace.json +++ b/plugins/apple-dev-skills/.agents/plugins/marketplace.json @@ -1,7 +1,7 @@ { - "name": "apple-dev-skills-local", + "name": "apple-dev-skills", "interface": { - "displayName": "Apple Dev Skills Local Plugins" + "displayName": "Apple Dev Skills" }, "plugins": [ { diff --git a/plugins/apple-dev-skills/.codex-plugin/plugin.json b/plugins/apple-dev-skills/.codex-plugin/plugin.json index cdf719dc..3c923268 100644 --- a/plugins/apple-dev-skills/.codex-plugin/plugin.json +++ b/plugins/apple-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "apple-dev-skills", - "version": "6.3.1", + "version": "6.3.2", "description": "Apple development workflows for Codex and Claude Code, including SwiftUI architecture and DocC authoring guidance.", "author": { "name": "Gale", diff --git a/plugins/apple-dev-skills/pyproject.toml b/plugins/apple-dev-skills/pyproject.toml index 2a8ad3ef..e9917a05 100644 --- a/plugins/apple-dev-skills/pyproject.toml +++ b/plugins/apple-dev-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apple-dev-skills-maintainer" -version = "6.3.1" +version = "6.3.2" description = "Maintainer tooling for the apple-dev-skills repository" requires-python = ">=3.9" dependencies = [] diff --git a/plugins/apple-dev-skills/uv.lock b/plugins/apple-dev-skills/uv.lock index 44debbd6..545c6fbf 100644 --- a/plugins/apple-dev-skills/uv.lock +++ b/plugins/apple-dev-skills/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "apple-dev-skills-maintainer" -version = "6.3.1" +version = "6.3.2" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/cardhop-app/.codex-plugin/plugin.json b/plugins/cardhop-app/.codex-plugin/plugin.json index 66eb2e92..8537e2f9 100644 --- a/plugins/cardhop-app/.codex-plugin/plugin.json +++ b/plugins/cardhop-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cardhop-app", - "version": "6.3.1", + "version": "6.3.2", "description": "Cardhop.app workflow guidance plus a bundled local MCP server for contact capture and updates on macOS.", "author": { "name": "Gale", diff --git a/plugins/cardhop-app/mcp/pyproject.toml b/plugins/cardhop-app/mcp/pyproject.toml index 005471c1..588c5ff0 100644 --- a/plugins/cardhop-app/mcp/pyproject.toml +++ b/plugins/cardhop-app/mcp/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cardhop-app-mcp" -version = "6.3.1" +version = "6.3.2" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/cardhop-app/mcp/uv.lock b/plugins/cardhop-app/mcp/uv.lock index 70af24b3..37caae3b 100644 --- a/plugins/cardhop-app/mcp/uv.lock +++ b/plugins/cardhop-app/mcp/uv.lock @@ -93,7 +93,7 @@ wheels = [ [[package]] name = "cardhop-app-mcp" -version = "6.3.1" +version = "6.3.2" source = { virtual = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/dotnet-skills/.codex-plugin/plugin.json b/plugins/dotnet-skills/.codex-plugin/plugin.json index c3cb3cc1..23c34a90 100644 --- a/plugins/dotnet-skills/.codex-plugin/plugin.json +++ b/plugins/dotnet-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dotnet-skills", - "version": "6.3.1", + "version": "6.3.2", "description": "Standalone plugin repository for future .NET-focused Codex skills.", "author": { "name": "Gale", diff --git a/plugins/productivity-skills/.codex-plugin/plugin.json b/plugins/productivity-skills/.codex-plugin/plugin.json index c2979737..bed17345 100644 --- a/plugins/productivity-skills/.codex-plugin/plugin.json +++ b/plugins/productivity-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "productivity-skills", - "version": "6.3.1", + "version": "6.3.2", "description": "Broadly useful productivity workflows for Codex and Claude Code.", "author": { "name": "Gale", diff --git a/plugins/productivity-skills/pyproject.toml b/plugins/productivity-skills/pyproject.toml index 454470a9..7a47c6d0 100644 --- a/plugins/productivity-skills/pyproject.toml +++ b/plugins/productivity-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "productivity-skills-maintenance" -version = "6.3.1" +version = "6.3.2" description = "Maintainer-only Python tooling baseline for productivity-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/productivity-skills/uv.lock b/plugins/productivity-skills/uv.lock index e1371dab..06758da5 100644 --- a/plugins/productivity-skills/uv.lock +++ b/plugins/productivity-skills/uv.lock @@ -40,7 +40,7 @@ wheels = [ [[package]] name = "productivity-skills-maintenance" -version = "6.3.1" +version = "6.3.2" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/python-skills/.codex-plugin/plugin.json b/plugins/python-skills/.codex-plugin/plugin.json index 3e04f563..1e40787c 100644 --- a/plugins/python-skills/.codex-plugin/plugin.json +++ b/plugins/python-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "python-skills", - "version": "6.3.1", + "version": "6.3.2", "description": "Bundled Python-focused Codex skills for uv bootstrapping, FastAPI and FastMCP scaffolding, FastAPI/FastMCP integration, and pytest workflows.", "author": { "name": "Gale", diff --git a/plugins/python-skills/pyproject.toml b/plugins/python-skills/pyproject.toml index 437156e3..12510575 100644 --- a/plugins/python-skills/pyproject.toml +++ b/plugins/python-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-skills-maintainer" -version = "6.3.1" +version = "6.3.2" description = "Maintainer tooling for the python-skills repository" requires-python = ">=3.11" dependencies = [] diff --git a/plugins/python-skills/uv.lock b/plugins/python-skills/uv.lock index dd892232..f4e40de1 100644 --- a/plugins/python-skills/uv.lock +++ b/plugins/python-skills/uv.lock @@ -206,7 +206,7 @@ wheels = [ [[package]] name = "python-skills-maintainer" -version = "6.3.1" +version = "6.3.2" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/rust-skills/.codex-plugin/plugin.json b/plugins/rust-skills/.codex-plugin/plugin.json index 3a99e15b..a9fd304d 100644 --- a/plugins/rust-skills/.codex-plugin/plugin.json +++ b/plugins/rust-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rust-skills", - "version": "6.3.1", + "version": "6.3.2", "description": "Standalone plugin repository for future Rust-focused Codex skills.", "author": { "name": "Gale", diff --git a/plugins/spotify/.codex-plugin/plugin.json b/plugins/spotify/.codex-plugin/plugin.json index 6c38687c..9acca262 100644 --- a/plugins/spotify/.codex-plugin/plugin.json +++ b/plugins/spotify/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spotify", - "version": "6.3.1", + "version": "6.3.2", "description": "Placeholder plugin repository for future Spotify-focused Codex workflows.", "author": { "name": "Gale", diff --git a/plugins/things-app/.codex-plugin/plugin.json b/plugins/things-app/.codex-plugin/plugin.json index 99275e93..ebcab060 100644 --- a/plugins/things-app/.codex-plugin/plugin.json +++ b/plugins/things-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "things-app", - "version": "6.3.1", + "version": "6.3.2", "description": "Things.app skills and a bundled local MCP server for reminders, planning digests, and structured task workflows.", "author": { "name": "Gale", diff --git a/plugins/things-app/mcp/pyproject.toml b/plugins/things-app/mcp/pyproject.toml index 5e6d39b8..71f2d071 100644 --- a/plugins/things-app/mcp/pyproject.toml +++ b/plugins/things-app/mcp/pyproject.toml @@ -7,7 +7,7 @@ packages = ["app"] [project] name = "things-mcp" -version = "6.3.1" +version = "6.3.2" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/things-app/mcp/uv.lock b/plugins/things-app/mcp/uv.lock index 587c4c2e..7dd43292 100644 --- a/plugins/things-app/mcp/uv.lock +++ b/plugins/things-app/mcp/uv.lock @@ -1118,7 +1118,7 @@ wheels = [ [[package]] name = "things-mcp" -version = "6.3.1" +version = "6.3.2" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/things-app/pyproject.toml b/plugins/things-app/pyproject.toml index dcb245fb..7a0299b3 100644 --- a/plugins/things-app/pyproject.toml +++ b/plugins/things-app/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "things-app-maintenance" -version = "6.3.1" +version = "6.3.2" description = "Maintainer-only Python tooling baseline for things-app skills and plugin packaging." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/things-app/uv.lock b/plugins/things-app/uv.lock index 4ca295ac..15eb3694 100644 --- a/plugins/things-app/uv.lock +++ b/plugins/things-app/uv.lock @@ -120,7 +120,7 @@ wheels = [ [[package]] name = "things-app-maintenance" -version = "6.3.1" +version = "6.3.2" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/web-dev-skills/.codex-plugin/plugin.json b/plugins/web-dev-skills/.codex-plugin/plugin.json index a3364321..1f99981c 100644 --- a/plugins/web-dev-skills/.codex-plugin/plugin.json +++ b/plugins/web-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "web-dev-skills", - "version": "6.3.1", + "version": "6.3.2", "description": "Standalone plugin repository for future web-focused Codex skills.", "author": { "name": "Gale", diff --git a/pyproject.toml b/pyproject.toml index 9e2e25a4..738f6304 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "socket-maintenance" -version = "6.3.1" +version = "6.3.2" description = "Root uv tooling baseline for the socket superproject." requires-python = ">=3.11" dependencies = [] diff --git a/scripts/cleanup_legacy_socket_installs.py b/scripts/cleanup_legacy_socket_installs.py new file mode 100644 index 00000000..2b57a767 --- /dev/null +++ b/scripts/cleanup_legacy_socket_installs.py @@ -0,0 +1,300 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# /// +"""Clean up pre-Git-marketplace socket plugin install artifacts.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +KNOWN_SOCKET_PLUGINS = { + "agent-plugin-skills", + "apple-dev-skills", + "cardhop-app", + "dotnet-skills", + "productivity-skills", + "python-skills", + "rust-skills", + "speak-swiftly-server", + "spotify", + "things-app", + "web-dev-skills", +} + +CANONICAL_MARKETPLACE_NAMES = KNOWN_SOCKET_PLUGINS | {"socket"} + + +@dataclass(frozen=True) +class PlannedAction: + kind: str + target: Path + description: str + + +@dataclass(frozen=True) +class RewriteMarketplace: + path: Path + data: dict[str, Any] | None + removed_names: tuple[str, ...] + + +def load_json(path: Path) -> Any: + try: + return json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError: + return None + except json.JSONDecodeError as exc: + raise SystemExit( + f"Cannot inspect legacy marketplace because JSON is invalid at " + f"{path}:{exc.lineno}:{exc.colno}: {exc.msg}" + ) from exc + + +def plugin_name_from_manifest(plugin_root: Path) -> str | None: + manifest_path = plugin_root / ".codex-plugin" / "plugin.json" + manifest = load_json(manifest_path) + if not isinstance(manifest, dict): + return None + name = manifest.get("name") + if isinstance(name, str): + return name + return None + + +def is_legacy_socket_plugin_dir(path: Path) -> bool: + if not path.is_dir(): + return False + if "cache" in path.parts: + return False + return plugin_name_from_manifest(path) in KNOWN_SOCKET_PLUGINS + + +def is_legacy_socket_marketplace_entry(entry: object) -> bool: + if not isinstance(entry, dict): + return False + name = entry.get("name") + if name not in KNOWN_SOCKET_PLUGINS: + return False + source = entry.get("source") + if isinstance(source, str): + return source.startswith("./") or source.startswith("/") or source.startswith("~") + if not isinstance(source, dict): + return False + if source.get("source") != "local": + return False + path = source.get("path") + return isinstance(path, str) and ( + path.startswith("./") or path.startswith("/") or path.startswith("~") + ) + + +def plan_marketplace_cleanup(personal_marketplace: Path) -> RewriteMarketplace | None: + marketplace = load_json(personal_marketplace) + if marketplace is None: + return None + if not isinstance(marketplace, dict): + raise SystemExit( + f"Cannot inspect legacy marketplace because it is not a JSON object: " + f"{personal_marketplace}" + ) + + plugins = marketplace.get("plugins") + if not isinstance(plugins, list): + return None + + kept: list[object] = [] + removed: list[str] = [] + for entry in plugins: + if is_legacy_socket_marketplace_entry(entry): + name = entry.get("name") if isinstance(entry, dict) else None + removed.append(str(name)) + else: + kept.append(entry) + + if not removed: + return None + + if not kept and marketplace.get("name") == "socket": + return RewriteMarketplace(personal_marketplace, None, tuple(sorted(removed))) + + rewritten = dict(marketplace) + rewritten["plugins"] = kept + return RewriteMarketplace(personal_marketplace, rewritten, tuple(sorted(removed))) + + +def plan_plugin_dir_cleanup(codex_plugins_root: Path) -> list[PlannedAction]: + actions: list[PlannedAction] = [] + for plugin_name in sorted(KNOWN_SOCKET_PLUGINS): + plugin_dir = codex_plugins_root / plugin_name + if is_legacy_socket_plugin_dir(plugin_dir): + actions.append( + PlannedAction( + kind="remove-directory", + target=plugin_dir, + description=( + f"Remove copied legacy plugin payload `{plugin_name}` from " + f"{plugin_dir}" + ), + ) + ) + return actions + + +def stale_config_plugin_tables(config_path: Path) -> list[str]: + if not config_path.is_file(): + return [] + stale_tables: list[str] = [] + for line in config_path.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if not stripped.startswith("[plugins.") or not stripped.endswith("]"): + continue + table_name = stripped.removeprefix("[plugins.").removesuffix("]").strip('"') + if "@" not in table_name: + continue + plugin_name, marketplace_name = table_name.rsplit("@", 1) + if ( + plugin_name in KNOWN_SOCKET_PLUGINS + and marketplace_name not in CANONICAL_MARKETPLACE_NAMES + ): + stale_tables.append(table_name) + return stale_tables + + +def backup_path_for(target: Path, *, home: Path, backup_root: Path) -> Path: + try: + relative = target.resolve().relative_to(home.resolve()) + except ValueError: + relative = Path(target.name) + return backup_root / relative + + +def backup_target(target: Path, *, home: Path, backup_root: Path) -> Path: + destination = backup_path_for(target, home=home, backup_root=backup_root) + destination.parent.mkdir(parents=True, exist_ok=True) + if target.is_dir(): + shutil.copytree(target, destination) + else: + shutil.copy2(target, destination) + return destination + + +def apply_marketplace_rewrite(rewrite: RewriteMarketplace, *, home: Path, backup_root: Path) -> None: + backup_target(rewrite.path, home=home, backup_root=backup_root) + if rewrite.data is None: + rewrite.path.unlink() + return + rewrite.path.write_text(json.dumps(rewrite.data, indent=2) + "\n", encoding="utf-8") + + +def apply_action(action: PlannedAction, *, home: Path, backup_root: Path) -> None: + backup_target(action.target, home=home, backup_root=backup_root) + if action.kind == "remove-directory": + shutil.rmtree(action.target) + return + raise AssertionError(f"Unsupported action kind: {action.kind}") + + +def print_plan( + *, + marketplace_rewrite: RewriteMarketplace | None, + actions: list[PlannedAction], + stale_tables: list[str], + apply: bool, + backup_root: Path, +) -> None: + mode = "Applying" if apply else "Dry run" + print(f"{mode}: legacy socket install cleanup") + + if marketplace_rewrite is None and not actions and not stale_tables: + print("No legacy socket install artifacts were found.") + return + + if marketplace_rewrite is not None: + target = marketplace_rewrite.path + if marketplace_rewrite.data is None: + print(f"- Remove personal marketplace file after backing it up: {target}") + else: + print(f"- Rewrite personal marketplace after backing it up: {target}") + print( + " Removes legacy plugin entries: " + + ", ".join(marketplace_rewrite.removed_names) + ) + + for action in actions: + print(f"- {action.description}") + + if stale_tables: + print("- Stale non-socket plugin enablement entries found in config.toml:") + for table_name in stale_tables: + print(f" - [plugins.\"{table_name}\"]") + print(" These are reported only; this helper does not rewrite config.toml yet.") + + if apply: + print(f"Backups written under: {backup_root}") + else: + print("No files changed. Re-run with --apply to perform this cleanup.") + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Remove legacy copied socket plugin payloads and personal marketplace entries " + "after migrating to the Git-backed socket marketplace." + ) + ) + parser.add_argument( + "--home", + type=Path, + default=Path.home(), + help="Home directory to inspect. Defaults to the current user's home.", + ) + parser.add_argument( + "--apply", + action="store_true", + help="Back up and remove the detected legacy artifacts.", + ) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> None: + args = parse_args(sys.argv[1:] if argv is None else argv) + home = args.home.expanduser().resolve() + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + backup_root = home / ".codex" / "backups" / "socket-legacy-install-cleanup" / timestamp + + personal_marketplace = home / ".agents" / "plugins" / "marketplace.json" + codex_plugins_root = home / ".codex" / "plugins" + config_path = home / ".codex" / "config.toml" + + marketplace_rewrite = plan_marketplace_cleanup(personal_marketplace) + actions = plan_plugin_dir_cleanup(codex_plugins_root) + stale_tables = stale_config_plugin_tables(config_path) + + print_plan( + marketplace_rewrite=marketplace_rewrite, + actions=actions, + stale_tables=stale_tables, + apply=args.apply, + backup_root=backup_root, + ) + + if not args.apply: + return + + if marketplace_rewrite is not None: + apply_marketplace_rewrite(marketplace_rewrite, home=home, backup_root=backup_root) + for action in actions: + apply_action(action, home=home, backup_root=backup_root) + + +if __name__ == "__main__": + main() diff --git a/tests/test_cleanup_legacy_socket_installs.py b/tests/test_cleanup_legacy_socket_installs.py new file mode 100644 index 00000000..4b13144a --- /dev/null +++ b/tests/test_cleanup_legacy_socket_installs.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + + +MODULE_PATH = ( + Path(__file__).resolve().parent.parent / "scripts" / "cleanup_legacy_socket_installs.py" +) +SPEC = importlib.util.spec_from_file_location("cleanup_legacy_socket_installs", MODULE_PATH) +assert SPEC and SPEC.loader +cleanup_legacy_socket_installs = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = cleanup_legacy_socket_installs +SPEC.loader.exec_module(cleanup_legacy_socket_installs) + + +def write_json(path: Path, value: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +def write_plugin_manifest(plugin_root: Path, name: str) -> None: + write_json(plugin_root / ".codex-plugin" / "plugin.json", {"name": name}) + + +def test_plan_marketplace_cleanup_removes_only_known_socket_plugins(tmp_path: Path) -> None: + marketplace_path = tmp_path / ".agents" / "plugins" / "marketplace.json" + write_json( + marketplace_path, + { + "name": "personal", + "plugins": [ + { + "name": "apple-dev-skills", + "source": { + "source": "local", + "path": "/Users/example/socket/plugins/apple-dev-skills", + }, + }, + { + "name": "unrelated", + "source": { + "source": "local", + "path": "/Users/example/.codex/plugins/unrelated", + }, + }, + ], + }, + ) + + rewrite = cleanup_legacy_socket_installs.plan_marketplace_cleanup(marketplace_path) + + assert rewrite is not None + assert rewrite.removed_names == ("apple-dev-skills",) + assert rewrite.data is not None + assert rewrite.data["plugins"] == [ + { + "name": "unrelated", + "source": { + "source": "local", + "path": "/Users/example/.codex/plugins/unrelated", + }, + } + ] + + +def test_plan_marketplace_cleanup_deletes_socket_only_marketplace(tmp_path: Path) -> None: + marketplace_path = tmp_path / ".agents" / "plugins" / "marketplace.json" + write_json( + marketplace_path, + { + "name": "socket", + "plugins": [ + { + "name": "agent-plugin-skills", + "source": { + "source": "local", + "path": "/Users/example/socket/plugins/agent-plugin-skills", + }, + } + ], + }, + ) + + rewrite = cleanup_legacy_socket_installs.plan_marketplace_cleanup(marketplace_path) + + assert rewrite is not None + assert rewrite.data is None + assert rewrite.removed_names == ("agent-plugin-skills",) + + +def test_plan_plugin_dir_cleanup_skips_cache_and_unknown_payloads(tmp_path: Path) -> None: + codex_plugins_root = tmp_path / ".codex" / "plugins" + write_plugin_manifest(codex_plugins_root / "apple-dev-skills", "apple-dev-skills") + write_plugin_manifest(codex_plugins_root / "unrelated", "unrelated") + write_plugin_manifest( + codex_plugins_root / "cache" / "socket" / "things-app" / "6.3.1", + "things-app", + ) + + actions = cleanup_legacy_socket_installs.plan_plugin_dir_cleanup(codex_plugins_root) + + assert [action.target for action in actions] == [codex_plugins_root / "apple-dev-skills"] + + +def test_apply_backs_up_and_removes_legacy_directory(tmp_path: Path) -> None: + home = tmp_path + backup_root = home / ".codex" / "backups" / "test" + plugin_dir = home / ".codex" / "plugins" / "python-skills" + write_plugin_manifest(plugin_dir, "python-skills") + action = cleanup_legacy_socket_installs.PlannedAction( + kind="remove-directory", + target=plugin_dir, + description="remove test plugin", + ) + + cleanup_legacy_socket_installs.apply_action(action, home=home, backup_root=backup_root) + + assert not plugin_dir.exists() + assert ( + backup_root + / ".codex" + / "plugins" + / "python-skills" + / ".codex-plugin" + / "plugin.json" + ).is_file() + + +def test_stale_config_plugin_tables_reports_non_socket_marketplaces(tmp_path: Path) -> None: + config_path = tmp_path / ".codex" / "config.toml" + config_path.parent.mkdir(parents=True) + config_path.write_text( + "\n".join( + [ + '[plugins."apple-dev-skills@socket"]', + "enabled = true", + '[plugins."apple-dev-skills@apple-dev-skills"]', + "enabled = true", + '[plugins."apple-dev-skills@local-repo"]', + "enabled = true", + '[plugins."unrelated@local-repo"]', + "enabled = true", + ] + ) + + "\n", + encoding="utf-8", + ) + + stale_tables = cleanup_legacy_socket_installs.stale_config_plugin_tables(config_path) + + assert stale_tables == ["apple-dev-skills@local-repo"] diff --git a/uv.lock b/uv.lock index 53703e6d..89f827dd 100644 --- a/uv.lock +++ b/uv.lock @@ -286,7 +286,7 @@ wheels = [ [[package]] name = "socket-maintenance" -version = "6.3.1" +version = "6.3.2" source = { virtual = "." } [package.dev-dependencies]