Skip to content

Commit b8ea144

Browse files
thschaffrthschaffr
authored andcommitted
This commit adds an opt-in --update-cache flag to src/convert_to_ide_formats.py to refresh the local Claude Code plugin cache after a successful build.
After the normal build pipeline succeeds, --update-cache mirrors the generated skills/software-security/ tree into: ~/.claude/plugins/cache/project-codeguard/codeguard-security/<version>/ skills/software-security/ so locally edited rules can be exercised in Claude Code immediately, without waiting for a marketplace release. Behavior: - Off by default; no change to standard CI builds. - Marketplace name (project-codeguard) and plugin name (codeguard-security) match .claude-plugin/marketplace.json and .claude-plugin/plugin.json. - Version is read from pyproject.toml via the existing helper and validated against [A-Za-z0-9._-]+ before being spliced into a filesystem path (defense-in-depth against path traversal). - The versioned cache directory is fully replaced (rmtree of cache_base followed by copytree) so stale rules from a previous build can't linger. This is not atomic: there is a brief window where the directory is missing or partially written. The flag is intended for dev use where this is acceptable. - All shutil operations are wrapped in try/except; failures exit non-zero with a clear message. No new dependencies; only stdlib (re, shutil, pathlib).
1 parent dddb5c3 commit b8ea144

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

src/convert_to_ide_formats.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,81 @@ def matches_tag_filter(rule_tags: list[str], filter_tags: list[str]) -> bool:
6565
return all(tag in rule_tags for tag in filter_tags)
6666

6767

68+
def update_claude_cache(version: str) -> bool:
69+
"""Refresh the local Claude Code plugin cache with generated rules.
70+
71+
Copies ``skills/software-security/`` into Claude Code's cache directory
72+
so locally modified or custom rules can be tested without waiting for a
73+
marketplace release.
74+
75+
Cache path mirrors the marketplace layout:
76+
``~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/software-security``
77+
78+
Args:
79+
version: Version string from pyproject.toml (e.g., "1.3.1")
80+
81+
Returns:
82+
True on success, False on any failure (logs an error message).
83+
"""
84+
# Marketplace and plugin names are pinned to the local manifest so the
85+
# path stays in sync if the marketplace structure ever changes.
86+
marketplace_name = "project-codeguard"
87+
plugin_name = "codeguard-security"
88+
89+
# Validate the version string against an allow-list before splicing it
90+
# into a filesystem path (defense-in-depth against path traversal).
91+
if not re.fullmatch(r"[A-Za-z0-9._-]+", version):
92+
print(f"❌ Refusing to update cache: invalid version format '{version}'")
93+
return False
94+
95+
source_dir = PROJECT_ROOT / "skills" / "software-security"
96+
if not source_dir.exists():
97+
print(f"❌ Source directory not found: {source_dir}")
98+
return False
99+
100+
cache_base = (
101+
Path.home()
102+
/ ".claude"
103+
/ "plugins"
104+
/ "cache"
105+
/ marketplace_name
106+
/ plugin_name
107+
/ version
108+
)
109+
cache_dir = cache_base / "skills" / "software-security"
110+
111+
try:
112+
if cache_base.exists():
113+
shutil.rmtree(cache_base)
114+
print(f"🗑️ Cleared existing cache: {cache_base}")
115+
except OSError as exc:
116+
print(f"❌ Failed to clear existing cache: {exc}")
117+
return False
118+
119+
try:
120+
cache_dir.parent.mkdir(parents=True, exist_ok=True)
121+
shutil.copytree(source_dir, cache_dir)
122+
except (OSError, shutil.Error) as exc:
123+
print(f"❌ Failed to copy to cache: {exc}")
124+
return False
125+
126+
rules_dir = cache_dir / "rules"
127+
if not rules_dir.exists():
128+
print("⚠️ Warning: rules directory missing after copy")
129+
return False
130+
131+
rule_count = sum(1 for _ in rules_dir.glob("*.md"))
132+
if rule_count == 0:
133+
print("⚠️ Warning: no rules found in cache after copy")
134+
return False
135+
136+
print(f"✅ Updated cache: {cache_dir}")
137+
print(f" → {rule_count} rules copied")
138+
if (cache_dir / "SKILL.md").exists():
139+
print(" → SKILL.md copied")
140+
return True
141+
142+
68143
def update_skill_md(language_to_rules: dict[str, list[str]], skill_path: Path) -> None:
69144
"""
70145
Update SKILL.md with language-to-rules mapping table.
@@ -326,6 +401,16 @@ def _resolve_source_paths(args) -> list[Path]:
326401
dest="tags",
327402
help="Filter rules by tags (comma-separated, case-insensitive, AND logic). Example: --tag api,web-security",
328403
)
404+
parser.add_argument(
405+
"--update-cache",
406+
action="store_true",
407+
help=(
408+
"After a successful build, refresh the local Claude Code plugin "
409+
"cache (~/.claude/plugins/cache/project-codeguard/codeguard-security/"
410+
"<version>/skills/software-security) so modified rules are picked "
411+
"up immediately without waiting for a marketplace release."
412+
),
413+
)
329414

330415
cli_args = parser.parse_args()
331416
try:
@@ -445,3 +530,12 @@ def _resolve_source_paths(args) -> list[Path]:
445530
sync_plugin_metadata(version)
446531

447532
print("✅ All conversions successful")
533+
534+
# Optional developer convenience: refresh the local Claude Code plugin
535+
# cache so the just-built rules can be exercised in Claude Code without
536+
# waiting for a marketplace release. Off by default.
537+
if cli_args.update_cache:
538+
print("\nUpdating Claude Code plugin cache...")
539+
if not update_claude_cache(version):
540+
print("❌ Failed to update cache")
541+
sys.exit(1)

0 commit comments

Comments
 (0)