Feat/monorepo migration (#238)

* feat: adapt LEDMatrix for monorepo plugin architecture

Update store_manager to fetch manifests from subdirectories within the
monorepo (plugin_path/manifest.json) instead of repo root. Remove 21
plugin submodule entries from .gitmodules, simplify workspace file to
reference the monorepo, and clean up scripts for the new layout.

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

* fix: auto-reinstall plugins when registry repo URL changes

When a user clicks "Update" on a git-cloned plugin, detect if the
local git remote URL no longer matches the registry's repo URL (e.g.
after monorepo migration). Instead of pulling from the stale archived
repo, automatically remove and reinstall from the new registry source.

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

* fix: plugin store "View" button links to correct monorepo subdirectory

When a plugin has a plugin_path (monorepo plugin), construct the GitHub
URL as repo/tree/main/plugin_path so users land on the specific plugin
directory. Pass plugin_path through the store API response to the
frontend.

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

* fix: monorepo manifest fetch in search + version-based update detection

Fix search_plugins() to pass plugin_path when fetching manifests from
GitHub, matching the fix already in get_plugin_info(). Without this,
monorepo plugin descriptions 404 in search results.

Add version comparison for non-git plugins (monorepo installs) so
"Update All" skips plugins already at latest_version instead of blindly
reinstalling every time.

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

* feat: show plugin version instead of misleading monorepo commit info

Replace commit hash, date, and stars on plugin cards with the plugin's
version number. In a monorepo all plugins share the same commit history
and star count, making those fields identical and misleading. Version
is the meaningful per-plugin signal users care about.

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

* docs: add CLAUDE.md with project structure and plugin store docs

Documents plugin store architecture, monorepo install flow, version-
based update detection, and the critical version bump workflow.

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

* perf: extract only target plugin from monorepo ZIP instead of all files

Previously _install_from_monorepo() called extractall() on the entire
monorepo ZIP (~13MB, 600+ files) just to grab one plugin subdirectory.
Now filter zip members by the plugin prefix and extract only matching
files, reducing disk I/O by ~96% per install/update.

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

* perf: download only target plugin files via GitHub Trees API

Replace full monorepo ZIP download (~5MB) with targeted file downloads
(~200KB per plugin) using the GitHub Git Trees API for directory listing
and raw.githubusercontent.com for individual file content.

One API call fetches the repo tree, client filters for the target
plugin's files, then downloads each file individually. Falls back to
ZIP if the API is unavailable (rate limited, no network, etc.).

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

* fix: clean up partial files between API and ZIP install fallbacks

Ensure target_path is fully removed before the ZIP fallback runs, and
before shutil.move() in the ZIP method. Prevents directory nesting if
the API method creates target_path then fails mid-download.

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

* fix: harden scripts and fix monorepo URL handling

- setup_plugin_repos.py: add type hints, remove unnecessary f-string,
  wrap manifest parsing in try/except to skip malformed manifests
- update_plugin_repos.py: add 120s timeout to git pull with
  TimeoutExpired handling
- store_manager.py: fix rstrip('.zip') stripping valid branch chars,
  use removesuffix('.zip'); remove redundant import json
- plugins_manager.js: View button uses dynamic branch, disables when
  repo is missing, encodes plugin_path in URL
- CLAUDE.md: document plugin repo naming convention

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

* fix: harden monorepo install security and cleanup

- store_manager: fix temp dir leak in _install_from_monorepo_zip by
  moving cleanup to finally block
- store_manager: add zip-slip guard validating extracted paths stay
  inside temp directory
- store_manager: add 500-file sanity cap to API-based install
- store_manager: extract _normalize_repo_url as @staticmethod
- setup_plugin_repos: propagate create_symlinks() failure via sys.exit,
  narrow except to OSError

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

* fix: add path traversal guard to API-based monorepo installer

Validate that each file's resolved destination stays inside
target_path before creating directories or writing bytes, mirroring
the zip-slip guard in _install_from_monorepo_zip.

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

* fix: use _safe_remove_directory for monorepo migration cleanup

Replace shutil.rmtree(ignore_errors=True) with _safe_remove_directory
which handles permission errors gracefully and returns status, preventing
install_plugin from running against a partially-removed directory.

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>
This commit is contained in:
Chuck
2026-02-11 18:57:30 -05:00
committed by GitHub
parent 448a15c1e6
commit df3cf9bb56
9 changed files with 392 additions and 783 deletions

View File

@@ -1,343 +0,0 @@
#!/bin/bash
# Script to normalize all plugins as git submodules
# This ensures uniform plugin management across the repository
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PLUGINS_DIR="$PROJECT_ROOT/plugins"
GITMODULES="$PROJECT_ROOT/.gitmodules"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if a plugin is in .gitmodules
is_in_gitmodules() {
local plugin_path="$1"
git config -f "$GITMODULES" --get-regexp "^submodule\." | grep -q "path = $plugin_path$" || return 1
}
# Get submodule URL from .gitmodules
get_submodule_url() {
local plugin_path="$1"
git config -f "$GITMODULES" "submodule.$plugin_path.url" 2>/dev/null || echo ""
}
# Check if directory is a git repo
is_git_repo() {
[[ -d "$1/.git" ]]
}
# Get git remote URL
get_git_remote() {
local plugin_dir="$1"
if is_git_repo "$plugin_dir"; then
(cd "$plugin_dir" && git remote get-url origin 2>/dev/null || echo "")
else
echo ""
fi
}
# Check if directory is a symlink
is_symlink() {
[[ -L "$1" ]]
}
# Check if plugin has GitHub repo
has_github_repo() {
local plugin_name="$1"
local url="https://github.com/ChuckBuilds/ledmatrix-$plugin_name"
local status=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "0")
[[ "$status" == "200" ]]
}
# Update .gitignore to allow a plugin submodule
update_gitignore() {
local plugin_name="$1"
local plugin_path="plugins/$plugin_name"
local gitignore="$PROJECT_ROOT/.gitignore"
# Check if already in .gitignore exceptions
if grep -q "!plugins/$plugin_name$" "$gitignore" 2>/dev/null; then
log_info "Plugin $plugin_name already in .gitignore exceptions"
return 0
fi
# Find the line with the last plugin exception
local last_line=$(grep -n "!plugins/" "$gitignore" | tail -1 | cut -d: -f1)
if [[ -z "$last_line" ]]; then
log_warn "Could not find plugin exceptions in .gitignore"
return 1
fi
# Add exceptions after the last plugin exception
log_info "Updating .gitignore to allow $plugin_name submodule"
sed -i "${last_line}a!plugins/$plugin_name\n!plugins/$plugin_name/" "$gitignore"
log_success "Updated .gitignore for $plugin_name"
}
# Re-initialize a submodule that appears as regular directory
reinit_submodule() {
local plugin_name="$1"
local plugin_path="plugins/$plugin_name"
local plugin_dir="$PLUGINS_DIR/$plugin_name"
log_info "Re-initializing submodule: $plugin_name"
if ! is_in_gitmodules "$plugin_path"; then
log_error "Plugin $plugin_name is not in .gitmodules"
return 1
fi
local submodule_url=$(get_submodule_url "$plugin_path")
if [[ -z "$submodule_url" ]]; then
log_error "Could not find URL for $plugin_name in .gitmodules"
return 1
fi
# If it's a symlink, remove it first
if is_symlink "$plugin_dir"; then
log_warn "Removing symlink: $plugin_dir"
rm "$plugin_dir"
fi
# If it's a regular directory with .git, we need to handle it carefully
if is_git_repo "$plugin_dir"; then
local remote_url=$(get_git_remote "$plugin_dir")
if [[ "$remote_url" == "$submodule_url" ]] || [[ "$remote_url" == "${submodule_url%.git}" ]] || [[ "${submodule_url%.git}" == "$remote_url" ]]; then
log_info "Directory is already the correct git repo, re-initializing submodule..."
# Remove from git index and re-add as submodule
git rm --cached "$plugin_path" 2>/dev/null || true
rm -rf "$plugin_dir"
else
log_warn "Directory has different remote ($remote_url vs $submodule_url)"
log_warn "Backing up to ${plugin_dir}.backup"
mv "$plugin_dir" "${plugin_dir}.backup"
fi
fi
# Re-add as submodule (use -f to force if needed)
if git submodule add -f "$submodule_url" "$plugin_path" 2>/dev/null; then
log_info "Submodule added successfully"
else
log_info "Submodule already exists, updating..."
git submodule update --init "$plugin_path"
fi
log_success "Re-initialized submodule: $plugin_name"
}
# Convert standalone git repo to submodule
convert_to_submodule() {
local plugin_name="$1"
local plugin_path="plugins/$plugin_name"
local plugin_dir="$PLUGINS_DIR/$plugin_name"
log_info "Converting to submodule: $plugin_name"
if is_in_gitmodules "$plugin_path"; then
log_warn "Plugin $plugin_name is already in .gitmodules, re-initializing instead"
reinit_submodule "$plugin_name"
return 0
fi
if ! is_git_repo "$plugin_dir"; then
log_error "Plugin $plugin_name is not a git repository"
return 1
fi
local remote_url=$(get_git_remote "$plugin_dir")
if [[ -z "$remote_url" ]]; then
log_error "Plugin $plugin_name has no remote URL"
return 1
fi
# If it's a symlink, we need to handle it differently
if is_symlink "$plugin_dir"; then
local target=$(readlink -f "$plugin_dir")
log_warn "Plugin is a symlink to $target"
log_warn "Removing symlink and adding as submodule"
rm "$plugin_dir"
# Update .gitignore first
update_gitignore "$plugin_name"
# Add as submodule
if git submodule add -f "$remote_url" "$plugin_path"; then
log_success "Added submodule: $plugin_name"
return 0
else
log_error "Failed to add submodule"
return 1
fi
fi
# Backup the directory
log_info "Backing up existing directory to ${plugin_dir}.backup"
mv "$plugin_dir" "${plugin_dir}.backup"
# Remove from git index
git rm --cached "$plugin_path" 2>/dev/null || true
# Update .gitignore first
update_gitignore "$plugin_name"
# Add as submodule (use -f to force if .gitignore blocks it)
if git submodule add -f "$remote_url" "$plugin_path"; then
log_success "Converted to submodule: $plugin_name"
log_warn "Backup saved at ${plugin_dir}.backup - you can remove it after verifying"
else
log_error "Failed to add submodule"
log_warn "Restoring backup..."
mv "${plugin_dir}.backup" "$plugin_dir"
return 1
fi
}
# Add new submodule for plugin with GitHub repo
add_new_submodule() {
local plugin_name="$1"
local plugin_path="plugins/$plugin_name"
local plugin_dir="$PLUGINS_DIR/$plugin_name"
local repo_url="https://github.com/ChuckBuilds/ledmatrix-$plugin_name.git"
log_info "Adding new submodule: $plugin_name"
if is_in_gitmodules "$plugin_path"; then
log_warn "Plugin $plugin_name is already in .gitmodules"
return 0
fi
if [[ -e "$plugin_dir" ]]; then
if is_symlink "$plugin_dir"; then
log_warn "Removing symlink: $plugin_dir"
rm "$plugin_dir"
elif is_git_repo "$plugin_dir"; then
log_warn "Directory exists as git repo, converting instead"
convert_to_submodule "$plugin_name"
return 0
else
log_warn "Backing up existing directory to ${plugin_dir}.backup"
mv "$plugin_dir" "${plugin_dir}.backup"
fi
fi
# Remove from git index if it exists
git rm --cached "$plugin_path" 2>/dev/null || true
# Update .gitignore first
update_gitignore "$plugin_name"
# Add as submodule (use -f to force if .gitignore blocks it)
if git submodule add -f "$repo_url" "$plugin_path"; then
log_success "Added new submodule: $plugin_name"
else
log_error "Failed to add submodule"
if [[ -e "${plugin_dir}.backup" ]]; then
log_warn "Restoring backup..."
mv "${plugin_dir}.backup" "$plugin_dir"
fi
return 1
fi
}
# Main processing function
main() {
cd "$PROJECT_ROOT"
log_info "Normalizing all plugins as git submodules..."
echo
# Step 1: Re-initialize submodules that appear as regular directories
log_info "Step 1: Re-initializing existing submodules..."
for plugin in basketball-scoreboard calendar clock-simple odds-ticker olympics-countdown soccer-scoreboard text-display mqtt-notifications; do
if [[ -d "$PLUGINS_DIR/$plugin" ]] && is_in_gitmodules "plugins/$plugin"; then
if ! git submodule status "plugins/$plugin" >/dev/null 2>&1; then
reinit_submodule "$plugin"
else
log_info "Submodule $plugin is already properly initialized"
fi
fi
done
echo
# Step 2: Convert standalone git repos to submodules
log_info "Step 2: Converting standalone git repos to submodules..."
for plugin in baseball-scoreboard ledmatrix-stocks; do
if [[ -d "$PLUGINS_DIR/$plugin" ]] && is_git_repo "$PLUGINS_DIR/$plugin"; then
if ! is_in_gitmodules "plugins/$plugin"; then
convert_to_submodule "$plugin"
fi
fi
done
echo
# Step 2b: Convert symlinks to submodules
log_info "Step 2b: Converting symlinks to submodules..."
for plugin in christmas-countdown ledmatrix-music static-image; do
if [[ -L "$PLUGINS_DIR/$plugin" ]]; then
if ! is_in_gitmodules "plugins/$plugin"; then
convert_to_submodule "$plugin"
fi
fi
done
echo
# Step 3: Add new submodules for plugins with GitHub repos
log_info "Step 3: Adding new submodules for plugins with GitHub repos..."
for plugin in football-scoreboard hockey-scoreboard; do
if [[ -d "$PLUGINS_DIR/$plugin" ]] && has_github_repo "$plugin"; then
if ! is_in_gitmodules "plugins/$plugin"; then
add_new_submodule "$plugin"
fi
fi
done
echo
# Step 4: Report on plugins without GitHub repos
log_info "Step 4: Checking plugins without GitHub repos..."
for plugin in ledmatrix-flights ledmatrix-leaderboard ledmatrix-weather; do
if [[ -d "$PLUGINS_DIR/$plugin" ]]; then
if ! is_in_gitmodules "plugins/$plugin" && ! is_git_repo "$PLUGINS_DIR/$plugin"; then
log_warn "Plugin $plugin has no GitHub repo and is not a git repo"
log_warn " This plugin may be local-only or needs a repository created"
fi
fi
done
echo
# Final: Initialize all submodules
log_info "Finalizing: Initializing all submodules..."
git submodule update --init --recursive
log_success "Plugin normalization complete!"
log_info "Run 'git status' to see changes"
log_info "Run 'git submodule status' to verify all submodules"
}
main "$@"

View File

@@ -1,151 +1,89 @@
#!/usr/bin/env python3
"""
Setup plugin repository references for multi-root workspace.
Setup plugin repository symlinks for local development.
This script creates symlinks in plugin-repos/ pointing to the actual
plugin repositories in the parent directory, allowing the system to
find plugins without modifying the LEDMatrix project structure.
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
# Paths
PROJECT_ROOT = Path(__file__).parent.parent
PLUGIN_REPOS_DIR = PROJECT_ROOT / "plugin-repos"
GITHUB_DIR = PROJECT_ROOT.parent
CONFIG_FILE = PROJECT_ROOT / "config" / "config.json"
MONOREPO_PLUGINS = PROJECT_ROOT.parent / "ledmatrix-plugins" / "plugins"
def get_workspace_plugins():
"""Get list of plugins from workspace file."""
workspace_file = PROJECT_ROOT / "LEDMatrix.code-workspace"
if not workspace_file.exists():
return []
try:
with open(workspace_file, 'r') as f:
workspace = json.load(f)
except json.JSONDecodeError as e:
print(f"Error: Failed to parse workspace file {workspace_file}: {e}")
print("Please check that the workspace file contains valid JSON.")
return []
plugins = []
for folder in workspace.get('folders', []):
path = folder.get('path', '')
if path.startswith('../') and path != '../ledmatrix-plugins':
plugin_name = path.replace('../', '')
plugins.append({
'name': plugin_name,
'workspace_path': path,
'actual_path': GITHUB_DIR / plugin_name,
'link_path': PLUGIN_REPOS_DIR / plugin_name
})
return plugins
def parse_json_with_trailing_commas(text: str) -> dict:
"""Parse JSON that may have trailing commas."""
text = re.sub(r",\s*([}\]])", r"\1", text)
return json.loads(text)
def create_symlinks():
"""Create symlinks in plugin-repos/ pointing to actual repos."""
plugins = get_workspace_plugins()
if not plugins:
print("No plugins found in workspace configuration")
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
# Ensure plugin-repos directory exists
PLUGIN_REPOS_DIR.mkdir(exist_ok=True)
created = 0
skipped = 0
errors = 0
print(f"Setting up plugin repository links...")
print(f" Source: {GITHUB_DIR}")
print("Setting up plugin symlinks...")
print(f" Source: {MONOREPO_PLUGINS}")
print(f" Links: {PLUGIN_REPOS_DIR}")
print()
for plugin in plugins:
actual_path = plugin['actual_path']
link_path = plugin['link_path']
if not actual_path.exists():
print(f" ⚠️ {plugin['name']} - source not found: {actual_path}")
errors += 1
for plugin_dir in sorted(MONOREPO_PLUGINS.iterdir()):
if not plugin_dir.is_dir():
continue
# Remove existing link/file if it exists
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():
# Check if it points to the right place
try:
if link_path.resolve() == actual_path.resolve():
print(f"{plugin['name']} - link already exists")
if link_path.resolve() == plugin_dir.resolve():
skipped += 1
continue
else:
# Remove old symlink pointing elsewhere
link_path.unlink()
except Exception as e:
print(f" ⚠️ {plugin['name']} - error checking link: {e}")
except OSError:
link_path.unlink()
else:
# It's a directory/file, not a symlink
print(f" ⚠️ {plugin['name']} - {link_path.name} exists but is not a symlink")
print(f" Skipping (manual cleanup required)")
print(f" {plugin_id} - exists but is not a symlink, skipping")
skipped += 1
continue
# Create symlink
try:
# Use relative path for symlink portability
relative_path = os.path.relpath(actual_path, link_path.parent)
link_path.symlink_to(relative_path)
print(f"{plugin['name']} - linked")
created += 1
except Exception as e:
print(f"{plugin['name']} - failed to create link: {e}")
errors += 1
print()
print(f"✅ Created {created} links, skipped {skipped}, errors {errors}")
return errors == 0
relative_path = os.path.relpath(plugin_dir, link_path.parent)
link_path.symlink_to(relative_path)
print(f" {plugin_id} - linked")
created += 1
def update_config_path():
"""Update config to use absolute path to parent directory (alternative approach)."""
# This is an alternative - set plugins_directory to absolute path
# Currently not implemented as symlinks are preferred
pass
print(f"\nCreated {created} links, skipped {skipped}")
return True
def main():
"""Main function."""
print("🔗 Setting up plugin repository symlinks...")
print()
if not GITHUB_DIR.exists():
print(f"Error: GitHub directory not found: {GITHUB_DIR}")
return 1
success = create_symlinks()
if success:
print()
print("✅ Plugin repository setup complete!")
print()
print("Plugins are now accessible via symlinks in plugin-repos/")
print("You can update plugins independently in their git repos.")
return 0
else:
print()
print("⚠️ Setup completed with some errors. Check output above.")
return 1
print("Setting up plugin repository symlinks from monorepo...\n")
if not create_symlinks():
sys.exit(1)
if __name__ == '__main__':
sys.exit(main())
if __name__ == "__main__":
main()

View File

@@ -1,123 +1,43 @@
#!/usr/bin/env python3
"""
Update all plugin repositories by pulling the latest changes.
This script updates all plugin repos without needing to modify
the LEDMatrix project itself.
Update the ledmatrix-plugins monorepo by pulling latest changes.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
# Paths
WORKSPACE_FILE = Path(__file__).parent.parent / "LEDMatrix.code-workspace"
GITHUB_DIR = Path(__file__).parent.parent.parent
def load_workspace_plugins():
"""Load plugin paths from workspace file."""
try:
with open(WORKSPACE_FILE, 'r', encoding='utf-8') as f:
workspace = json.load(f)
except FileNotFoundError:
print(f"Error: Workspace file not found: {WORKSPACE_FILE}")
return []
except PermissionError as e:
print(f"Error: Permission denied reading workspace file {WORKSPACE_FILE}: {e}")
return []
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in workspace file {WORKSPACE_FILE}: {e}")
return []
plugins = []
for folder in workspace.get('folders', []):
path = folder.get('path', '')
name = folder.get('name', '')
# Only process plugin folders (those starting with ../)
if path.startswith('../') and path != '../ledmatrix-plugins':
plugin_name = path.replace('../', '')
plugin_path = GITHUB_DIR / plugin_name
if plugin_path.exists():
plugins.append({
'name': plugin_name,
'display_name': name,
'path': plugin_path
})
return plugins
def update_repo(repo_path):
"""Update a git repository by pulling latest changes."""
if not (repo_path / '.git').exists():
print(f" ⚠️ {repo_path.name} is not a git repository, skipping")
return False
try:
# Fetch latest changes
fetch_result = subprocess.run(['git', 'fetch', 'origin'],
cwd=repo_path, capture_output=True, text=True)
if fetch_result.returncode != 0:
print(f" ✗ Failed to fetch {repo_path.name}: {fetch_result.stderr.strip()}")
return False
# Get current branch
branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
cwd=repo_path, capture_output=True, text=True)
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else 'main'
# Pull latest changes
pull_result = subprocess.run(['git', 'pull', 'origin', current_branch],
cwd=repo_path, capture_output=True, text=True)
if pull_result.returncode == 0:
# Check if there were actual updates
if 'Already up to date' in pull_result.stdout:
print(f"{repo_path.name} is up to date")
else:
print(f" ✓ Updated {repo_path.name}")
return True
else:
print(f" ✗ Failed to update {repo_path.name}: {pull_result.stderr.strip()}")
return False
except (subprocess.SubprocessError, OSError) as e:
print(f" ✗ Error updating {repo_path.name}: {e}")
return False
MONOREPO_DIR = Path(__file__).parent.parent.parent / "ledmatrix-plugins"
def main():
"""Main function."""
print("🔍 Finding plugin repositories...")
plugins = load_workspace_plugins()
if not plugins:
print(" No plugin repositories found!")
if not MONOREPO_DIR.exists():
print(f"Error: Monorepo not found: {MONOREPO_DIR}")
return 1
print(f" Found {len(plugins)} plugin repositories")
print(f"\n🚀 Updating plugins in {GITHUB_DIR}...")
print()
success_count = 0
for plugin in plugins:
print(f"Updating {plugin['name']}...")
if update_repo(plugin['path']):
success_count += 1
print()
print(f"\n✅ Updated {success_count}/{len(plugins)} plugins successfully!")
if success_count < len(plugins):
print("⚠️ Some plugins failed to update. Check the errors above.")
if not (MONOREPO_DIR / ".git").exists():
print(f"Error: {MONOREPO_DIR} is not a git repository")
return 1
print(f"Updating {MONOREPO_DIR}...")
try:
result = subprocess.run(
["git", "-C", str(MONOREPO_DIR), "pull"],
capture_output=True,
text=True,
timeout=120,
)
except subprocess.TimeoutExpired:
print(f"Error: git pull timed out after 120 seconds for {MONOREPO_DIR}")
return 1
if result.returncode == 0:
print(result.stdout.strip())
return 0
else:
print(f"Error: {result.stderr.strip()}")
return 1
return 0
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main())