Update plugin validation script
Some checks failed
Deploy to GitHub Pages / deploy (push) Failing after 10s
Validate Plugins / validate (push) Successful in 9m24s

This commit is contained in:
Sean O'Connor
2026-03-21 20:21:31 -04:00
parent 14137ba631
commit d772aecc54

View File

@@ -1,35 +1,35 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const fs = require("fs");
const path = require("path");
// Color output helpers
const colors = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m'
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
reset: "\x1b[0m",
};
function log(message, color = 'reset') {
function log(message, color = "reset") {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function error(message) {
log(`${message}`, 'red');
log(`${message}`, "red");
}
function success(message) {
log(`${message}`, 'green');
log(`${message}`, "green");
}
function warn(message) {
log(`⚠️ ${message}`, 'yellow');
log(`⚠️ ${message}`, "yellow");
}
function info(message) {
log(` ${message}`, 'blue');
log(` ${message}`, "blue");
}
// Plugin schema validation
@@ -40,7 +40,7 @@ function validatePlugin(pluginPath) {
let plugin;
try {
plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
plugin = JSON.parse(fs.readFileSync(pluginPath, "utf8"));
} catch (e) {
throw new Error(`Invalid JSON syntax: ${e.message}`);
}
@@ -50,17 +50,17 @@ function validatePlugin(pluginPath) {
// Required fields validation
const requiredFields = [
'robotId',
'name',
'platform',
'version',
'pluginApiVersion',
'hriStudioVersion',
'trustLevel',
'category'
"robotId",
"name",
"platform",
"version",
"pluginApiVersion",
"hriStudioVersion",
"trustLevel",
"category",
];
requiredFields.forEach(field => {
requiredFields.forEach((field) => {
if (!plugin[field]) {
errors.push(`Missing required field: ${field}`);
}
@@ -68,36 +68,43 @@ function validatePlugin(pluginPath) {
// Field format validation
if (plugin.robotId && !/^[a-z0-9-]+$/.test(plugin.robotId)) {
errors.push('robotId must be lowercase with hyphens only');
errors.push("robotId must be lowercase with hyphens only");
}
if (plugin.version && !/^\d+\.\d+\.\d+/.test(plugin.version)) {
errors.push('version must follow semantic versioning (e.g., 1.0.0)');
errors.push("version must follow semantic versioning (e.g., 1.0.0)");
}
if (plugin.trustLevel && !['official', 'verified', 'community'].includes(plugin.trustLevel)) {
errors.push(`Invalid trustLevel: ${plugin.trustLevel}. Must be: official, verified, or community`);
if (
plugin.trustLevel &&
!["official", "verified", "community"].includes(plugin.trustLevel)
) {
errors.push(
`Invalid trustLevel: ${plugin.trustLevel}. Must be: official, verified, or community`,
);
}
// Category validation
const validCategories = [
'mobile-robot',
'humanoid-robot',
'manipulator',
'drone',
'sensor-platform',
'simulation'
"mobile-robot",
"humanoid-robot",
"manipulator",
"drone",
"sensor-platform",
"simulation",
];
if (plugin.category && !validCategories.includes(plugin.category)) {
errors.push(`Invalid category: ${plugin.category}. Valid categories: ${validCategories.join(', ')}`);
errors.push(
`Invalid category: ${plugin.category}. Valid categories: ${validCategories.join(", ")}`,
);
}
// Actions validation
if (!plugin.actions || !Array.isArray(plugin.actions)) {
errors.push('Plugin must have an actions array');
errors.push("Plugin must have an actions array");
} else if (plugin.actions.length === 0) {
warnings.push('Plugin has no actions defined');
warnings.push("Plugin has no actions defined");
} else {
plugin.actions.forEach((action, index) => {
const actionErrors = validateAction(action, index);
@@ -108,37 +115,43 @@ function validatePlugin(pluginPath) {
// Assets validation
if (plugin.assets) {
if (!plugin.assets.thumbnailUrl) {
errors.push('assets.thumbnailUrl is required');
errors.push("assets.thumbnailUrl is required");
}
// Check if asset paths exist
const assetChecks = [
['thumbnailUrl', plugin.assets.thumbnailUrl],
['main image', plugin.assets.images?.main],
['logo', plugin.assets.images?.logo]
["thumbnailUrl", plugin.assets.thumbnailUrl],
["main image", plugin.assets.images?.main],
["logo", plugin.assets.images?.logo],
];
if (plugin.assets.images?.angles) {
Object.entries(plugin.assets.images.angles).forEach(([angle, assetPath]) => {
Object.entries(plugin.assets.images.angles).forEach(
([angle, assetPath]) => {
assetChecks.push([`${angle} angle`, assetPath]);
});
},
);
}
assetChecks.forEach(([description, assetPath]) => {
if (assetPath && assetPath.startsWith('assets/')) {
const fullPath = path.resolve(path.dirname(pluginPath), '..', assetPath);
if (assetPath && assetPath.startsWith("assets/")) {
const fullPath = path.resolve(
path.dirname(pluginPath),
"..",
assetPath,
);
if (!fs.existsSync(fullPath)) {
warnings.push(`Asset not found: ${description} (${assetPath})`);
}
}
});
} else {
errors.push('Plugin must have assets definition');
errors.push("Plugin must have assets definition");
}
// Manufacturer validation
if (!plugin.manufacturer?.name) {
warnings.push('manufacturer.name is recommended');
warnings.push("manufacturer.name is recommended");
}
return { errors, warnings, plugin };
@@ -148,8 +161,8 @@ function validateAction(action, index) {
const errors = [];
// Required action fields
const requiredFields = ['id', 'name', 'category', 'parameterSchema'];
requiredFields.forEach(field => {
const requiredFields = ["id", "name", "category", "parameterSchema"];
requiredFields.forEach((field) => {
if (!action[field]) {
errors.push(`Action ${index}: missing required field '${field}'`);
}
@@ -157,18 +170,22 @@ function validateAction(action, index) {
// Action ID format
if (action.id && !/^[a-z_]+$/.test(action.id)) {
errors.push(`Action ${index}: id must be snake_case (lowercase with underscores)`);
errors.push(
`Action ${index}: id must be snake_case (lowercase with underscores)`,
);
}
// Action category validation
const validActionCategories = ['movement', 'interaction', 'sensors', 'logic'];
const validActionCategories = ["movement", "interaction", "sensors", "logic"];
if (action.category && !validActionCategories.includes(action.category)) {
errors.push(`Action ${index}: invalid category '${action.category}'. Valid: ${validActionCategories.join(', ')}`);
errors.push(
`Action ${index}: invalid category '${action.category}'. Valid: ${validActionCategories.join(", ")}`,
);
}
// Parameter schema validation
if (action.parameterSchema) {
if (action.parameterSchema.type !== 'object') {
if (action.parameterSchema.type !== "object") {
errors.push(`Action ${index}: parameterSchema.type must be 'object'`);
}
@@ -187,7 +204,9 @@ function validateAction(action, index) {
const hasRestApi = action.restApi;
if (!hasRos2 && !hasNaoqi && !hasRestApi) {
errors.push(`Action ${index}: must have at least one communication protocol (ros2, naoqi, or restApi)`);
errors.push(
`Action ${index}: must have at least one communication protocol (ros2, naoqi, or restApi)`,
);
}
return errors;
@@ -195,15 +214,15 @@ function validateAction(action, index) {
// Repository validation
function validateRepository() {
const repoPath = path.resolve('repository.json');
const repoPath = path.resolve("repository.json");
if (!fs.existsSync(repoPath)) {
throw new Error('repository.json not found');
throw new Error("repository.json not found");
}
let repo;
try {
repo = JSON.parse(fs.readFileSync(repoPath, 'utf8'));
repo = JSON.parse(fs.readFileSync(repoPath, "utf8"));
} catch (e) {
throw new Error(`Invalid repository.json: ${e.message}`);
}
@@ -212,22 +231,30 @@ function validateRepository() {
const warnings = [];
// Required repository fields
const requiredFields = ['id', 'name', 'apiVersion', 'pluginApiVersion', 'trust'];
requiredFields.forEach(field => {
const requiredFields = [
"id",
"name",
"apiVersion",
"pluginApiVersion",
"trust",
];
requiredFields.forEach((field) => {
if (!repo[field]) {
errors.push(`Missing required repository field: ${field}`);
}
});
// Validate plugin count
const indexPath = path.resolve('plugins/index.json');
const indexPath = path.resolve("plugins/index.json");
if (fs.existsSync(indexPath)) {
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
const actualCount = index.length;
const reportedCount = repo.stats?.plugins || 0;
if (actualCount !== reportedCount) {
errors.push(`Plugin count mismatch: reported ${reportedCount}, actual ${actualCount}`);
errors.push(
`Plugin count mismatch: reported ${reportedCount}, actual ${actualCount}`,
);
}
}
@@ -236,24 +263,25 @@ function validateRepository() {
// Update plugin index
function updateIndex() {
const pluginsDir = path.resolve('plugins');
const indexPath = path.join(pluginsDir, 'index.json');
const pluginsDir = path.resolve("plugins");
const indexPath = path.join(pluginsDir, "index.json");
if (!fs.existsSync(pluginsDir)) {
throw new Error('plugins directory not found');
throw new Error("plugins directory not found");
}
const pluginFiles = fs.readdirSync(pluginsDir)
.filter(file => file.endsWith('.json') && file !== 'index.json')
const pluginFiles = fs
.readdirSync(pluginsDir)
.filter((file) => file.endsWith(".json") && file !== "index.json")
.sort();
fs.writeFileSync(indexPath, JSON.stringify(pluginFiles, null, 2));
success(`Updated index.json with ${pluginFiles.length} plugins`);
// Update repository stats
const repoPath = path.resolve('repository.json');
const repoPath = path.resolve("repository.json");
if (fs.existsSync(repoPath)) {
const repo = JSON.parse(fs.readFileSync(repoPath, 'utf8'));
const repo = JSON.parse(fs.readFileSync(repoPath, "utf8"));
repo.stats = repo.stats || {};
repo.stats.plugins = pluginFiles.length;
fs.writeFileSync(repoPath, JSON.stringify(repo, null, 2));
@@ -268,10 +296,10 @@ function main() {
try {
switch (command) {
case 'validate':
case "validate":
const pluginPath = args[1];
if (!pluginPath) {
error('Usage: validate <plugin-file>');
error("Usage: validate <plugin-file>");
process.exit(1);
}
@@ -279,53 +307,53 @@ function main() {
const { errors, warnings } = validatePlugin(pluginPath);
if (errors.length > 0) {
error('Validation failed:');
errors.forEach(err => console.log(` - ${err}`));
error("Validation failed:");
errors.forEach((err) => console.log(` - ${err}`));
}
if (warnings.length > 0) {
warn('Warnings:');
warnings.forEach(warn => console.log(` - ${warn}`));
warn("Warnings:");
warnings.forEach((warn) => console.log(` - ${warn}`));
}
if (errors.length === 0) {
success('Plugin validation passed!');
success("Plugin validation passed!");
if (warnings.length === 0) {
success('No warnings found');
success("No warnings found");
}
} else {
process.exit(1);
}
break;
case 'validate-all':
info('Validating all plugins...');
case "validate-all":
info("Validating all plugins...");
// Validate repository
const repoResult = validateRepository();
if (repoResult.errors.length > 0) {
error('Repository validation failed:');
repoResult.errors.forEach(err => console.log(` - ${err}`));
error("Repository validation failed:");
repoResult.errors.forEach((err) => console.log(` - ${err}`));
process.exit(1);
}
// Validate all plugins
const indexPath = path.resolve('plugins/index.json');
const indexPath = path.resolve("plugins/index.json");
if (!fs.existsSync(indexPath)) {
error('plugins/index.json not found');
error("plugins/index.json not found");
process.exit(1);
}
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
let allValid = true;
for (const pluginFile of index) {
const pluginPath = path.resolve('plugins', pluginFile);
const pluginPath = path.resolve("plugins", pluginFile);
try {
const { errors } = validatePlugin(pluginPath);
if (errors.length > 0) {
error(`${pluginFile}: ${errors.length} errors`);
errors.forEach(err => console.log(` - ${err}`));
errors.forEach((err) => console.log(` - ${err}`));
allValid = false;
} else {
success(`${pluginFile}: valid`);
@@ -337,18 +365,18 @@ function main() {
}
if (allValid) {
success('All plugins are valid!');
success("All plugins are valid!");
} else {
process.exit(1);
}
break;
case 'update-index':
info('Updating plugin index...');
case "update-index":
info("Updating plugin index...");
updateIndex();
break;
case 'help':
case "help":
default:
console.log(`
HRIStudio Plugin Validator
@@ -379,5 +407,5 @@ if (require.main === module) {
module.exports = {
validatePlugin,
validateRepository,
updateIndex
updateIndex,
};