forked from fcakyon/claude-codex-settings
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync_marketplace_to_plugins.py
More file actions
executable file
·108 lines (86 loc) · 3.82 KB
/
Copy pathsync_marketplace_to_plugins.py
File metadata and controls
executable file
·108 lines (86 loc) · 3.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/env python3
"""Sync marketplace.json plugin entries to individual plugin.json files across Claude Code, Codex CLI, and Cursor."""
import json
import sys
from pathlib import Path
def get_edited_file_path():
"""Extract file path from hook input."""
try:
input_data = json.load(sys.stdin)
return input_data.get("tool_input", {}).get("file_path", "")
except (json.JSONDecodeError, KeyError):
return ""
def sync_marketplace_to_plugins():
"""Sync marketplace.json entries to individual plugin.json files."""
edited_path = get_edited_file_path()
# Only trigger for marketplace.json edits
if not edited_path.endswith("marketplace.json"):
return 0
marketplace_path = Path(edited_path)
if not marketplace_path.exists():
return 0
try:
marketplace = json.loads(marketplace_path.read_text())
except (json.JSONDecodeError, OSError) as e:
print(f"❌ Failed to read marketplace.json: {e}", file=sys.stderr)
return 2
plugins = marketplace.get("plugins", [])
if not plugins:
return 0
marketplace_dir = marketplace_path.parent.parent # Go up from .claude-plugin/
synced = []
for plugin in plugins:
source = plugin.get("source")
if not source:
continue
# Resolve plugin directory relative to marketplace root
plugin_dir = (marketplace_dir / source).resolve()
plugin_json_dir = plugin_dir / ".claude-plugin"
plugin_json_path = plugin_json_dir / "plugin.json"
# Merge marketplace fields into existing plugin.json (preserve local-only fields)
plugin_json_dir.mkdir(parents=True, exist_ok=True)
current_data = {}
if plugin_json_path.exists():
try:
current_data = json.loads(plugin_json_path.read_text())
except json.JSONDecodeError:
pass
plugin_data = dict(current_data)
plugin_data["name"] = plugin.get("name", "")
for field in ["version", "description"]:
if field in plugin:
plugin_data[field] = plugin[field]
if current_data != plugin_data:
plugin_json_path.write_text(json.dumps(plugin_data, indent=2) + "\n")
synced.append(plugin.get("name", source))
# Sync to Codex and Cursor manifests (merge, same as Claude)
for tool_dir in [".codex-plugin", ".cursor-plugin"]:
tool_json_dir = plugin_dir / tool_dir
tool_json_path = tool_json_dir / "plugin.json"
tool_json_dir.mkdir(parents=True, exist_ok=True)
tool_current = {}
if tool_json_path.exists():
try:
tool_current = json.loads(tool_json_path.read_text())
except json.JSONDecodeError:
pass
tool_merged = dict(tool_current)
tool_merged.update({k: plugin_data[k] for k in ["name", "version", "description"] if k in plugin_data})
if tool_current != tool_merged:
tool_json_path.write_text(json.dumps(tool_merged, indent=2) + "\n")
# Sync gemini-extension.json (minimal: name, version, description)
gemini_path = plugin_dir / "gemini-extension.json"
gemini_data = {k: plugin_data[k] for k in ["name", "version", "description"] if k in plugin_data}
gemini_current = {}
if gemini_path.exists():
try:
gemini_current = json.loads(gemini_path.read_text())
except json.JSONDecodeError:
pass
if gemini_current != gemini_data:
gemini_path.write_text(json.dumps(gemini_data, indent=2) + "\n")
if synced:
print(f"✓ Synced {len(synced)} plugin manifest(s): {', '.join(synced)}")
return 0
if __name__ == "__main__":
sys.exit(sync_marketplace_to_plugins())