From c5f49e94623108804f34db38e4840b263ac767cc Mon Sep 17 00:00:00 2001
From: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
Date: Tue, 11 Feb 2025 01:54:59 +0200
Subject: [PATCH 1/5] Remove code formatting workflows and related
 configuration files

---
 .github/workflows/on_pr_check_code_format.yml | 51 -------------------
 .../workflows/on_push_check_code_format.yml   | 49 ------------------
 .prettierignore                               | 12 -----
 .prettierrc.json                              |  3 --
 package.json                                  |  9 +---
 requirements.txt                              |  1 -
 yarn.lock                                     |  8 ---
 7 files changed, 1 insertion(+), 132 deletions(-)
 delete mode 100644 .github/workflows/on_pr_check_code_format.yml
 delete mode 100644 .github/workflows/on_push_check_code_format.yml
 delete mode 100644 .prettierignore
 delete mode 100644 .prettierrc.json
 delete mode 100644 requirements.txt
 delete mode 100644 yarn.lock

diff --git a/.github/workflows/on_pr_check_code_format.yml b/.github/workflows/on_pr_check_code_format.yml
deleted file mode 100644
index 1aac93ce..00000000
--- a/.github/workflows/on_pr_check_code_format.yml
+++ /dev/null
@@ -1,51 +0,0 @@
-name: Check Code Format
-
-on:
-  pull_request:
-    branches:
-      - main
-
-  workflow_dispatch:
-
-permissions:
-  contents: write
-
-jobs:
-  check_js_code_format:
-    name: Check JS code format
-    runs-on: ubuntu-latest
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-          ref: ${{ github.event.pull_request.head.ref }}
-          repository: ${{ github.event.pull_request.head.repo.full_name }}
-
-      - name: Check format
-        uses: creyD/prettier_action@v4.3
-        with:
-          prettier_options: --check .
-          dry: true
-          only_changed: true
-          github_token: ${{ secrets.GITHUB_TOKEN }}
-
-  check_py_code_format:
-    name: Check PY code format
-    runs-on: ubuntu-latest
-    needs: check_js_code_format
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-          ref: ${{ github.event.pull_request.head.ref }}
-          repository: ${{ github.event.pull_request.head.repo.full_name }}
-
-      - name: Check format
-        uses: psf/black@stable
-        with:
-          options: '--exclude="3rd party"'
-          src: "."
diff --git a/.github/workflows/on_push_check_code_format.yml b/.github/workflows/on_push_check_code_format.yml
deleted file mode 100644
index f934b6c6..00000000
--- a/.github/workflows/on_push_check_code_format.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: Check Code Format
-
-on:
-  push:
-    branches:
-      - main
-
-  workflow_dispatch:
-
-permissions:
-  contents: write
-
-jobs:
-  check_js_code_format:
-    name: Check JS code format
-    runs-on: ubuntu-latest
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-          ref: ${{ github.head_ref }}
-
-      - name: Check format
-        uses: creyD/prettier_action@v4.3
-        with:
-          prettier_options: --check .
-          dry: true
-          only_changed: true
-          github_token: ${{ secrets.GITHUB_TOKEN }}
-
-  check_py_code_format:
-    name: Check PY code format
-    runs-on: ubuntu-latest
-    needs: check_js_code_format
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-          ref: ${{ github.head_ref }}
-
-      - name: Check format
-        uses: psf/black@stable
-        with:
-          options: '--exclude="3rd party"'
-          src: "."
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index 08d0e446..00000000
--- a/.prettierignore
+++ /dev/null
@@ -1,12 +0,0 @@
-*.md
-
-# dependencies
-node_modules
-yarn.lock
-
-# production
-dist
-
-# 3rd party
-# https://github.com/killhellokitty/stash-material-ize-theme/blob/main/stash-theme.css
-plugins/themeSwitch/assets/themes/materialize
diff --git a/.prettierrc.json b/.prettierrc.json
deleted file mode 100644
index 757fd64c..00000000
--- a/.prettierrc.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "trailingComma": "es5"
-}
diff --git a/package.json b/package.json
index 1691a6b0..9a4308dc 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,5 @@
 {
   "name": "community-scripts",
   "description": "This repository contains plugin and utility scripts created by the Stash community and hosted on the official GitHub repo.",
-  "license": "AGPL-3.0-only",
-  "scripts": {
-    "format": "prettier --write .",
-    "format-py": "black --exclude=\"3rd party\" ."
-  },
-  "devDependencies": {
-    "prettier": "^3.2.5"
-  }
+  "license": "AGPL-3.0-only"
 }
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 05a538a1..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-black>=24.1.1
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
deleted file mode 100644
index e8e07c44..00000000
--- a/yarn.lock
+++ /dev/null
@@ -1,8 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-prettier@^3.2.5:
-  version "3.2.5"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
-  integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==

From 59a7e587a1a08617884fad25d12d07d8ee65d36f Mon Sep 17 00:00:00 2001
From: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
Date: Tue, 11 Feb 2025 01:55:13 +0200
Subject: [PATCH 2/5] Remove formatting instructions from README

---
 README.md | 28 +---------------------------
 1 file changed, 1 insertion(+), 27 deletions(-)

diff --git a/README.md b/README.md
index 68bef78f..67b25865 100644
--- a/README.md
+++ b/README.md
@@ -78,33 +78,7 @@ To install/run a script follow the install instructions listed in individual REA
 
 ## Contributing
 
-### Formatting
-
-Formatting is enforced on all files. Follow this setup guide:
-
-1. **[Yarn](https://yarnpkg.com/en/docs/install)** and **its dependencies** must be installed to run the formatting tools.
-    ```sh
-    yarn install --frozen-lockfile
-    ```
-
-2. **Python dependencies** must also be installed to format `py` files.
-    ```sh
-    pip install -r requirements.txt
-    ```
-
-#### Formatting non-`py` files
-
-```sh
-yarn run format
-```
-
-#### Formatting `py` files
-
-`py` files are formatted using [`black`](https://pypi.org/project/black/).
-
-```sh
-yarn run format-py
-```
+Submit a PR to add your plugin, theme, userscript and other utility script to the repository. 
 
 ## Deprecation
 

From a280f538a73e4b961a118a3c413a83093d1774cf Mon Sep 17 00:00:00 2001
From: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
Date: Thu, 13 Feb 2025 04:59:33 +0200
Subject: [PATCH 3/5] Add plugin validator

---
 validate.js                  |   3 +
 validator/index.js           | 163 +++++++++++++++++++
 validator/package.json       |  16 ++
 validator/plugin.schema.json | 300 +++++++++++++++++++++++++++++++++++
 validator/yarn.lock          | 210 ++++++++++++++++++++++++
 5 files changed, 692 insertions(+)
 create mode 100644 validate.js
 create mode 100644 validator/index.js
 create mode 100644 validator/package.json
 create mode 100644 validator/plugin.schema.json
 create mode 100644 validator/yarn.lock

diff --git a/validate.js b/validate.js
new file mode 100644
index 00000000..8ac557eb
--- /dev/null
+++ b/validate.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+'use strict';
+require('./validator/index.js')();
\ No newline at end of file
diff --git a/validator/index.js b/validator/index.js
new file mode 100644
index 00000000..d3bc6c6a
--- /dev/null
+++ b/validator/index.js
@@ -0,0 +1,163 @@
+#!/usr/bin/env node
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+const safeRequire = (name) => {
+  try {
+    return require(name);
+  } catch (error) {
+    if (error && error.code === 'MODULE_NOT_FOUND') {
+      console.log(`Error: Cannot find module '${name}', have you installed the dependencies?`);
+      process.exit(1);
+    }
+    throw error;
+  }
+};
+
+const Ajv = safeRequire('ajv').default;
+const betterAjvErrors = safeRequire('better-ajv-errors').default;
+const chalk = safeRequire('chalk');
+const YAML = safeRequire('yaml');
+const addFormats = safeRequire('ajv-formats');
+
+// https://www.peterbe.com/plog/nodejs-fs-walk-or-glob-or-fast-glob
+function walk(directory, ext, filepaths = []) {
+  const files = fs.readdirSync(directory);
+  for (const filename of files) {
+    const filepath = path.join(directory, filename);
+    if (fs.statSync(filepath).isDirectory()) {
+      walk(filepath, ext, filepaths);
+    } else if (path.extname(filename) === ext && !filename.includes('config')) {
+      filepaths.push(filepath);
+    }
+  }
+  return filepaths;
+}
+
+// https://stackoverflow.com/a/53833620
+const isSorted = arr => arr.every((v,i,a) => !i || a[i-1] <= v);
+
+class Validator {
+  constructor(flags) {
+    this.allowDeprecations = flags.includes('-d');
+    this.stopOnError = !flags.includes('-a');
+    this.sortedURLs = flags.includes('-s');
+    this.verbose = flags.includes('-v');
+
+    const schemaPath = path.resolve(__dirname, './plugin.schema.json');
+    this.schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
+    this.ajv = new Ajv({
+      // allErrors: true,
+      allowUnionTypes: true, // Use allowUnionTypes instead of ignoreKeywordsWithRef
+      strict: true,
+      allowMatchingProperties: true, // Allow properties that match a pattern
+    });
+    addFormats(this.ajv);
+  }
+
+  run(files) {
+    let plugins;
+
+    if (files && Array.isArray(files) && files.length > 0) {
+      plugins = files.map(file => path.resolve(file));
+    } else {
+      const pluginsDir = path.resolve(__dirname, '../plugins');
+      const themesDir = path.resolve(__dirname, '../themes');
+      plugins = walk(pluginsDir, '.yml').concat(walk(themesDir, '.yml'));
+    }
+
+    let result = true;
+    const validate = this.ajv.compile(this.schema);
+
+    for (const file of plugins) {
+      const relPath = path.relative(process.cwd(), file);
+      let contents, data;
+      try {
+        contents = fs.readFileSync(file, 'utf8');
+        data = YAML.parse(contents);
+      } catch (error) {
+        console.error(`${chalk.red(chalk.bold('ERROR'))} in: ${relPath}:`);
+        error.stack = null;
+        console.error(error);
+        result = result && false;
+        if (this.stopOnError) break;
+        else continue;
+      }
+
+      let valid = validate(data);
+
+      // Output validation errors
+      if (!valid) {
+        const output = betterAjvErrors(this.schema, data, validate.errors, { indent: 2 });
+        console.log(output);
+
+        // Detailed error checks
+        validate.errors.forEach(err => {
+          switch (err.keyword) {
+            case 'required':
+              console.error(`${chalk.red('Missing Required Property:')} ${err.params.missingProperty}`);
+              break;
+            case 'type':
+              console.error(`${chalk.red('Type Mismatch:')} ${err.dataPath} should be ${err.params.type}`);
+              break;
+            case 'pattern':
+              console.error(`${chalk.red('Pattern Mismatch:')} ${err.dataPath} should match pattern ${err.params.pattern}`);
+              break;
+            case 'enum':
+              console.error(`${chalk.red('Enum Violation:')} ${err.dataPath} should be one of ${err.params.allowedValues.join(', ')}`);
+              break;
+            case 'additionalProperties':
+              console.error(`${chalk.red('Additional Properties:')} ${err.params.additionalProperty} is not allowed`);
+              break;
+            case '$ref':
+              console.error(`${chalk.red('Invalid Reference:')} ${err.dataPath} ${err.message}`);
+              break;
+            case 'items':
+              console.error(`${chalk.red('Array Item Type Mismatch:')} ${err.dataPath} ${err.message}`);
+              break;
+            case 'format':
+              console.error(`${chalk.red('Invalid Format:')} ${err.dataPath} should match format ${err.params.format}`);
+              break;
+            default:
+              console.error(`${chalk.red('Validation Error:')} ${err.dataPath} ${err.message}`);
+          }
+        });
+      }
+
+      if (this.verbose || !valid) {
+        const validColor = valid ? chalk.green : chalk.red;
+        console.log(`${relPath} Valid: ${validColor(valid)}`);
+      }
+
+      result = result && valid;
+
+      if (!valid && this.stopOnError) break;
+    }
+
+    if (!this.verbose && result) {
+      console.log(chalk.green('Validation passed!'));
+    }
+
+    return result;
+  }
+}
+
+function main(flags, files) {
+  const args = process.argv.slice(2)
+  flags = (flags === undefined) ? args.filter(arg => arg.startsWith('-')) : flags;
+  files = (files === undefined) ? args.filter(arg => !arg.startsWith('-')) : files;
+  const validator = new Validator(flags);
+  const result = validator.run(files);
+  if (flags.includes('--ci')) {
+    process.exit(result ? 0 : 1);
+  }
+}
+
+if (require.main === module) {
+  main();
+}
+
+module.exports = main;
+module.exports.Validator = Validator;
\ No newline at end of file
diff --git a/validator/package.json b/validator/package.json
new file mode 100644
index 00000000..f0c4627a
--- /dev/null
+++ b/validator/package.json
@@ -0,0 +1,16 @@
+{
+    "name": "stash-script-validator",
+    "version": "1.0.0",
+    "main": "index.js",
+    "license": "MIT",
+    "scripts": {
+      "main": "node index.js"
+    },
+    "dependencies": {
+      "ajv": "7",
+      "ajv-formats": "^3.0.1",
+      "better-ajv-errors": "^1.2.0",
+      "chalk": "4",
+      "yaml": "^2.5.1"
+    }
+  }
\ No newline at end of file
diff --git a/validator/plugin.schema.json b/validator/plugin.schema.json
new file mode 100644
index 00000000..9822fbfa
--- /dev/null
+++ b/validator/plugin.schema.json
@@ -0,0 +1,300 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "plugin",
+  "title": "Stash Plugin",
+  "description": "A stash plugin config",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["name"],
+  "properties": {
+    "name": {
+      "title": "Plugin name",
+      "description": "The name of the plugin.",
+      "type": "string"
+    },
+    "description": {
+      "title": "Plugin description",
+      "description": "Short description of the plugin.",
+      "type": ["string", "null"]
+    },
+    "version": {
+      "title": "Plugin version",
+      "description": "Format: x.y.z where x y and z are integers.",
+      "type": ["string", "integer", "number"],
+      "pattern": "^\\d+(\\.\\d+)?(\\.\\d+)?$"
+    },
+    "url": {
+      "title": "Url",
+      "description": "Optional url",
+      "type": ["string", "null"]
+    },
+    "ui": {
+      "title": "Plugin UI",
+      "description": "Optional files needed to render plugin specific UI",
+      "$ref": "#/definitions/UIConfig"
+    },
+    "exec": {
+      "title": "Command to run the plugin",
+      "description": "For external plugin tasks, the exec field is a list with the first element being the binary that will be executed, and the subsequent elements are the arguments passed. The execution process will search the path for the binary, then will attempt to find the program in the same directory as the plugin configuration file. The exe extension is not necessary on Windows systems.\n\nFor embedded plugins, the exec field is a list with the first element being the path to the Javascript file that will be executed. It is expected that the path to the Javascript file is relative to the directory of the plugin configuration file.",
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "interface": {
+      "title": "Plugin interface",
+      "description": "For external plugin tasks, the interface field must be set to one of the following values: rpc, raw\n\nFor embedded plugins, the interface field must be set to one of the following values: js\n\nThe interface field defaults to raw if not provided.",
+      "type": "string",
+      "enum": ["js", "raw", "rpc"],
+      "default": "raw"
+    },
+    "errLog": {
+      "title": "Log level",
+      "description": "Tells stash what the default log level should be when the plugin outputs to stderr without encoding a log level. It defaults to the error level if no provided. This field is not necessary if the plugin outputs logging with the appropriate encoding.",
+      "type": "string",
+      "enum": ["trace", "debug", "info", "warning", "error"],
+      "default": "raw"
+    },
+    "hooks": {
+      "title": "Hooks configuration",
+      "description": "Array of individual hook configurations.",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/HookConfig"
+      }
+    },
+    "tasks": {
+      "title": "Tasks configuration",
+      "description": "Array of individual tasks configurations.",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/TaskConfig"
+      }
+    },
+    "settings": {
+      "title": "Plugin settings",
+      "description": "The settings defined for this plugin.",
+      "$ref": "#/definitions/SettingConfig"
+    }
+  },
+  "definitions": {
+    "UIConfig": {
+      "type": "object",
+      "additionalProperties": false,
+      "anyOf": [
+        {
+          "required": ["css"]
+        },
+        {
+          "required": ["javascript"]
+        }
+      ],
+      "properties": {
+        "css": {
+          "title": "CSS files",
+          "description": "Optional list of CSS files to include in the UI",
+          "items": {
+            "description": "Path to CSS file"
+          },
+          "$ref": "#/definitions/StringList"
+        },
+        "javascript": {
+          "title": "Javascript files",
+          "description": "Optional list of Javascript files to include in the UI",
+          "items": {
+            "description": "Path to Javascript file"
+          },
+          "$ref": "#/definitions/StringList"
+        },
+        "requires": {
+          "title": "Required plugins",
+          "description": "Optional list of plugin IDs to load prior to this plugin.",
+          "items": {
+            "description": "Required plugin ID"
+          },
+          "$ref": "#/definitions/StringList"
+        },
+        "assets": {
+          "title": "Assets",
+          "description": "Optional map of assets.",
+          "type": "object",
+          "patternProperties": {
+            "": {
+              "type": "string",
+              "title": "Asset location",
+              "description": "Asset location on the file system as pair of values urlPrefix: fsLocation."
+            }
+          }
+        },
+        "csp": {
+          "title": "Content-security policy overrides",
+          "description": "Optional map of policy directives",
+          "type": "object",
+          "additionalProperties": true,
+          "properties": {
+            "script-src": {
+              "$ref": "#/definitions/CspDirective"
+            },
+            "style-src": {
+              "$ref": "#/definitions/CspDirective"
+            },
+            "connect-src": {
+              "$ref": "#/definitions/CspDirective"
+            }
+          },
+          "patternProperties": {
+            "": {
+              "$ref": "#/definitions/CspDirective"
+            }
+          }
+        },
+        "settings": {
+          "title": "UI plugin settings",
+          "description": "Map of setting names to be displayed in the plugins page in the UI.",
+          "$ref": "#/definitions/SettingConfig"
+        }
+      }
+    },
+    "HookConfig": {
+      "type": "object",
+      "required": ["name", "triggeredBy"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "Hook name"
+        },
+        "description": {
+          "type": ["string", "null"],
+          "description": "Optional description for this hook"
+        },
+        "triggeredBy": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/TriggerType"
+          }
+        },
+        "defaultArgs": {
+          "description": "Default arguments",
+          "type": "object",
+          "items": {
+            "type": "string"
+          }
+        }
+      }
+    },
+    "TriggerType": {
+      "type": "string",
+      "pattern": "^(Scene|SceneMarker|Image|Gallery|GalleryChapter|Movie|Performer|Studio|Tag)\\.(Create|Update|Destroy|Merge)\\.Post$",
+      "enum": [
+        "Scene.Create.Post",
+        "Scene.Update.Post",
+        "Scene.Destroy.Post",
+        "Scene.Merge.Post",
+        "SceneMarker.Create.Post",
+        "SceneMarker.Update.Post",
+        "SceneMarker.Destroy.Post",
+        "SceneMarker.Merge.Post",
+        "Image.Create.Post",
+        "Image.Update.Post",
+        "Image.Destroy.Post",
+        "Image.Merge.Post",
+        "Gallery.Create.Post",
+        "Gallery.Update.Post",
+        "Gallery.Destroy.Post",
+        "Gallery.Merge.Post",
+        "GalleryChapter.Create.Post",
+        "GalleryChapter.Update.Post",
+        "GalleryChapter.Destroy.Post",
+        "Movie.Create.Post",
+        "Movie.Update.Post",
+        "Movie.Destroy.Post",
+        "Movie.Merge.Post",
+        "Performer.Create.Post",
+        "Performer.Update.Post",
+        "Performer.Destroy.Post",
+        "Performer.Merge.Post",
+        "Studio.Create.Post",
+        "Studio.Update.Post",
+        "Studio.Destroy.Post",
+        "Studio.Merge.Post",
+        "Tag.Create.Post",
+        "Tag.Update.Post",
+        "Tag.Destroy.Post",
+        "Tag.Merge.Post"
+      ]
+    },
+    "TaskConfig": {
+      "type": "object",
+      "required": ["name"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "Task name"
+        },
+        "description": {
+          "type": "string",
+          "description": "Optional description for this task"
+        },
+        "defaultArgs": {
+          "description": "Default arguments",
+          "type": "object",
+          "additionalProperties": {
+            "type": "string"
+          }
+        },
+        "execArgs": {
+          "description": "Default arguments",
+          "type": "object",
+          "additionalProperties": {
+            "type": "string"
+          }
+        }
+      }
+    },
+    "SettingConfig": {
+      "type": "object",
+      "patternProperties": {
+        "": {
+          "type": "object",
+          "description": "Internal name",
+          "required": ["displayName", "type"],
+          "properties": {
+            "displayName": {
+              "type": "string",
+              "description": "Name to display in the UI"
+            },
+            "description": {
+              "type": ["string", "null"],
+              "description": "Optional description for this setting"
+            },
+            "type": {
+              "type": "string",
+              "description": "Type of the attribute to show in the UI. It can be one of BOOLEAN, NUMBER, STRING",
+              "enum": ["BOOLEAN", "NUMBER", "STRING"]
+            }
+          }
+        }
+      }
+    },
+    "StringList": {
+      "type": ["array", "null"],
+      "items": {
+        "type": "string"
+      }
+    },
+    "CspDirective": {
+      "description": "Policy directive",
+      "type": ["array", "null"],
+      "items": {
+        "type": "string",
+        "description": "Allowed domain",
+        "examples": [
+          "self",
+          "http://alloweddomain.com",
+          "example.com",
+          "*.example.com"
+        ]
+      }
+    }
+  }
+}
diff --git a/validator/yarn.lock b/validator/yarn.lock
new file mode 100644
index 00000000..58970ef0
--- /dev/null
+++ b/validator/yarn.lock
@@ -0,0 +1,210 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.16.0":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
+  integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==
+  dependencies:
+    "@babel/highlight" "^7.24.7"
+    picocolors "^1.0.0"
+
+"@babel/helper-validator-identifier@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
+  integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
+
+"@babel/highlight@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d"
+  integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.24.7"
+    chalk "^2.4.2"
+    js-tokens "^4.0.0"
+    picocolors "^1.0.0"
+
+"@humanwhocodes/momoa@^2.0.2":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/momoa/-/momoa-2.0.4.tgz#8b9e7a629651d15009c3587d07a222deeb829385"
+  integrity sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==
+
+ajv-formats@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578"
+  integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==
+  dependencies:
+    ajv "^8.0.0"
+
+ajv@7:
+  version "7.2.4"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.4.tgz#8e239d4d56cf884bccca8cca362f508446dc160f"
+  integrity sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+    uri-js "^4.2.2"
+
+ajv@^8.0.0:
+  version "8.17.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
+  integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    fast-uri "^3.0.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+better-ajv-errors@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/better-ajv-errors/-/better-ajv-errors-1.2.0.tgz#6412d58fa4d460ff6ccbd9e65c5fef9781cc5286"
+  integrity sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==
+  dependencies:
+    "@babel/code-frame" "^7.16.0"
+    "@humanwhocodes/momoa" "^2.0.2"
+    chalk "^4.1.2"
+    jsonpointer "^5.0.0"
+    leven "^3.1.0 < 4"
+
+chalk@4, chalk@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-uri@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134"
+  integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+jsonpointer@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
+  integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
+
+"leven@^3.1.0 < 4":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+picocolors@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
+  integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
+
+punycode@^2.1.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+  integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+yaml@^2.5.1:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130"
+  integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==

From a6162ee5c5c28c6a598ed4086e3be6ef51bcb13d Mon Sep 17 00:00:00 2001
From: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
Date: Thu, 13 Feb 2025 04:59:43 +0200
Subject: [PATCH 4/5] Add workflow

---
 .github/workflows/validate.yml | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 .github/workflows/validate.yml

diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 00000000..62b8e0f3
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,18 @@
+name: Validate Plugins
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+jobs:
+  validate:
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/checkout@v4
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20.x'
+    - run: cd ./validator && yarn install --frozen-lockfile
+    - run: node ./validate.js --ci
\ No newline at end of file

From 2d3f4e61dc51fa42bd607b9aa873e909f6a466d7 Mon Sep 17 00:00:00 2001
From: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
Date: Thu, 13 Feb 2025 05:09:43 +0200
Subject: [PATCH 5/5] Limit workflow to plugins/themes directories

---
 .github/workflows/validate.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 62b8e0f3..cdc1f872 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -3,8 +3,14 @@ name: Validate Plugins
 on:
   push:
     branches: [main]
+    paths:
+      - 'plugins/**'
+      - 'themes/**'
   pull_request:
     branches: [main]
+    paths:
+      - 'plugins/**'
+      - 'themes/**'
 
 jobs:
   validate: