refactor: migrate from submodules to multi-root workspace for plugins (#198)

* refactor: migrate from submodules to multi-root workspace for plugins

- Updated LEDMatrix.code-workspace to include all plugin repos as root folders
- Removed symlinks from plugin-repos/ and plugins/ directories
- Updated .gitignore to reflect new plugin management approach
- Added setup_plugin_repos.py script for managing plugin symlinks (if needed)
- Added MULTI_ROOT_WORKSPACE_SETUP.md documentation

Plugins are now managed as independent repositories via multi-root workspace,
allowing for easier development and independent updates without modifying
the LEDMatrix project structure.

* Fix MULTI_ROOT_WORKSPACE_SETUP.md and add JSON error handling

- Remove deprecated clone_plugin_repos.py command reference
- Add language tag to directory tree code fence (fixes MD040)
- Add JSONDecodeError handling in setup_plugin_repos.py with user-friendly error messages

---------

Co-authored-by: Chuck <chuck@example.com>
This commit is contained in:
Chuck
2026-01-18 13:06:57 -05:00
committed by GitHub
parent 23ada60544
commit bff16d3e00
25 changed files with 421 additions and 51 deletions

27
.gitignore vendored
View File

@@ -35,29 +35,8 @@ htmlcov/
# Cache directory (root level only, not src/cache which is source code) # Cache directory (root level only, not src/cache which is source code)
/cache/ /cache/
# Development plugins directory (contains symlinks to plugin repos) # Development plugins directory
# Plugins are installed via plugin store, not bundled with main repo # Plugins are managed as separate repositories via multi-root workspace
# Allow git submodules # See docs/MULTI_ROOT_WORKSPACE_SETUP.md for details
plugins/* plugins/*
!plugins/.gitkeep !plugins/.gitkeep
!plugins/odds-ticker/
!plugins/clock-simple/
!plugins/text-display/
!plugins/basketball-scoreboard/
!plugins/soccer-scoreboard/
!plugins/calendar/
!plugins/mqtt-notifications/
!plugins/youtube-stats/
!plugins/olympics-countdown/
!plugins/ledmatrix-stocks/
!plugins/ledmatrix-music/
!plugins/static-image/
!plugins/football-scoreboard/
!plugins/hockey-scoreboard/
!plugins/baseball-scoreboard/
!plugins/christmas-countdown/
!plugins/ledmatrix-flights/
!plugins/ledmatrix-leaderboard/
!plugins/ledmatrix-weather/
!plugins/ledmatrix-news/
!plugins/ledmatrix-of-the-day/

View File

@@ -1,7 +1,99 @@
{ {
"folders": [ "folders": [
{ {
"path": "." "path": ".",
"name": "LEDMatrix (Main)"
},
{
"path": "../ledmatrix-odds-ticker",
"name": "Odds Ticker"
},
{
"path": "../ledmatrix-clock-simple",
"name": "Clock Simple"
},
{
"path": "../ledmatrix-text-display",
"name": "Text Display"
},
{
"path": "../ledmatrix-basketball-scoreboard",
"name": "Basketball Scoreboard"
},
{
"path": "../ledmatrix-soccer-scoreboard",
"name": "Soccer Scoreboard"
},
{
"path": "../ledmatrix-calendar",
"name": "Calendar"
},
{
"path": "../ledmatrix-olympics-countdown",
"name": "Olympics Countdown"
},
{
"path": "../ledmatrix-stocks",
"name": "Stocks"
},
{
"path": "../ledmatrix-music",
"name": "Music"
},
{
"path": "../ledmatrix-static-image",
"name": "Static Image"
},
{
"path": "../ledmatrix-football-scoreboard",
"name": "Football Scoreboard"
},
{
"path": "../ledmatrix-hockey-scoreboard",
"name": "Hockey Scoreboard"
},
{
"path": "../ledmatrix-baseball-scoreboard",
"name": "Baseball Scoreboard"
},
{
"path": "../ledmatrix-christmas-countdown",
"name": "Christmas Countdown"
},
{
"path": "../ledmatrix-flights",
"name": "Flights"
},
{
"path": "../ledmatrix-leaderboard",
"name": "Leaderboard"
},
{
"path": "../ledmatrix-weather",
"name": "Weather"
},
{
"path": "../ledmatrix-news",
"name": "News"
},
{
"path": "../ledmatrix-of-the-day",
"name": "Of The Day"
},
{
"path": "../ledmatrix-youtube-stats",
"name": "YouTube Stats"
},
{
"path": "../ledmatrix-plugins",
"name": "Plugin Registry"
}
],
"settings": {
"files.exclude": {
"**/.git": true,
"**/__pycache__": true,
"**/*.pyc": true
}
} }
]
} }

View File

@@ -0,0 +1,169 @@
# Multi-Root Workspace Setup Guide
This document explains how the LEDMatrix project uses a multi-root workspace to manage plugins as separate Git repositories.
## Overview
The LEDMatrix project has been migrated from a git submodule implementation to a **multi-root workspace** implementation for managing plugins. This allows:
- ✅ Plugins to exist as independent Git repositories
- ✅ Updates to plugins without modifying the LEDMatrix project
- ✅ Easy development workflow with all repos in one workspace
- ✅ Plugin system discovers plugins via symlinks in `plugin-repos/`
## Directory Structure
```text
/home/chuck/Github/
├── LEDMatrix/ # Main project
│ ├── plugin-repos/ # Symlinks to actual repos (managed automatically)
│ │ ├── ledmatrix-clock-simple -> ../../ledmatrix-clock-simple
│ │ ├── ledmatrix-weather -> ../../ledmatrix-weather
│ │ └── ...
│ ├── LEDMatrix.code-workspace # Multi-root workspace configuration
│ └── ...
├── ledmatrix-clock-simple/ # Plugin repository (actual git repo)
├── ledmatrix-weather/ # Plugin repository (actual git repo)
├── ledmatrix-football-scoreboard/ # Plugin repository (actual git repo)
└── ... # Other plugin repos
```
## How It Works
### 1. Plugin Repositories
All plugin repositories are cloned to `/home/chuck/Github/` (parent directory of LEDMatrix) as regular Git repositories:
- `ledmatrix-clock-simple/`
- `ledmatrix-weather/`
- `ledmatrix-football-scoreboard/`
- etc.
### 2. Symlinks in plugin-repos/
The `LEDMatrix/plugin-repos/` directory contains symlinks pointing to the actual repositories in the parent directory. This allows the plugin system to discover plugins without modifying the project structure.
### 3. Multi-Root Workspace
The `LEDMatrix.code-workspace` file configures VS Code/Cursor to open all plugin repositories as separate workspace roots, allowing easy development across all repos.
## Setup Scripts
### Initial Setup
If you already have plugin repositories cloned, use the setup script:
```bash
cd /home/chuck/Github/LEDMatrix
python3 scripts/setup_plugin_repos.py
```
This script:
- Reads the workspace configuration
- Creates symlinks in `plugin-repos/` pointing to actual repos
- Verifies all links are created correctly
### Updating Plugins
To update all plugin repositories:
```bash
cd /home/chuck/Github/LEDMatrix
python3 scripts/update_plugin_repos.py
```
This script:
- Finds all plugins in the workspace
- Runs `git pull` on each repository
- Reports which plugins were updated
## Configuration
The plugin system is configured in `config/config.json`:
```json
{
"plugin_system": {
"plugins_directory": "plugin-repos",
"auto_discover": true,
"auto_load_enabled": true
}
}
```
The `plugins_directory` points to `plugin-repos/`, which contains symlinks to the actual repositories.
## Workflow
### Daily Development
1. **Open Workspace**: Open `LEDMatrix.code-workspace` in VS Code/Cursor
2. **All Repos Available**: All plugin repos appear as separate folders in the workspace
3. **Edit Plugins**: Edit plugin code directly in their repositories
4. **Update Plugins**: Run `update_plugin_repos.py` to pull latest changes
### Adding New Plugins
1. **Clone Repository**: Clone the new plugin repo to `/home/chuck/Github/`
2. **Add to Workspace**: Add the plugin folder to `LEDMatrix.code-workspace`
3. **Create Symlink**: Run `setup_plugin_repos.py` to create the symlink
### Updating Individual Plugins
Since plugins are regular Git repositories, you can update them individually:
```bash
cd /home/chuck/Github/ledmatrix-weather
git pull origin master
```
Or update all at once:
```bash
cd /home/chuck/Github/LEDMatrix
python3 scripts/update_plugin_repos.py
```
## Benefits
1. **No Submodule Hassle**: No need to update `.gitmodules` or run `git submodule update`
2. **Independent Updates**: Update plugins independently without touching LEDMatrix
3. **Clean Separation**: Each plugin is a separate repository with its own history
4. **Easy Development**: Multi-root workspace makes it easy to work across repos
5. **Automatic Discovery**: Plugin system automatically discovers plugins via symlinks
## Troubleshooting
### Symlinks Not Working
If plugins aren't being discovered:
```bash
cd /home/chuck/Github/LEDMatrix
python3 scripts/setup_plugin_repos.py
```
This will recreate all symlinks.
### Missing Plugins
If a plugin is in the workspace but not found:
1. Check if the repo exists in `/home/chuck/Github/`
2. Check if the symlink exists in `plugin-repos/`
3. Run `setup_plugin_repos.py` to recreate symlinks
### Plugin Updates Not Showing
If changes to plugins aren't appearing:
1. Verify the symlink points to the correct directory: `ls -la plugin-repos/ledmatrix-weather`
2. Check that you're editing in the actual repo, not a copy
3. Restart the LEDMatrix service if running
## Notes
- The `plugin-repos/` directory is tracked in git, but only contains symlinks
- Actual plugin code lives in `/home/chuck/Github/ledmatrix-*/`
- Each plugin repo can be updated independently via `git pull`
- The LEDMatrix project doesn't need to be updated when plugins change

Submodule plugins/calendar deleted from 940a045a3b

151
scripts/setup_plugin_repos.py Executable file
View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Setup plugin repository references for multi-root workspace.
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.
"""
import json
import os
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"
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 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")
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(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
continue
# Remove existing link/file if it exists
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")
skipped += 1
continue
else:
# Remove old symlink pointing elsewhere
link_path.unlink()
except Exception as e:
print(f" ⚠️ {plugin['name']} - error checking link: {e}")
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)")
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
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
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
if __name__ == '__main__':
sys.exit(main())