Files
LEDMatrix/scripts/setup_plugin_repos.py
Chuck 9d3bc55c18 fix: post-merge monorepo hardening and cleanup (#239)
* fix: address PR review nitpicks for monorepo hardening

- Add docstring note about regex limitation in parse_json_with_trailing_commas
- Abort on zip-slip in ZIP installer instead of skipping (consistent with API installer)
- Use _safe_remove_directory for non-git plugin reinstall path
- Use segment-wise encodeURIComponent for View button URL encoding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: check _safe_remove_directory result before reinstalling plugin

Avoid calling install_plugin into a partially-removed directory by
checking the boolean return of _safe_remove_directory, mirroring the
guard already used in the git-remote migration path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: normalize subpath prefix and add zip-slip guard to download installer

- Strip trailing slashes from plugin_subpath before building the tree
  filter prefix, preventing double-slash ("subpath//") that would cause
  file_entries to silently miss all matches.
- Add zip-slip protection to _install_via_download (extractall path),
  matching the guard already present in _install_from_monorepo_zip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 11:59:23 -05:00

94 lines
2.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Setup plugin repository symlinks for local development.
Creates symlinks in plugin-repos/ pointing to plugin directories
in the ledmatrix-plugins monorepo.
"""
import json
import os
import re
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
PLUGIN_REPOS_DIR = PROJECT_ROOT / "plugin-repos"
MONOREPO_PLUGINS = PROJECT_ROOT.parent / "ledmatrix-plugins" / "plugins"
def parse_json_with_trailing_commas(text: str) -> dict:
"""Parse JSON that may have trailing commas.
Note: The regex also matches commas inside string values (e.g., "hello, }").
This is fine for manifest files but may corrupt complex JSON with such patterns.
"""
text = re.sub(r",\s*([}\]])", r"\1", text)
return json.loads(text)
def create_symlinks() -> bool:
"""Create symlinks in plugin-repos/ pointing to monorepo plugin dirs."""
if not MONOREPO_PLUGINS.exists():
print(f"Error: Monorepo plugins directory not found: {MONOREPO_PLUGINS}")
return False
PLUGIN_REPOS_DIR.mkdir(exist_ok=True)
created = 0
skipped = 0
print("Setting up plugin symlinks...")
print(f" Source: {MONOREPO_PLUGINS}")
print(f" Links: {PLUGIN_REPOS_DIR}")
print()
for plugin_dir in sorted(MONOREPO_PLUGINS.iterdir()):
if not plugin_dir.is_dir():
continue
manifest_path = plugin_dir / "manifest.json"
if not manifest_path.exists():
continue
try:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = parse_json_with_trailing_commas(f.read())
except (OSError, json.JSONDecodeError) as e:
print(f" {plugin_dir.name} - failed to read {manifest_path}: {e}")
continue
plugin_id = manifest.get("id", plugin_dir.name)
link_path = PLUGIN_REPOS_DIR / plugin_id
if link_path.exists() or link_path.is_symlink():
if link_path.is_symlink():
try:
if link_path.resolve() == plugin_dir.resolve():
skipped += 1
continue
else:
link_path.unlink()
except OSError:
link_path.unlink()
else:
print(f" {plugin_id} - exists but is not a symlink, skipping")
skipped += 1
continue
relative_path = os.path.relpath(plugin_dir, link_path.parent)
link_path.symlink_to(relative_path)
print(f" {plugin_id} - linked")
created += 1
print(f"\nCreated {created} links, skipped {skipped}")
return True
def main():
print("Setting up plugin repository symlinks from monorepo...\n")
if not create_symlinks():
sys.exit(1)
if __name__ == "__main__":
main()