From 4d9c52f182ff31fdf7b07f30f6d98754a1263b1e Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 17 Sep 2026 09:03:52 -0400 Subject: [PATCH] fix(dev): link-github links plugins from the ledmatrix-plugins monorepo link-github cloned https://github.com/ChuckBuilds/ledmatrix-.git, and those per-plugin repositories no longer exist: official plugins are directories in the ledmatrix-plugins monorepo. It now clones (or pulls) the monorepo once into the dev directory, finds plugins/, plugins/ledmatrix- or the plugin whose manifest id is , and links it under its manifest id. With an explicit repo URL it still links a single-repository plugin as before. dev_plugins.json: github_user is honoured again (monorepo owner, e.g. a fork), plus plugins_repo and plugins_branch; github_pattern, which was documented but never read, is dropped and warned about. Ships dev_plugins.json.example and git-ignores dev_plugins.json, both of which the guide promised. Reading JSON falls back to python3 when jq is missing (get_plugin_id silently returned nothing without jq). update/status/list find the git checkout above a monorepo plugin directory (its .git is not in the plugin dir), and update pulls a shared checkout once. status no longer exits 1 when nothing is broken. Docs: PLUGIN_DEVELOPMENT_GUIDE (quick start, link-github, configuration, workflow, store integration, hello-world link, submission), and the nonexistent scripts/git-hooks/pre-push-plugin-version and scripts/bump_plugin_version.py replaced with the real rule: bump the manifest version and run update_registry.py. scripts/dev/README.md and CLAUDE.md updated to match. Co-Authored-By: Claude Opus 5 --- .gitignore | 7 +- CLAUDE.md | 2 +- dev_plugins.json.example | 6 + docs/PLUGIN_DEVELOPMENT_GUIDE.md | 191 ++++++++++++---------- scripts/dev/README.md | 10 ++ scripts/dev/dev_plugin_setup.sh | 268 ++++++++++++++++++++++--------- 6 files changed, 319 insertions(+), 165 deletions(-) create mode 100644 dev_plugins.json.example diff --git a/.gitignore b/.gitignore index afb9b3df..3b59b144 100644 --- a/.gitignore +++ b/.gitignore @@ -39,11 +39,12 @@ htmlcov/ # Cache directory (root level only, not src/cache which is source code) /cache/ -# Development plugins directory -# Plugins are managed as separate repositories via multi-root workspace -# See docs/MULTI_ROOT_WORKSPACE_SETUP.md for details +# Development plugins directory: symlinks into a ledmatrix-plugins checkout +# See docs/PLUGIN_DEVELOPMENT_GUIDE.md and docs/MULTI_ROOT_WORKSPACE_SETUP.md plugins/* !plugins/.gitkeep +# Local settings for scripts/dev/dev_plugin_setup.sh (template: dev_plugins.json.example) +/dev_plugins.json # Binary files and backups bin/pixlet/ diff --git a/CLAUDE.md b/CLAUDE.md index 764b637a..1a3cd990 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ `config.get(...)`, never a separate accessor ## Dev Workflow -- Link a plugin for development: `./scripts/dev/dev_plugin_setup.sh link-github ` (or `link `); symlinks land in `plugins/` — set `plugin_system.plugins_directory` to `plugins` so discovery picks them up +- Link a plugin for development: `./scripts/dev/dev_plugin_setup.sh link-github ` clones the `ledmatrix-plugins` monorepo into `~/.ledmatrix-dev-plugins/` and links its `plugins/` under the manifest id (add a repo URL for a plugin with its own repo; or `link `); symlinks land in `plugins/` — set `plugin_system.plugins_directory` to `plugins` so discovery picks them up. Fork/location overrides: `dev_plugins.json` (from `dev_plugins.json.example`) - Browser preview without the display loop: `python3 scripts/dev_server.py` → http://localhost:5001 - Full display in emulator mode: `python3 run.py -e` (or `EMULATOR=true python3 run.py`) - Validate one plugin headlessly: `python3 scripts/check_plugin.py --plugin ` diff --git a/dev_plugins.json.example b/dev_plugins.json.example new file mode 100644 index 00000000..64f26abb --- /dev/null +++ b/dev_plugins.json.example @@ -0,0 +1,6 @@ +{ + "dev_plugins_dir": "~/.ledmatrix-dev-plugins", + "github_user": "ChuckBuilds", + "plugins_repo": "ledmatrix-plugins", + "plugins_branch": "main" +} diff --git a/docs/PLUGIN_DEVELOPMENT_GUIDE.md b/docs/PLUGIN_DEVELOPMENT_GUIDE.md index b4c8a28d..491bd8ad 100644 --- a/docs/PLUGIN_DEVELOPMENT_GUIDE.md +++ b/docs/PLUGIN_DEVELOPMENT_GUIDE.md @@ -45,28 +45,45 @@ The solution uses **symbolic links** to connect plugin repositories to the `plug ## Quick Start -### 1. Link a Plugin from GitHub +Official plugins all live in one repository, +[ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins), with +one directory per plugin under `plugins/` (there are no per-plugin +`ledmatrix-` repositories). The helper script links a plugin directory +from a checkout of that monorepo into LEDMatrix's `plugins/` directory. -The easiest way to link a plugin that's already on GitHub: +### 1. Link an Official Plugin ```bash -./scripts/dev/dev_plugin_setup.sh link-github music +./scripts/dev/dev_plugin_setup.sh link-github football-scoreboard ``` This will: -- Clone `https://github.com/ChuckBuilds/ledmatrix-music.git` to `~/.ledmatrix-dev-plugins/ledmatrix-music` -- Create a symbolic link from `plugins/music` to the cloned repository -- Validate that the plugin has a proper `manifest.json` +- Clone `https://github.com/ChuckBuilds/ledmatrix-plugins.git` to + `~/.ledmatrix-dev-plugins/ledmatrix-plugins` (or `git pull` it if it is + already there) +- Find `plugins/football-scoreboard` in it (also accepted: + `plugins/ledmatrix-`, or a plugin whose manifest `id` is the name) +- Validate that it has a `manifest.json` +- Create a symbolic link named after the plugin's manifest id, e.g. + `plugins/football-scoreboard` → `~/.ledmatrix-dev-plugins/ledmatrix-plugins/plugins/football-scoreboard` -### 2. Link a Local Plugin Repository +`link-github music` finds the monorepo's `plugins/ledmatrix-music` directory +and links it into LEDMatrix as `plugins/ledmatrix-music`, because +`ledmatrix-music` is that plugin's manifest id. -If you already have a plugin repository cloned locally: +To work from your fork of the monorepo, set `github_user` in +`dev_plugins.json` (see [Configuration](#configuration)). + +### 2. Link a Local Plugin Directory + +If you already have the monorepo (or a third-party plugin repository) cloned +locally: ```bash -./scripts/dev/dev_plugin_setup.sh link music ../ledmatrix-music +./scripts/dev/dev_plugin_setup.sh link hello-world ../ledmatrix-plugins/plugins/hello-world ``` -This creates a symlink from `plugins/music` to your local repository path. +This creates a symlink from `plugins/hello-world` to that directory. ### 3. Check Status @@ -79,13 +96,17 @@ See which plugins are linked and their git status: ### 4. Work on Your Plugin ```bash -cd plugins/music # Actually editing the linked repository -# Make your changes +cd plugins/football-scoreboard # Actually editing the monorepo checkout +# Make your changes, then bump "version" in manifest.json git add . -git commit -m "feat: add new feature" -git push origin main +git commit -m "feat(football-scoreboard): add new feature" +git push # to your fork, then open a PR against ledmatrix-plugins ``` +In the monorepo, every plugin change must bump `version` in the plugin's +`manifest.json` and run `python update_registry.py`, or users won't receive +the update. + ### 5. Update Plugins Pull latest changes from remote: @@ -118,7 +139,7 @@ Links a local plugin repository to the plugins directory. **Example:** ```bash -./scripts/dev/dev_plugin_setup.sh link football-scoreboard ../ledmatrix-football-scoreboard +./scripts/dev/dev_plugin_setup.sh link football-scoreboard ../ledmatrix-plugins/plugins/football-scoreboard ``` **Notes:** @@ -131,23 +152,25 @@ Links a local plugin repository to the plugins directory. Clones a plugin from GitHub and links it. **Arguments:** -- `plugin-name`: The name of the plugin (will be the directory name in `plugins/`) -- `repo-url`: (Optional) Full GitHub repository URL. If omitted, constructs from pattern: `https://github.com/ChuckBuilds/ledmatrix-.git` +- `plugin-name`: Without `repo-url`, the plugin to link from the monorepo: a + directory under `plugins/` (`` or `ledmatrix-`) or a manifest + id. The link is named after the plugin's manifest id. With `repo-url`, the + name of the link in `plugins/`. +- `repo-url`: (Optional) A plugin that has its own repository (e.g. a + third-party plugin). The repository root is linked. **Examples:** ```bash -# Auto-construct URL from plugin name -./scripts/dev/dev_plugin_setup.sh link-github music +# Official plugin, from the ledmatrix-plugins monorepo +./scripts/dev/dev_plugin_setup.sh link-github stocks -# Use explicit URL -./scripts/dev/dev_plugin_setup.sh link-github stocks https://github.com/ChuckBuilds/ledmatrix-stocks.git - -# Link from a different GitHub user +# Third-party plugin with its own repository ./scripts/dev/dev_plugin_setup.sh link-github custom-plugin https://github.com/OtherUser/custom-plugin.git ``` **Notes:** - Repositories are cloned to `~/.ledmatrix-dev-plugins/` by default (configurable) +- The monorepo is cloned once and shared by every plugin you link from it - If the repository already exists, it will be updated with `git pull` instead of re-cloning - The cloned repository is preserved when you unlink the plugin @@ -219,30 +242,28 @@ Updates plugin(s) by running `git pull` in their repositories. ### Custom Development Directory -By default, GitHub repositories are cloned to `~/.ledmatrix-dev-plugins/`. You can customize this by creating a `dev_plugins.json` file: +By default, GitHub repositories are cloned to `~/.ledmatrix-dev-plugins/` +and official plugins come from `ChuckBuilds/ledmatrix-plugins`. To change +either, copy `dev_plugins.json.example` (in the LEDMatrix root) to +`dev_plugins.json` and edit it. `dev_plugins.json` is git-ignored. ```json { - "dev_plugins_dir": "/path/to/your/dev/plugins", - "github_user": "ChuckBuilds", - "github_pattern": "ledmatrix-", - "plugins": { - "music": { - "source": "github", - "url": "https://github.com/ChuckBuilds/ledmatrix-music.git", - "branch": "main" - } - } + "dev_plugins_dir": "~/.ledmatrix-dev-plugins", + "github_user": "your-github-user", + "plugins_repo": "ledmatrix-plugins", + "plugins_branch": "main" } ``` -**Configuration options:** +**Configuration options** (all optional): - `dev_plugins_dir`: Where to clone GitHub repositories (default: `~/.ledmatrix-dev-plugins`) -- `github_user`: Default GitHub username for auto-constructing URLs -- `github_pattern`: Pattern for repository names (default: `ledmatrix-`) -- `plugins`: Plugin definitions (optional, for future auto-discovery features) +- `github_user`: Owner of the plugin monorepo that `link-github ` clones — set it to use your fork (default: `ChuckBuilds`) +- `plugins_repo`: Name of that monorepo (default: `ledmatrix-plugins`) +- `plugins_branch`: Branch to clone it at (default: the repository's default branch). Only applies when the clone is first made. -**Note:** Copy `dev_plugins.json.example` to `dev_plugins.json` and customize it. The `dev_plugins.json` file is git-ignored. +`github_pattern` from older versions of this guide is no longer used (the +script warns if it is set). ## Development Workflow @@ -250,43 +271,46 @@ By default, GitHub repositories are cloned to `~/.ledmatrix-dev-plugins/`. You c 1. **Link your plugin for development:** ```bash - ./scripts/dev/dev_plugin_setup.sh link-github music + ./scripts/dev/dev_plugin_setup.sh link-github clock-simple ``` 2. **Test in LEDMatrix:** ```bash - # Run LEDMatrix with your plugin - python run.py + # Run LEDMatrix with your plugin (emulator shown) + python3 run.py -e ``` 3. **Make changes:** ```bash - cd plugins/music + cd plugins/clock-simple # Edit files... # Test changes... ``` -4. **Commit to plugin repository:** +4. **Commit to the plugin repository:** ```bash - cd plugins/music # This is actually your repo + cd plugins/clock-simple # This is inside your monorepo checkout + # bump "version" in manifest.json, then from the monorepo root: + # python update_registry.py git add . - git commit -m "feat: add new feature" - git push origin main + git commit -m "feat(clock-simple): add new feature" + git push ``` 5. **Update from remote (if needed):** ```bash - ./scripts/dev/dev_plugin_setup.sh update music + ./scripts/dev/dev_plugin_setup.sh update clock-simple ``` 6. **When done developing:** ```bash - ./scripts/dev/dev_plugin_setup.sh unlink music + ./scripts/dev/dev_plugin_setup.sh unlink clock-simple ``` ### Working with Multiple Plugins -You can have multiple plugins linked simultaneously: +You can have multiple plugins linked simultaneously. Plugins linked from the +monorepo share one checkout: ```bash ./scripts/dev/dev_plugin_setup.sh link-github music @@ -296,7 +320,7 @@ You can have multiple plugins linked simultaneously: # Check status of all ./scripts/dev/dev_plugin_setup.sh status -# Update all at once +# Update all at once (the shared monorepo checkout is pulled once) ./scripts/dev/dev_plugin_setup.sh update ``` @@ -411,7 +435,7 @@ If you have conflicts when updating: 1. **Manually resolve in the plugin repository:** ```bash - cd ~/.ledmatrix-dev-plugins/ledmatrix-music + cd ~/.ledmatrix-dev-plugins/ledmatrix-plugins git pull # Resolve conflicts... git add . @@ -472,18 +496,19 @@ You can mix local and GitHub plugins: The development workflow is separate from the plugin store installation: -- **Plugin Store:** Installs plugins to `plugins/` as regular directories -- **Development Setup:** Links plugin repositories as symlinks +- **Plugin Store:** Installs plugins as regular directories in the configured + plugins directory (`plugin-repos/` by default) +- **Development Setup:** Links plugin directories as symlinks in `plugins/` -If you install a plugin via the store, you can still link it for development: +The plugin loader scans only one directory, so while developing set +`plugin_system.plugins_directory` to `plugins` (see the note at the top of +this guide). If `plugins/` already holds a regular directory of the same +name, `link`/`link-github` offers to rename it to +`.backup.` before linking. -```bash -# Store installs to plugins/music (regular directory) -# Link for development (will prompt to replace) -./scripts/dev/dev_plugin_setup.sh link-github music -``` - -When you unlink, the directory is removed. If you want to switch back to the store version, re-install it via the plugin store. +`unlink` removes only the symlink. To switch back to the store version, set +`plugins_directory` back to `plugin-repos` (or reinstall the plugin from the +store). ## API Reference @@ -527,7 +552,7 @@ Want to create and share your own plugin? Here's everything you need to know. - [Advanced Plugin Development](ADVANCED_PLUGIN_DEVELOPMENT.md) - Patterns and examples 2. **Start with a template**: - - Use the [Hello World plugin](https://github.com/ChuckBuilds/ledmatrix-hello-world) as a starting point + - Use the [Hello World plugin](https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hello-world) as a starting point - Or fork an existing plugin and modify it 3. **Follow the plugin structure**: @@ -591,24 +616,16 @@ Your plugin must: ### Versioning Best Practices - **Use semantic versioning**: `MAJOR.MINOR.PATCH` (e.g., `1.2.3`) -- **GitHub as source of truth**: the plugin store resolves versions in this - order: GitHub Releases → GitHub Tags → manifest from branch → git commit hash -- **Automatic version bumping**: install the self-contained pre-push hook in - your plugin repo and patch versions bump themselves on push (a git tag - `v{version}` is created and `manifest.json` staged automatically): - - ```bash - # From your plugin repository directory - cp /path/to/LEDMatrix/scripts/git-hooks/pre-push-plugin-version .git/hooks/pre-push - chmod +x .git/hooks/pre-push - ``` - - Set `SKIP_TAG=1` in the environment to skip auto-tagging for one push. -- **Manual versioning**: only needed for major/minor bumps, CI pipelines that - bypass hooks, or forks without the hook — use - `scripts/bump_plugin_version.py`. -- **Registry stores no versions**: `plugins.json` holds only metadata (name, - description, repo URL). +- **Bump `version` in `manifest.json` by hand** for every change you ship. + There is no automatic version-bump hook or bump script. +- **Official (monorepo) plugins**: after bumping the manifest, run + `python update_registry.py` in the `ledmatrix-plugins` checkout. It copies + each manifest's version into `plugins.json` as `latest_version`, which is + what the store compares installed versions against. Without it, users + won't be offered the update. +- **Plugins in their own repository**: still bump the manifest `version`, + so users can see which version they run; tagging releases (`v1.2.3`) to + match is a good habit. ### Submitting to Official Registry @@ -620,12 +637,14 @@ To have your plugin added to the official plugin store: - Follows best practices - Tested on Raspberry Pi hardware -2. **Create GitHub repository**: - - Repository name: `ledmatrix-` - - Public repository - - Proper README.md with installation instructions +2. **Choose where it lives** (see `SUBMISSION.md` in + [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins)): + - **In the monorepo (preferred):** fork ledmatrix-plugins, add + `plugins//`, and open a pull request + - **In your own public repository** (conventionally + `ledmatrix-`), with a README that covers installation -3. **Contact maintainers**: +3. **Contact maintainers** (own-repository plugins): - Open a GitHub issue in the [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) repository - Or reach out on Discord: https://discord.gg/uW36dVAtcT - Include: Repository URL, plugin description, why it's useful diff --git a/scripts/dev/README.md b/scripts/dev/README.md index 3768f17e..cec5a631 100644 --- a/scripts/dev/README.md +++ b/scripts/dev/README.md @@ -12,9 +12,19 @@ This directory contains scripts and utilities for development and testing. ### Plugin Development Setup ```bash +# Official plugin: clones ChuckBuilds/ledmatrix-plugins (once) and links +# its plugins/ into plugins/ ./scripts/dev/dev_plugin_setup.sh link-github + +# Plugin with its own repository +./scripts/dev/dev_plugin_setup.sh link-github ``` +Set `plugin_system.plugins_directory` to `plugins` so the loader finds the +links. To use a fork or another clone location, copy +`dev_plugins.json.example` to `dev_plugins.json`. Details: +[docs/PLUGIN_DEVELOPMENT_GUIDE.md](../../docs/PLUGIN_DEVELOPMENT_GUIDE.md). + ### Running Emulator ```bash ./scripts/dev/run_emulator.sh diff --git a/scripts/dev/dev_plugin_setup.sh b/scripts/dev/dev_plugin_setup.sh index f9f6363b..1d4d1d39 100755 --- a/scripts/dev/dev_plugin_setup.sh +++ b/scripts/dev/dev_plugin_setup.sh @@ -10,8 +10,14 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" PLUGINS_DIR="$PROJECT_ROOT/plugins" CONFIG_FILE="$PROJECT_ROOT/dev_plugins.json" DEFAULT_DEV_DIR="$HOME/.ledmatrix-dev-plugins" -GITHUB_USER="ChuckBuilds" -GITHUB_PATTERN="ledmatrix-" +# Official plugins live in one monorepo: /, one +# directory per plugin under plugins/. Both can be overridden in +# dev_plugins.json (e.g. to work from a fork). +DEFAULT_GITHUB_USER="ChuckBuilds" +DEFAULT_PLUGINS_REPO="ledmatrix-plugins" +GITHUB_USER="$DEFAULT_GITHUB_USER" +PLUGINS_REPO="$DEFAULT_PLUGINS_REPO" +PLUGINS_BRANCH="" # Colors for output RED='\033[0;31m' @@ -37,18 +43,55 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1" } +# Print a top-level string field of a JSON file, or nothing if it is absent. +# Uses jq when installed, else python3. +json_field() { + local file="$1" + local key="$2" + if command -v jq >/dev/null 2>&1; then + jq -r --arg k "$key" '.[$k] // empty | select(type == "string")' "$file" 2>/dev/null || true + elif command -v python3 >/dev/null 2>&1; then + python3 - "$file" "$key" <<'PY' 2>/dev/null || true +import json, sys +try: + with open(sys.argv[1], encoding="utf-8") as f: + value = json.load(f).get(sys.argv[2]) +except Exception: + value = None +if isinstance(value, str): + print(value) +PY + fi +} + # Load configuration file load_config() { + DEV_PLUGINS_DIR="$DEFAULT_DEV_DIR" if [[ -f "$CONFIG_FILE" ]]; then - DEV_PLUGINS_DIR=$(jq -r '.dev_plugins_dir // "'"$DEFAULT_DEV_DIR"'"' "$CONFIG_FILE" 2>/dev/null || echo "$DEFAULT_DEV_DIR") - # Expand ~ in path - DEV_PLUGINS_DIR="${DEV_PLUGINS_DIR/#\~/$HOME}" - else - DEV_PLUGINS_DIR="$DEFAULT_DEV_DIR" + local value + value=$(json_field "$CONFIG_FILE" dev_plugins_dir) + [[ -n "$value" ]] && DEV_PLUGINS_DIR="$value" + value=$(json_field "$CONFIG_FILE" github_user) + [[ -n "$value" ]] && GITHUB_USER="$value" + value=$(json_field "$CONFIG_FILE" plugins_repo) + [[ -n "$value" ]] && PLUGINS_REPO="$value" + value=$(json_field "$CONFIG_FILE" plugins_branch) + [[ -n "$value" ]] && PLUGINS_BRANCH="$value" + if [[ -n "$(json_field "$CONFIG_FILE" github_pattern)" ]]; then + log_warn "dev_plugins.json: github_pattern is no longer used (official plugins are in the $PLUGINS_REPO monorepo)" + fi fi + # Expand ~ in path + DEV_PLUGINS_DIR="${DEV_PLUGINS_DIR/#\~/$HOME}" mkdir -p "$DEV_PLUGINS_DIR" } +# Top level of the git checkout containing a path, or nothing. +# A monorepo plugin is a subdirectory, so its .git is not in the plugin dir. +git_root_of() { + git -C "$1" rev-parse --show-toplevel 2>/dev/null || true +} + # Validate plugin structure validate_plugin() { local plugin_path="$1" @@ -63,7 +106,7 @@ validate_plugin() { get_plugin_id() { local plugin_path="$1" if [[ -f "$plugin_path/manifest.json" ]]; then - jq -r '.id // empty' "$plugin_path/manifest.json" 2>/dev/null || echo "" + json_field "$plugin_path/manifest.json" id fi } @@ -176,50 +219,104 @@ clone_from_github() { return 0 } +# Clone a repository into DEV_PLUGINS_DIR, or update the existing clone. +# Prints the clone's path on stdout (log output goes to stderr). +ensure_clone() { + local repo_url="$1" + local branch="${2:-}" + local repo_name + repo_name=$(basename "$repo_url" .git) + local target_dir="$DEV_PLUGINS_DIR/$repo_name" + + if [[ -d "$target_dir" ]]; then + log_info "Repository already exists at $target_dir" >&2 + if [[ -d "$target_dir/.git" ]]; then + log_info "Updating repository..." >&2 + (cd "$target_dir" && git pull --rebase) >&2 || true + fi + else + if ! clone_from_github "$repo_url" "$target_dir" "$branch" >&2; then + return 1 + fi + fi + echo "$target_dir" +} + +# Find a plugin's directory inside a monorepo clone: plugins/, +# plugins/ledmatrix-, or the directory whose manifest id is . +find_monorepo_plugin() { + local repo_dir="$1" + local name="$2" + local candidate + for candidate in "$repo_dir/plugins/$name" "$repo_dir/plugins/ledmatrix-$name"; do + if [[ -f "$candidate/manifest.json" ]]; then + echo "$candidate" + return 0 + fi + done + for candidate in "$repo_dir"/plugins/*/; do + candidate="${candidate%/}" + [[ -f "$candidate/manifest.json" ]] || continue + if [[ "$(get_plugin_id "$candidate")" == "$name" ]]; then + echo "$candidate" + return 0 + fi + done + return 1 +} + # Link plugin from GitHub link_github_plugin() { - local plugin_name="$1" + local plugin_name="${1:-}" local repo_url="${2:-}" - + if [[ -z "$plugin_name" ]]; then log_error "Usage: $0 link-github [repo-url]" exit 1 fi - + load_config - - # Construct repo URL if not provided - if [[ -z "$repo_url" ]]; then - repo_url="https://github.com/${GITHUB_USER}/${GITHUB_PATTERN}${plugin_name}.git" - log_info "Using default GitHub URL: $repo_url" - fi - - # Determine target directory name from URL - local repo_name=$(basename "$repo_url" .git) - local target_dir="$DEV_PLUGINS_DIR/$repo_name" - - # Check if already cloned - if [[ -d "$target_dir" ]]; then - log_info "Repository already exists at $target_dir" - if [[ -d "$target_dir/.git" ]]; then - log_info "Updating repository..." - (cd "$target_dir" && git pull --rebase) || true - fi - else - # Clone the repository - if ! clone_from_github "$repo_url" "$target_dir"; then + + if [[ -n "$repo_url" ]]; then + # A plugin with its own repository (e.g. a third-party plugin): the + # repository root is the plugin. + local target_dir + if ! target_dir=$(ensure_clone "$repo_url"); then exit 1 fi + if ! validate_plugin "$target_dir"; then + log_error "Cloned repository does not appear to be a valid plugin" + exit 1 + fi + link_plugin "$plugin_name" "$target_dir" + return fi - - # Validate plugin structure - if ! validate_plugin "$target_dir"; then - log_error "Cloned repository does not appear to be a valid plugin" + + # Official plugins: clone the monorepo once, link plugins/ from it. + repo_url="https://github.com/${GITHUB_USER}/${PLUGINS_REPO}.git" + log_info "Using plugin monorepo: $repo_url" + local repo_dir + if ! repo_dir=$(ensure_clone "$repo_url" "$PLUGINS_BRANCH"); then exit 1 fi - - # Link the plugin - link_plugin "$plugin_name" "$target_dir" + + local plugin_dir + if ! plugin_dir=$(find_monorepo_plugin "$repo_dir" "$plugin_name"); then + log_error "No plugin named '$plugin_name' in $repo_dir/plugins" + log_info "Plugins are the directory names under $repo_dir/plugins, or their manifest ids" + exit 1 + fi + + # Link under the manifest id: that is the name the plugin loader and + # config.json use, and it can differ from the directory name + # (plugins/ledmatrix-music has id ledmatrix-music, not music). + local link_name + link_name=$(get_plugin_id "$plugin_dir") + [[ -n "$link_name" ]] || link_name=$(basename "$plugin_dir") + if [[ "$link_name" != "$plugin_name" ]]; then + log_info "Linking as '$link_name' (the plugin's manifest id)" + fi + link_plugin "$link_name" "$plugin_dir" } # Unlink a plugin @@ -274,7 +371,7 @@ list_plugins() { echo " → $target" # Check git status if it's a git repo - if [[ -d "$target/.git" ]]; then + if [[ -n "$(git_root_of "$target")" ]]; then local branch=$(cd "$target" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") local status=$(cd "$target" && git status --porcelain 2>/dev/null | head -1) if [[ -n "$status" ]]; then @@ -327,7 +424,7 @@ check_status() { echo -e "${GREEN}✓${NC} ${BLUE}$plugin_name${NC}" echo " Path: $target" - if [[ -d "$target/.git" ]]; then + if [[ -n "$(git_root_of "$target")" ]]; then local branch=$(cd "$target" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") local remote=$(cd "$target" && git remote get-url origin 2>/dev/null || echo "no remote") local commits_behind=$(cd "$target" && git rev-list --count HEAD..@{upstream} 2>/dev/null || echo "0") @@ -360,9 +457,13 @@ check_status() { done echo "Summary:" - echo " ${GREEN}Clean: $clean_count${NC}" - echo " ${YELLOW}Needs attention: $dirty_count${NC}" - [[ $broken_count -gt 0 ]] && echo -e " ${RED}Broken: $broken_count${NC}" + echo -e " ${GREEN}Clean: $clean_count${NC}" + echo -e " ${YELLOW}Needs attention: $dirty_count${NC}" + # An if, not `[[ ]] &&`: as the function's last command a false test made + # `status` exit 1 whenever nothing was broken. + if [[ $broken_count -gt 0 ]]; then + echo -e " ${RED}Broken: $broken_count${NC}" + fi } # Update plugin(s) @@ -384,39 +485,46 @@ update_plugins() { fi local target=$(get_symlink_target "$plugin_name") - - if [[ ! -d "$target/.git" ]]; then + local root + root=$(git_root_of "$target") + + if [[ -z "$root" ]]; then log_error "Plugin repository is not a git repository: $target" exit 1 fi - - log_info "Updating $plugin_name from $target" - (cd "$target" && git pull --rebase) + + log_info "Updating $plugin_name from $root" + (cd "$root" && git pull --rebase) log_success "Updated $plugin_name" else - # Update all linked plugins + # Update all linked plugins. Plugins linked from the monorepo share one + # checkout, which is pulled once. log_info "Updating all linked plugins..." local updated=0 local failed=0 - + local pulled_roots=" " + for item in "$PLUGINS_DIR"/*; do [[ -e "$item" ]] || continue [[ -d "$item" ]] || continue - + local name=$(basename "$item") [[ "$name" =~ ^\.|^_ ]] && continue - + if is_symlink "$item"; then local target=$(get_symlink_target "$name") - if [[ -d "$target/.git" ]]; then - log_info "Updating $name..." - if (cd "$target" && git pull --rebase); then - log_success "Updated $name" - updated=$((updated + 1)) - else - log_error "Failed to update $name" - failed=$((failed + 1)) - fi + local root + root=$(git_root_of "$target") + [[ -n "$root" ]] || continue + [[ "$pulled_roots" == *" $root "* ]] && continue + pulled_roots="$pulled_roots$root " + log_info "Updating $root (for $name)..." + if (cd "$root" && git pull --rebase); then + log_success "Updated $root" + updated=$((updated + 1)) + else + log_error "Failed to update $root" + failed=$((failed + 1)) fi fi done @@ -439,7 +547,12 @@ Commands: link-github [repo-url] Clone and link a plugin from GitHub - If repo-url is not provided, uses: https://github.com/${GITHUB_USER}/${GITHUB_PATTERN}.git + Without repo-url: clones (or updates) the official plugin monorepo, + https://github.com/${DEFAULT_GITHUB_USER}/${DEFAULT_PLUGINS_REPO}.git, and links its + plugins/ (or plugins/ledmatrix-) under the + plugin's manifest id + With repo-url: clones a plugin that has its own repository and links + the repository root unlink Remove symlink for a plugin (preserves repository) @@ -458,25 +571,30 @@ Commands: Show this help message Examples: - # Link a local plugin - $0 link music ../ledmatrix-music - - # Link from GitHub (auto-detects URL) - $0 link-github music - - # Link from GitHub with custom URL - $0 link-github stocks https://github.com/ChuckBuilds/ledmatrix-stocks.git - + # Link an official plugin from the monorepo + $0 link-github football-scoreboard + + # Link a plugin from a local monorepo checkout + $0 link hello-world ../ledmatrix-plugins/plugins/hello-world + + # Link a third-party plugin from its own repository + $0 link-github my-plugin https://github.com/OtherUser/ledmatrix-my-plugin.git + # Check status $0 status - + # Update all plugins $0 update Configuration: - Create dev_plugins.json in project root to customize: + Copy dev_plugins.json.example to dev_plugins.json (git-ignored) to customize: - dev_plugins_dir: Where to clone GitHub repos (default: ~/.ledmatrix-dev-plugins) - - plugins: Plugin definitions (optional, for auto-discovery) + - github_user: Owner of the plugin monorepo, e.g. your fork (default: ${DEFAULT_GITHUB_USER}) + - plugins_repo: Name of the plugin monorepo (default: ${DEFAULT_PLUGINS_REPO}) + - plugins_branch: Branch to clone the monorepo at (default: its default branch) + + Symlinks are created in plugins/. Set plugin_system.plugins_directory to + "plugins" in config/config.json so the plugin loader discovers them. EOF }