Initial commit
This commit is contained in:
60
skills/wordpress-ability-api/scripts/README.md
Normal file
60
skills/wordpress-ability-api/scripts/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# WordPress Ability API Scripts
|
||||
|
||||
This directory contains automation scripts for scaffolding and validating WordPress Abilities and Categories.
|
||||
|
||||
## Scripts
|
||||
|
||||
### Ability Scripts
|
||||
|
||||
- **scaffold-ability.php** - Generate ability code from CLI arguments
|
||||
- **validate-ability.php** - Validate PHP ability registration code
|
||||
|
||||
### Category Scripts
|
||||
|
||||
- **scaffold-category.php** - Generate category registration code (PHP or JS)
|
||||
- **validate-category.php** - Validate PHP category registration code
|
||||
- **validate-category.js** - Validate JavaScript category registration code
|
||||
|
||||
## Requirements
|
||||
|
||||
### PHP Scripts
|
||||
|
||||
- PHP 7.0+ (uses `token_get_all()` for parsing)
|
||||
- No external dependencies
|
||||
|
||||
### JavaScript Validator
|
||||
|
||||
- Node.js
|
||||
- acorn parser
|
||||
|
||||
To install acorn:
|
||||
|
||||
```bash
|
||||
npm install acorn
|
||||
```
|
||||
|
||||
Or install globally:
|
||||
|
||||
```bash
|
||||
npm install -g acorn
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
See individual script headers for detailed usage instructions.
|
||||
|
||||
Quick examples:
|
||||
|
||||
```bash
|
||||
# Scaffold a server-side ability
|
||||
php scaffold-ability.php --name="plugin/ability" --type="server" --category="data-retrieval"
|
||||
|
||||
# Scaffold a category
|
||||
php scaffold-category.php --name="custom-category" --label="Custom" --description="Description" --type="server"
|
||||
|
||||
# Validate PHP category
|
||||
php validate-category.php path/to/category.php
|
||||
|
||||
# Validate JavaScript category
|
||||
node validate-category.js path/to/category.js
|
||||
```
|
||||
5
skills/wordpress-ability-api/scripts/package.json
Normal file
5
skills/wordpress-ability-api/scripts/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0"
|
||||
}
|
||||
}
|
||||
141
skills/wordpress-ability-api/scripts/scaffold-ability.php
Executable file
141
skills/wordpress-ability-api/scripts/scaffold-ability.php
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* WordPress Ability Scaffolding Script
|
||||
*
|
||||
* Generates ability registration code from templates based on command-line arguments.
|
||||
* Designed to be called by AI agents to quickly scaffold abilities.
|
||||
*
|
||||
* Usage:
|
||||
* php scaffold-ability.php --name="plugin/ability" --type="server" --category="data-retrieval"
|
||||
*
|
||||
* Required Arguments:
|
||||
* --name Ability name in format "namespace/ability-name"
|
||||
* --type Type of ability: "server" or "client"
|
||||
* --category Category slug (e.g., "data-retrieval", "data-modification")
|
||||
*
|
||||
* Optional Arguments:
|
||||
* --label Human-readable label (default: generated from name)
|
||||
* --description Detailed description (default: placeholder)
|
||||
* --readonly Whether ability only reads data: "true" or "false" (default: false)
|
||||
* --destructive Whether ability can delete data: "true" or "false" (default: true)
|
||||
* --idempotent Whether repeated calls are safe: "true" or "false" (default: false)
|
||||
*
|
||||
* Output:
|
||||
* Complete ability registration code printed to stdout
|
||||
*
|
||||
* Exit Codes:
|
||||
* 0 - Success
|
||||
* 1 - Missing required arguments
|
||||
* 2 - Invalid argument values
|
||||
*/
|
||||
|
||||
// Parse command-line arguments
|
||||
$options = getopt('', [
|
||||
'name:',
|
||||
'type:',
|
||||
'category:',
|
||||
'label::',
|
||||
'description::',
|
||||
'readonly::',
|
||||
'destructive::',
|
||||
'idempotent::',
|
||||
]);
|
||||
|
||||
// Validate required arguments
|
||||
$required = ['name', 'type', 'category'];
|
||||
$missing = array_diff($required, array_keys($options));
|
||||
|
||||
if (!empty($missing)) {
|
||||
fwrite(STDERR, "Error: Missing required arguments: " . implode(', ', $missing) . "\n");
|
||||
fwrite(STDERR, "Usage: php scaffold-ability.php --name=\"plugin/ability\" --type=\"server\" --category=\"data-retrieval\"\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Extract and validate arguments
|
||||
$name = $options['name'];
|
||||
$type = strtolower($options['type']);
|
||||
$category = $options['category'];
|
||||
|
||||
// Validate ability name format
|
||||
if (!preg_match('/^[a-z0-9-]+\/[a-z0-9-]+$/', $name)) {
|
||||
fwrite(STDERR, "Error: Invalid ability name format. Must be 'namespace/ability-name' (lowercase, hyphens allowed)\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (!in_array($type, ['server', 'client'])) {
|
||||
fwrite(STDERR, "Error: Invalid type. Must be 'server' or 'client'\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Parse optional arguments with defaults
|
||||
$label = $options['label'] ?? generate_label_from_name($name);
|
||||
$description = $options['description'] ?? 'TODO: Add a detailed description of what this ability does.';
|
||||
$readonly = parse_bool($options['readonly'] ?? 'false');
|
||||
$destructive = parse_bool($options['destructive'] ?? 'true');
|
||||
$idempotent = parse_bool($options['idempotent'] ?? 'false');
|
||||
|
||||
// Load the appropriate template and generate code
|
||||
$template_file = $type === 'server'
|
||||
? __DIR__ . '/../assets/server-ability-template.php'
|
||||
: __DIR__ . '/../assets/client-ability-template.js';
|
||||
|
||||
if (!file_exists($template_file)) {
|
||||
fwrite(STDERR, "Error: Template file not found: {$template_file}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$template = file_get_contents($template_file);
|
||||
if ($template === false) {
|
||||
fwrite(STDERR, "Error: Unable to read template file: {$template_file}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Prepare placeholder replacements
|
||||
$namespace = explode('/', $name)[0];
|
||||
$ability_slug = str_replace(['/', '-'], '_', $name);
|
||||
|
||||
$placeholders = [
|
||||
'{{ABILITY_NAME}}' => $name,
|
||||
'{{NAMESPACE}}' => $namespace,
|
||||
'{{LABEL}}' => $label,
|
||||
'{{DESCRIPTION}}' => $description,
|
||||
'{{CATEGORY}}' => $category,
|
||||
'{{CALLBACK_FUNCTION}}' => $ability_slug . '_callback',
|
||||
'{{REGISTER_FUNCTION}}' => $ability_slug . '_register',
|
||||
'{{READONLY}}' => $readonly ? 'true' : 'false',
|
||||
'{{DESTRUCTIVE}}' => $destructive ? 'true' : 'false',
|
||||
'{{IDEMPOTENT}}' => $idempotent ? 'true' : 'false',
|
||||
];
|
||||
|
||||
// Replace placeholders
|
||||
$output = str_replace(array_keys($placeholders), array_values($placeholders), $template);
|
||||
|
||||
// Output the result
|
||||
echo $output;
|
||||
|
||||
exit(0);
|
||||
|
||||
/**
|
||||
* Generate a human-readable label from the ability name.
|
||||
*
|
||||
* @param string $name Ability name in format "namespace/ability-name"
|
||||
* @return string Human-readable label
|
||||
*/
|
||||
function generate_label_from_name($name) {
|
||||
$parts = explode('/', $name);
|
||||
$ability_part = end($parts);
|
||||
$words = explode('-', $ability_part);
|
||||
return ucwords(implode(' ', $words));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse boolean string to actual boolean.
|
||||
*
|
||||
* @param string $value String value ("true" or "false")
|
||||
* @return bool
|
||||
*/
|
||||
function parse_bool($value) {
|
||||
return strtolower($value) === 'true';
|
||||
}
|
||||
97
skills/wordpress-ability-api/scripts/scaffold-category.php
Executable file
97
skills/wordpress-ability-api/scripts/scaffold-category.php
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* WordPress Ability Category Scaffolding Script
|
||||
*
|
||||
* Generates category registration code from templates based on command-line arguments.
|
||||
* Designed to be called by AI agents to quickly scaffold categories.
|
||||
*
|
||||
* Usage:
|
||||
* php scaffold-category.php --name="category-slug" --label="Category Label" --description="Description" --type="server"
|
||||
*
|
||||
* Required Arguments:
|
||||
* --name Category slug (kebab-case, e.g., "data-retrieval")
|
||||
* --label Human-readable label
|
||||
* --description Detailed description of what abilities belong in this category
|
||||
* --type Type: "server" (PHP) or "client" (JavaScript)
|
||||
*
|
||||
* Output:
|
||||
* Complete category registration code printed to stdout
|
||||
*
|
||||
* Exit Codes:
|
||||
* 0 - Success
|
||||
* 1 - Missing required arguments
|
||||
* 2 - Invalid argument values
|
||||
*/
|
||||
|
||||
// Parse command-line arguments
|
||||
$options = getopt('', [
|
||||
'name:',
|
||||
'label:',
|
||||
'description:',
|
||||
'type:',
|
||||
]);
|
||||
|
||||
// Validate required arguments
|
||||
$required = ['name', 'label', 'description', 'type'];
|
||||
$missing = array_diff($required, array_keys($options));
|
||||
|
||||
if (!empty($missing)) {
|
||||
fwrite(STDERR, "Error: Missing required arguments: " . implode(', ', $missing) . "\n");
|
||||
fwrite(STDERR, "Usage: php scaffold-category.php --name=\"category-slug\" --label=\"Label\" --description=\"Description\" --type=\"server\"\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Extract and validate arguments
|
||||
$name = $options['name'];
|
||||
$label = $options['label'];
|
||||
$description = $options['description'];
|
||||
$type = strtolower($options['type']);
|
||||
|
||||
// Validate category name format (kebab-case)
|
||||
if (!preg_match('/^[a-z0-9-]+$/', $name)) {
|
||||
fwrite(STDERR, "Error: Invalid category name format. Must be kebab-case (lowercase, hyphens allowed, e.g., 'data-retrieval')\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (!in_array($type, ['server', 'client'])) {
|
||||
fwrite(STDERR, "Error: Invalid type. Must be 'server' or 'client'\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Load the appropriate template
|
||||
$template_file = $type === 'server'
|
||||
? __DIR__ . '/../assets/category-template.php'
|
||||
: __DIR__ . '/../assets/client-category-template.js';
|
||||
|
||||
if (!file_exists($template_file)) {
|
||||
fwrite(STDERR, "Error: Template file not found: {$template_file}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$template = file_get_contents($template_file);
|
||||
if ($template === false) {
|
||||
fwrite(STDERR, "Error: Unable to read template file: {$template_file}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Prepare placeholder replacements
|
||||
$register_function = str_replace('-', '_', $name) . '_category_register';
|
||||
$namespace = 'text-domain'; // Generic text domain for templates
|
||||
|
||||
$placeholders = [
|
||||
'{{CATEGORY_NAME}}' => $name,
|
||||
'{{LABEL}}' => $label,
|
||||
'{{DESCRIPTION}}' => $description,
|
||||
'{{REGISTER_FUNCTION}}' => $register_function,
|
||||
'{{NAMESPACE}}' => $namespace,
|
||||
];
|
||||
|
||||
// Replace placeholders
|
||||
$output = str_replace(array_keys($placeholders), array_values($placeholders), $template);
|
||||
|
||||
// Output the result
|
||||
echo $output;
|
||||
|
||||
exit(0);
|
||||
439
skills/wordpress-ability-api/scripts/validate-ability.js
Executable file
439
skills/wordpress-ability-api/scripts/validate-ability.js
Executable file
@@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* WordPress Ability Validation Script (JavaScript)
|
||||
*
|
||||
* Validates JavaScript ability registration code using acorn AST parser.
|
||||
* For PHP validation, use validate-ability.php instead.
|
||||
*
|
||||
* Usage:
|
||||
* node validate-ability.js path/to/ability-file.js
|
||||
*
|
||||
* Requirements:
|
||||
* - Node.js
|
||||
* - acorn parser: npm install acorn
|
||||
*
|
||||
* Arguments:
|
||||
* file_path Path to the JavaScript file containing ability registration
|
||||
*
|
||||
* Exit Codes:
|
||||
* 0 - Validation passed
|
||||
* 1 - Validation failed
|
||||
* 2 - File not found, invalid usage, or missing dependencies
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Check for acorn dependency
|
||||
let acorn;
|
||||
try {
|
||||
acorn = require('acorn');
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Error: acorn parser not found. Install with: npm install acorn'
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Check for file argument
|
||||
if (process.argv.length < 3) {
|
||||
console.error('Usage: node validate-ability.js path/to/ability-file.js');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const filePath = process.argv[2];
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`Error: File not found: ${filePath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Check if file is JavaScript
|
||||
const ext = path.extname(filePath);
|
||||
if (!['.js', '.jsx', '.ts', '.tsx', '.mjs'].includes(ext)) {
|
||||
console.error(
|
||||
'Error: This validator only supports JavaScript files. For PHP validation, use validate-ability.php'
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, 'utf8');
|
||||
} catch (e) {
|
||||
console.error(`Error: Unable to read file: ${filePath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Initialize validation results
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
// Parse and validate JavaScript abilities
|
||||
try {
|
||||
const ast = acorn.parse(content, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
allowAwaitOutsideFunction: true,
|
||||
});
|
||||
|
||||
const abilities = extractAbilities(ast);
|
||||
|
||||
if (abilities.length === 0) {
|
||||
errors.push('No registerAbility() calls found in file');
|
||||
} else {
|
||||
abilities.forEach((ability) => {
|
||||
console.log(`Validating ability: ${ability.name || '(unnamed)'}`);
|
||||
validateAbility(ability, errors, warnings);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
errors.push(`JavaScript syntax error: ${e.message}`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Output final results
|
||||
outputResults(filePath, errors, warnings);
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(errors.length > 0 ? 1 : 0);
|
||||
|
||||
/**
|
||||
* Extract ability registrations from AST.
|
||||
*
|
||||
* @param {Object} ast - Acorn AST
|
||||
* @returns {Array} Array of ability objects
|
||||
*/
|
||||
function extractAbilities(ast) {
|
||||
const abilities = [];
|
||||
const processed = new Set(); // Track processed nodes to avoid duplicates
|
||||
|
||||
// Walk the AST to find registerAbility calls
|
||||
walk(ast, (node) => {
|
||||
let callNode = null;
|
||||
|
||||
// Direct call: registerAbility()
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'registerAbility' &&
|
||||
!processed.has(node)
|
||||
) {
|
||||
callNode = node;
|
||||
}
|
||||
|
||||
// Await call: await registerAbility()
|
||||
if (
|
||||
node.type === 'AwaitExpression' &&
|
||||
node.argument.type === 'CallExpression' &&
|
||||
node.argument.callee.type === 'Identifier' &&
|
||||
node.argument.callee.name === 'registerAbility' &&
|
||||
!processed.has(node.argument)
|
||||
) {
|
||||
callNode = node.argument;
|
||||
}
|
||||
|
||||
if (callNode) {
|
||||
processed.add(callNode);
|
||||
const ability = extractAbilityFromCall(callNode);
|
||||
if (ability) {
|
||||
abilities.push(ability);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return abilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ability data from a CallExpression node.
|
||||
*
|
||||
* @param {Object} node - CallExpression AST node
|
||||
* @returns {Object|null} Ability object or null
|
||||
*/
|
||||
function extractAbilityFromCall(node) {
|
||||
if (node.arguments.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ability = {
|
||||
name: null,
|
||||
label: null,
|
||||
description: null,
|
||||
category: null,
|
||||
hasCallback: false,
|
||||
hasPermissionCallback: false,
|
||||
hasInputSchema: false,
|
||||
hasOutputSchema: false,
|
||||
annotations: {},
|
||||
};
|
||||
|
||||
// First argument: config object
|
||||
const configArg = node.arguments[0];
|
||||
if (configArg.type !== 'ObjectExpression') {
|
||||
return null; // Config must be an object
|
||||
}
|
||||
|
||||
// Extract properties from config object
|
||||
configArg.properties.forEach((prop) => {
|
||||
if (prop.type !== 'Property' || prop.key.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = prop.key.name;
|
||||
|
||||
// Extract string fields
|
||||
if (['name', 'label', 'description', 'category'].includes(key)) {
|
||||
if (
|
||||
prop.value.type === 'Literal' &&
|
||||
typeof prop.value.value === 'string'
|
||||
) {
|
||||
ability[key] = prop.value.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for callback (function or arrow function)
|
||||
if (key === 'callback') {
|
||||
ability.hasCallback = [
|
||||
'FunctionExpression',
|
||||
'ArrowFunctionExpression',
|
||||
].includes(prop.value.type);
|
||||
}
|
||||
|
||||
// Check for permissionCallback
|
||||
if (key === 'permissionCallback') {
|
||||
ability.hasPermissionCallback = [
|
||||
'FunctionExpression',
|
||||
'ArrowFunctionExpression',
|
||||
].includes(prop.value.type);
|
||||
}
|
||||
|
||||
// Check for schemas (object expressions)
|
||||
if (key === 'inputSchema' && prop.value.type === 'ObjectExpression') {
|
||||
ability.hasInputSchema = true;
|
||||
}
|
||||
|
||||
if (key === 'outputSchema' && prop.value.type === 'ObjectExpression') {
|
||||
ability.hasOutputSchema = true;
|
||||
}
|
||||
|
||||
// Extract annotations from meta
|
||||
if (key === 'meta' && prop.value.type === 'ObjectExpression') {
|
||||
prop.value.properties.forEach((metaProp) => {
|
||||
if (
|
||||
metaProp.type === 'Property' &&
|
||||
metaProp.key.type === 'Identifier' &&
|
||||
metaProp.key.name === 'annotations' &&
|
||||
metaProp.value.type === 'ObjectExpression'
|
||||
) {
|
||||
metaProp.value.properties.forEach((annoProp) => {
|
||||
if (
|
||||
annoProp.type === 'Property' &&
|
||||
annoProp.key.type === 'Identifier'
|
||||
) {
|
||||
const annoKey = annoProp.key.name;
|
||||
if (
|
||||
[
|
||||
'readonly',
|
||||
'destructive',
|
||||
'idempotent',
|
||||
].includes(annoKey)
|
||||
) {
|
||||
if (
|
||||
annoProp.value.type === 'Literal' &&
|
||||
typeof annoProp.value.value === 'boolean'
|
||||
) {
|
||||
ability.annotations[annoKey] =
|
||||
annoProp.value.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ability data.
|
||||
*
|
||||
* @param {Object} ability - Ability data
|
||||
* @param {Array} errors - Error messages array
|
||||
* @param {Array} warnings - Warning messages array
|
||||
*/
|
||||
function validateAbility(ability, errors, warnings) {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
category,
|
||||
hasCallback,
|
||||
hasPermissionCallback,
|
||||
hasInputSchema,
|
||||
hasOutputSchema,
|
||||
annotations,
|
||||
} = ability;
|
||||
|
||||
// Validate required fields
|
||||
if (!name) {
|
||||
errors.push('Missing required field: name');
|
||||
return; // Can't continue without a name
|
||||
}
|
||||
|
||||
// Validate name format (namespace/ability-name in kebab-case)
|
||||
if (!/^[a-z0-9-]+\/[a-z0-9-]+$/.test(name)) {
|
||||
errors.push(
|
||||
`Invalid ability name format: '${name}'. Must be 'namespace/ability-name' in kebab-case (e.g., 'my-plugin/do-something')`
|
||||
);
|
||||
}
|
||||
|
||||
if (!label) {
|
||||
errors.push(`Missing required field 'label' in ability '${name}'`);
|
||||
} else {
|
||||
// Validate label quality
|
||||
if (label.trim().length < 2) {
|
||||
warnings.push(
|
||||
`Label for ability '${name}' is too short. Provide a meaningful label.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
errors.push(
|
||||
`Missing required field 'description' in ability '${name}'`
|
||||
);
|
||||
} else {
|
||||
// Validate description quality
|
||||
const descLength = description.trim().length;
|
||||
|
||||
if (descLength < 20) {
|
||||
warnings.push(
|
||||
`Description for ability '${name}' is too short (${descLength} chars). Provide detailed information about what this ability does, when to use it, and what parameters it accepts.`
|
||||
);
|
||||
}
|
||||
|
||||
if (/TODO/i.test(description)) {
|
||||
warnings.push(
|
||||
`Description for ability '${name}' contains TODO placeholder. Replace with actual description.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!category) {
|
||||
errors.push(`Missing required field 'category' in ability '${name}'`);
|
||||
}
|
||||
|
||||
// Validate callback presence
|
||||
if (!hasCallback) {
|
||||
errors.push(
|
||||
`Missing required field 'callback' in ability '${name}'. Abilities must have an execute callback function.`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate permission callback
|
||||
if (!hasPermissionCallback) {
|
||||
warnings.push(
|
||||
`Missing 'permissionCallback' in ability '${name}'. Consider adding permission checks to control who can execute this ability.`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate schemas (recommended if ability takes input or provides output)
|
||||
if (!hasInputSchema) {
|
||||
warnings.push(
|
||||
`Missing 'inputSchema' in ability '${name}'. Input schema should be provided if this ability accepts parameters.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasOutputSchema) {
|
||||
warnings.push(
|
||||
`Missing 'outputSchema' in ability '${name}'. Output schema should be provided if this ability returns data.`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate annotations
|
||||
if ('readonly' in annotations && 'destructive' in annotations) {
|
||||
if (annotations.readonly === true && annotations.destructive === true) {
|
||||
warnings.push(
|
||||
`Conflicting annotations in ability '${name}': readonly=true and destructive=true. A readonly ability should have destructive=false.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple AST walker.
|
||||
*
|
||||
* @param {Object} node - AST node
|
||||
* @param {Function} callback - Function to call for each node
|
||||
*/
|
||||
function walk(node, callback) {
|
||||
if (!node || typeof node !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(node);
|
||||
|
||||
for (const key in node) {
|
||||
if (
|
||||
key === 'loc' ||
|
||||
key === 'range' ||
|
||||
key === 'start' ||
|
||||
key === 'end'
|
||||
) {
|
||||
continue; // Skip position metadata
|
||||
}
|
||||
|
||||
const value = node[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((child) => walk(child, callback));
|
||||
} else if (value && typeof value === 'object') {
|
||||
walk(value, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output validation results.
|
||||
*
|
||||
* @param {string} filePath - File being validated
|
||||
* @param {Array} errors - Error messages
|
||||
* @param {Array} warnings - Warning messages
|
||||
*/
|
||||
function outputResults(filePath, errors, warnings) {
|
||||
console.log('='.repeat(70));
|
||||
console.log(`Validation Results: ${filePath}`);
|
||||
console.log('='.repeat(70));
|
||||
console.log('');
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(`ERRORS (${errors.length}):`);
|
||||
errors.forEach((error) => console.log(` ✗ ${error}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log(`WARNINGS (${warnings.length}):`);
|
||||
warnings.forEach((warning) => console.log(` ⚠ ${warning}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
console.log('✓ All validations passed! No issues found.');
|
||||
console.log('');
|
||||
} else if (errors.length === 0) {
|
||||
console.log('✓ Validation passed with warnings.');
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('✗ Validation failed. Please fix the errors above.');
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
306
skills/wordpress-ability-api/scripts/validate-ability.php
Executable file
306
skills/wordpress-ability-api/scripts/validate-ability.php
Executable file
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* WordPress Ability Validation Script
|
||||
*
|
||||
* Validates ability registration code independently of WordPress.
|
||||
* Checks structure, required fields, and JSON Schema validity.
|
||||
*
|
||||
* Usage:
|
||||
* php validate-ability.php path/to/ability-file.php
|
||||
*
|
||||
* Arguments:
|
||||
* file_path Path to the PHP file containing ability registration code
|
||||
*
|
||||
* Exit Codes:
|
||||
* 0 - Validation passed
|
||||
* 1 - Validation failed
|
||||
* 2 - File not found or invalid usage
|
||||
*/
|
||||
|
||||
// Check for file argument
|
||||
if ($argc < 2) {
|
||||
fwrite(STDERR, "Usage: php validate-ability.php path/to/ability-file.php\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$file_path = $argv[1];
|
||||
|
||||
// Check if file exists
|
||||
if (!file_exists($file_path)) {
|
||||
fwrite(STDERR, "Error: File not found: {$file_path}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
$content = file_get_contents($file_path);
|
||||
if ($content === false) {
|
||||
fwrite(STDERR, "Error: Unable to read file: {$file_path}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Initialize validation results
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
// Extract wp_register_ability calls
|
||||
$pattern = '/wp_register_ability\s*\(\s*[\'"]([^\'"]+)[\'"]\s*,\s*array\s*\((.*?)\)\s*\)\s*;/s';
|
||||
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
|
||||
|
||||
if (empty($matches)) {
|
||||
$errors[] = "No wp_register_ability() calls found in file";
|
||||
output_results($file_path, $errors, $warnings);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Validate each ability registration
|
||||
foreach ($matches as $index => $match) {
|
||||
$ability_name = $match[1];
|
||||
$args_string = $match[2];
|
||||
|
||||
echo "Validating ability: {$ability_name}\n";
|
||||
|
||||
// Validate ability name format
|
||||
if (!preg_match('/^[a-z0-9-]+\/[a-z0-9-]+$/', $ability_name)) {
|
||||
$errors[] = "Invalid ability name format: '{$ability_name}'. Must be 'namespace/ability-name' (lowercase, hyphens allowed)";
|
||||
}
|
||||
|
||||
// Parse the arguments array
|
||||
$args = parse_ability_args($args_string);
|
||||
|
||||
// Validate required fields
|
||||
validate_required_fields($ability_name, $args, $errors, $warnings);
|
||||
|
||||
// Validate schemas
|
||||
if (isset($args['input_schema'])) {
|
||||
validate_json_schema($ability_name, 'input_schema', $args['input_schema'], $errors);
|
||||
}
|
||||
if (isset($args['output_schema'])) {
|
||||
validate_json_schema($ability_name, 'output_schema', $args['output_schema'], $errors);
|
||||
}
|
||||
|
||||
// Best practice checks
|
||||
check_best_practices($ability_name, $args, $warnings);
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Output final results
|
||||
output_results($file_path, $errors, $warnings);
|
||||
|
||||
// Exit with appropriate code
|
||||
exit(empty($errors) ? 0 : 1);
|
||||
|
||||
/**
|
||||
* Parse ability registration arguments from string.
|
||||
*
|
||||
* This is a simplified parser that extracts key configuration values.
|
||||
*
|
||||
* @param string $args_string The arguments array as a string
|
||||
* @return array Parsed arguments
|
||||
*/
|
||||
function parse_ability_args($args_string) {
|
||||
$args = [];
|
||||
|
||||
// Extract simple string/boolean values
|
||||
$simple_patterns = [
|
||||
'label' => '/[\'"]label[\'"]\s*=>\s*(?:__\s*\(\s*)?[\'"]([^\'"]+)[\'"]/',
|
||||
'description' => '/[\'"]description[\'"]\s*=>\s*(?:__\s*\(\s*)?[\'"]([^\'"]+)[\'"]/',
|
||||
'category' => '/[\'"]category[\'"]\s*=>\s*[\'"]([^\'"]+)[\'"]/',
|
||||
'execute_callback' => '/[\'"]execute_callback[\'"]\s*=>\s*[\'"]([^\'"]+)[\'"]/',
|
||||
];
|
||||
|
||||
foreach ($simple_patterns as $key => $pattern) {
|
||||
if (preg_match($pattern, $args_string, $matches)) {
|
||||
$args[$key] = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for permission_callback (function or string)
|
||||
if (preg_match('/[\'"]permission_callback[\'"]\s*=>\s*(.+?),?\s*(?=[\'"][a-z_]+[\'"]|$)/s', $args_string, $matches)) {
|
||||
$args['permission_callback'] = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Extract schema arrays (look for 'input_schema' and 'output_schema')
|
||||
if (preg_match('/[\'"]input_schema[\'"]\s*=>\s*array\s*\((.*?)\),?\s*(?=[\'"][a-z_]+[\'"]|$)/s', $args_string, $matches)) {
|
||||
$args['input_schema'] = $matches[1];
|
||||
}
|
||||
if (preg_match('/[\'"]output_schema[\'"]\s*=>\s*array\s*\((.*?)\),?\s*(?=[\'"][a-z_]+[\'"]|$)/s', $args_string, $matches)) {
|
||||
$args['output_schema'] = $matches[1];
|
||||
}
|
||||
|
||||
// Extract meta array
|
||||
if (preg_match('/[\'"]meta[\'"]\s*=>\s*array\s*\((.*?)\),?\s*(?:\)|$)/s', $args_string, $matches)) {
|
||||
$args['meta'] = $matches[1];
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required fields are present.
|
||||
*
|
||||
* @param string $ability_name Ability name
|
||||
* @param array $args Parsed arguments
|
||||
* @param array &$errors Error messages array
|
||||
* @param array &$warnings Warning messages array
|
||||
*/
|
||||
function validate_required_fields($ability_name, $args, &$errors, &$warnings) {
|
||||
$required_fields = ['label', 'description', 'category', 'execute_callback'];
|
||||
|
||||
foreach ($required_fields as $field) {
|
||||
if (!isset($args[$field])) {
|
||||
$errors[] = "Missing required field '{$field}' in ability '{$ability_name}'";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for empty descriptions (common mistake)
|
||||
if (isset($args['description']) && strlen(trim($args['description'])) < 10) {
|
||||
$warnings[] = "Description for '{$ability_name}' is too short. Provide detailed information for AI agents.";
|
||||
}
|
||||
|
||||
// Check for placeholder/TODO descriptions
|
||||
if (isset($args['description']) && stripos($args['description'], 'TODO') !== false) {
|
||||
$warnings[] = "Description for '{$ability_name}' contains TODO placeholder. Replace with actual description.";
|
||||
}
|
||||
|
||||
// Warn about missing schemas (recommended if ability takes input or provides output)
|
||||
if (!isset($args['input_schema'])) {
|
||||
$warnings[] = "Missing 'input_schema' in ability '{$ability_name}'. Input schema should be provided if this ability accepts parameters.";
|
||||
}
|
||||
|
||||
if (!isset($args['output_schema'])) {
|
||||
$warnings[] = "Missing 'output_schema' in ability '{$ability_name}'. Output schema should be provided if this ability returns data.";
|
||||
}
|
||||
|
||||
// Warn about missing permission callback
|
||||
if (!isset($args['permission_callback'])) {
|
||||
$warnings[] = "Missing 'permission_callback' in ability '{$ability_name}'. Consider adding permission checks to control who can execute this ability.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a JSON Schema structure.
|
||||
*
|
||||
* @param string $ability_name Ability name
|
||||
* @param string $schema_type Schema type ('input_schema' or 'output_schema')
|
||||
* @param string $schema_string Schema as string
|
||||
* @param array &$errors Error messages array
|
||||
*/
|
||||
function validate_json_schema($ability_name, $schema_type, $schema_string, &$errors) {
|
||||
// Check for 'type' field (required in JSON Schema)
|
||||
if (strpos($schema_string, "'type'") === false && strpos($schema_string, '"type"') === false) {
|
||||
$errors[] = "{$schema_type} for '{$ability_name}' missing 'type' field (required by JSON Schema)";
|
||||
}
|
||||
|
||||
// Check for 'properties' if type is 'object'
|
||||
if (preg_match('/[\'"]type[\'"]\s*=>\s*[\'"]object[\'"]/', $schema_string)) {
|
||||
if (strpos($schema_string, "'properties'") === false && strpos($schema_string, '"properties"') === false) {
|
||||
$errors[] = "{$schema_type} for '{$ability_name}' has type 'object' but missing 'properties' field";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for 'items' if type is 'array'
|
||||
if (preg_match('/[\'"]type[\'"]\s*=>\s*[\'"]array[\'"]/', $schema_string)) {
|
||||
if (strpos($schema_string, "'items'") === false && strpos($schema_string, '"items"') === false) {
|
||||
$warnings[] = "{$schema_type} for '{$ability_name}' has type 'array' but missing 'items' field (recommended)";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that properties have types
|
||||
if (preg_match_all('/[\'"]properties[\'"]\s*=>\s*array\s*\((.*?)\)/s', $schema_string, $prop_matches)) {
|
||||
foreach ($prop_matches[1] as $properties) {
|
||||
// Extract each property definition
|
||||
if (preg_match_all('/[\'"]([a-z_]+)[\'"]\s*=>\s*array\s*\((.*?)\)/s', $properties, $prop_defs, PREG_SET_ORDER)) {
|
||||
foreach ($prop_defs as $prop_def) {
|
||||
$prop_name = $prop_def[1];
|
||||
$prop_content = $prop_def[2];
|
||||
|
||||
// Each property should have a 'type'
|
||||
if (strpos($prop_content, "'type'") === false && strpos($prop_content, '"type"') === false) {
|
||||
$errors[] = "{$schema_type} property '{$prop_name}' in '{$ability_name}' missing 'type' field";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for best practices.
|
||||
*
|
||||
* @param string $ability_name Ability name
|
||||
* @param array $args Parsed arguments
|
||||
* @param array &$warnings Warning messages array
|
||||
*/
|
||||
function check_best_practices($ability_name, $args, &$warnings) {
|
||||
// Check for __return_true permission callback (context-aware security check)
|
||||
if (isset($args['permission_callback']) && strpos($args['permission_callback'], '__return_true') !== false) {
|
||||
// Extract annotations to determine if this is a safe public ability
|
||||
$is_readonly = false;
|
||||
$is_destructive = true;
|
||||
|
||||
if (isset($args['meta']) && strpos($args['meta'], 'annotations') !== false) {
|
||||
$is_readonly = preg_match('/[\'"]readonly[\'"]\s*=>\s*true/', $args['meta']);
|
||||
$is_destructive = !preg_match('/[\'"]destructive[\'"]\s*=>\s*false/', $args['meta']);
|
||||
}
|
||||
|
||||
// Only warn if this is potentially dangerous (not readonly OR is destructive)
|
||||
if (!$is_readonly || $is_destructive) {
|
||||
$warnings[] = "Ability '{$ability_name}' uses __return_true() for permissions with write/destructive operations. Ensure public access is intentional.";
|
||||
}
|
||||
// If it's readonly and non-destructive, __return_true is fine - no warning needed
|
||||
}
|
||||
|
||||
// Check for annotations in meta
|
||||
if (isset($args['meta'])) {
|
||||
$has_annotations = strpos($args['meta'], 'annotations') !== false;
|
||||
if (!$has_annotations) {
|
||||
$warnings[] = "Ability '{$ability_name}' missing annotations (readonly, destructive, idempotent). These help AI agents understand the ability's behavior.";
|
||||
}
|
||||
} else {
|
||||
$warnings[] = "Ability '{$ability_name}' missing 'meta' array. Consider adding annotations for better discoverability.";
|
||||
}
|
||||
|
||||
// Check category naming (should be kebab-case)
|
||||
if (isset($args['category']) && !preg_match('/^[a-z0-9-]+$/', $args['category'])) {
|
||||
$warnings[] = "Category '{$args['category']}' should use kebab-case naming (lowercase with hyphens)";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output validation results.
|
||||
*
|
||||
* @param string $file_path File being validated
|
||||
* @param array $errors Error messages
|
||||
* @param array $warnings Warning messages
|
||||
*/
|
||||
function output_results($file_path, $errors, $warnings) {
|
||||
echo str_repeat('=', 70) . "\n";
|
||||
echo "Validation Results: {$file_path}\n";
|
||||
echo str_repeat('=', 70) . "\n\n";
|
||||
|
||||
if (!empty($errors)) {
|
||||
echo "ERRORS (" . count($errors) . "):\n";
|
||||
foreach ($errors as $error) {
|
||||
echo " ✗ {$error}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
if (!empty($warnings)) {
|
||||
echo "WARNINGS (" . count($warnings) . "):\n";
|
||||
foreach ($warnings as $warning) {
|
||||
echo " ⚠ {$warning}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
if (empty($errors) && empty($warnings)) {
|
||||
echo "✓ All validations passed! No issues found.\n\n";
|
||||
} elseif (empty($errors)) {
|
||||
echo "✓ Validation passed with warnings.\n\n";
|
||||
} else {
|
||||
echo "✗ Validation failed. Please fix the errors above.\n\n";
|
||||
}
|
||||
}
|
||||
317
skills/wordpress-ability-api/scripts/validate-category.js
Executable file
317
skills/wordpress-ability-api/scripts/validate-category.js
Executable file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* WordPress Ability Category Validation Script (JavaScript)
|
||||
*
|
||||
* Validates JavaScript category registration code using acorn AST parser.
|
||||
* For PHP validation, use validate-category.php instead.
|
||||
*
|
||||
* Usage:
|
||||
* node validate-category.js path/to/category-file.js
|
||||
*
|
||||
* Requirements:
|
||||
* - Node.js
|
||||
* - acorn parser: npm install acorn
|
||||
*
|
||||
* Arguments:
|
||||
* file_path Path to the JavaScript file containing category registration
|
||||
*
|
||||
* Exit Codes:
|
||||
* 0 - Validation passed
|
||||
* 1 - Validation failed
|
||||
* 2 - File not found, invalid usage, or missing dependencies
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Check for acorn dependency
|
||||
let acorn;
|
||||
try {
|
||||
acorn = require('acorn');
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Error: acorn parser not found. Install with: npm install acorn'
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Check for file argument
|
||||
if (process.argv.length < 3) {
|
||||
console.error('Usage: node validate-category.js path/to/category-file.js');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const filePath = process.argv[2];
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`Error: File not found: ${filePath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Check if file is JavaScript
|
||||
const ext = path.extname(filePath);
|
||||
if (!['.js', '.jsx', '.ts', '.tsx', '.mjs'].includes(ext)) {
|
||||
console.error(
|
||||
'Error: This validator only supports JavaScript files. For PHP validation, use validate-category.php'
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, 'utf8');
|
||||
} catch (e) {
|
||||
console.error(`Error: Unable to read file: ${filePath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Initialize validation results
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
// Parse and validate JavaScript categories
|
||||
try {
|
||||
const ast = acorn.parse(content, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
allowAwaitOutsideFunction: true,
|
||||
});
|
||||
|
||||
const categories = extractCategories(ast);
|
||||
|
||||
if (categories.length === 0) {
|
||||
errors.push('No registerAbilityCategory() calls found in file');
|
||||
} else {
|
||||
categories.forEach((category) => {
|
||||
console.log(`Validating category: ${category.name}`);
|
||||
validateCategory(category, errors, warnings);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
errors.push(`JavaScript syntax error: ${e.message}`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Output final results
|
||||
outputResults(filePath, errors, warnings);
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(errors.length > 0 ? 1 : 0);
|
||||
|
||||
/**
|
||||
* Extract category registrations from AST.
|
||||
*
|
||||
* @param {Object} ast - Acorn AST
|
||||
* @returns {Array} Array of category objects
|
||||
*/
|
||||
function extractCategories(ast) {
|
||||
const categories = [];
|
||||
const processed = new Set(); // Track processed nodes to avoid duplicates
|
||||
|
||||
// Walk the AST to find registerAbilityCategory calls
|
||||
walk(ast, (node) => {
|
||||
let callNode = null;
|
||||
|
||||
// Direct call: registerAbilityCategory()
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'registerAbilityCategory' &&
|
||||
!processed.has(node)
|
||||
) {
|
||||
callNode = node;
|
||||
}
|
||||
|
||||
// Await call: await registerAbilityCategory()
|
||||
if (
|
||||
node.type === 'AwaitExpression' &&
|
||||
node.argument.type === 'CallExpression' &&
|
||||
node.argument.callee.type === 'Identifier' &&
|
||||
node.argument.callee.name === 'registerAbilityCategory' &&
|
||||
!processed.has(node.argument)
|
||||
) {
|
||||
callNode = node.argument;
|
||||
}
|
||||
|
||||
if (callNode) {
|
||||
processed.add(callNode);
|
||||
const category = extractCategoryFromCall(callNode);
|
||||
if (category) {
|
||||
categories.push(category);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract category data from a CallExpression node.
|
||||
*
|
||||
* @param {Object} node - CallExpression AST node
|
||||
* @returns {Object|null} Category object or null
|
||||
*/
|
||||
function extractCategoryFromCall(node) {
|
||||
if (node.arguments.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const category = {
|
||||
name: null,
|
||||
label: null,
|
||||
description: null,
|
||||
};
|
||||
|
||||
// First argument: category name (string literal)
|
||||
const nameArg = node.arguments[0];
|
||||
if (nameArg.type === 'Literal' && typeof nameArg.value === 'string') {
|
||||
category.name = nameArg.value;
|
||||
} else {
|
||||
return null; // Name must be a string literal
|
||||
}
|
||||
|
||||
// Second argument: config object
|
||||
const configArg = node.arguments[1];
|
||||
if (configArg.type === 'ObjectExpression') {
|
||||
configArg.properties.forEach((prop) => {
|
||||
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
||||
const key = prop.key.name;
|
||||
if (
|
||||
(key === 'label' || key === 'description') &&
|
||||
prop.value.type === 'Literal' &&
|
||||
typeof prop.value.value === 'string'
|
||||
) {
|
||||
category[key] = prop.value.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate category data.
|
||||
*
|
||||
* @param {Object} category - Category data
|
||||
* @param {Array} errors - Error messages array
|
||||
* @param {Array} warnings - Warning messages array
|
||||
*/
|
||||
function validateCategory(category, errors, warnings) {
|
||||
const { name, label, description } = category;
|
||||
|
||||
// Validate category name format (kebab-case)
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
errors.push(
|
||||
`Invalid category name format: '${name}'. Must be kebab-case (lowercase, hyphens allowed)`
|
||||
);
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (!label) {
|
||||
errors.push(`Missing required field 'label' in category '${name}'`);
|
||||
} else {
|
||||
// Validate label quality
|
||||
if (label.trim().length < 2) {
|
||||
warnings.push(
|
||||
`Label for category '${name}' is too short. Provide a meaningful label.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
errors.push(
|
||||
`Missing required field 'description' in category '${name}'`
|
||||
);
|
||||
} else {
|
||||
// Validate description quality
|
||||
const descLength = description.trim().length;
|
||||
|
||||
if (descLength < 15) {
|
||||
warnings.push(
|
||||
`Description for category '${name}' is too short (${descLength} chars). Provide detailed information about what abilities belong in this category.`
|
||||
);
|
||||
}
|
||||
|
||||
if (/TODO/i.test(description)) {
|
||||
warnings.push(
|
||||
`Description for category '${name}' contains TODO placeholder. Replace with actual description.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple AST walker.
|
||||
*
|
||||
* @param {Object} node - AST node
|
||||
* @param {Function} callback - Function to call for each node
|
||||
*/
|
||||
function walk(node, callback) {
|
||||
if (!node || typeof node !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(node);
|
||||
|
||||
for (const key in node) {
|
||||
if (
|
||||
key === 'loc' ||
|
||||
key === 'range' ||
|
||||
key === 'start' ||
|
||||
key === 'end'
|
||||
) {
|
||||
continue; // Skip position metadata
|
||||
}
|
||||
|
||||
const value = node[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((child) => walk(child, callback));
|
||||
} else if (value && typeof value === 'object') {
|
||||
walk(value, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output validation results.
|
||||
*
|
||||
* @param {string} filePath - File being validated
|
||||
* @param {Array} errors - Error messages
|
||||
* @param {Array} warnings - Warning messages
|
||||
*/
|
||||
function outputResults(filePath, errors, warnings) {
|
||||
console.log('='.repeat(70));
|
||||
console.log(`Validation Results: ${filePath}`);
|
||||
console.log('='.repeat(70));
|
||||
console.log('');
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(`ERRORS (${errors.length}):`);
|
||||
errors.forEach((error) => console.log(` ✗ ${error}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log(`WARNINGS (${warnings.length}):`);
|
||||
warnings.forEach((warning) => console.log(` ⚠ ${warning}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
console.log('✓ All validations passed! No issues found.');
|
||||
console.log('');
|
||||
} else if (errors.length === 0) {
|
||||
console.log('✓ Validation passed with warnings.');
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('✗ Validation failed. Please fix the errors above.');
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
325
skills/wordpress-ability-api/scripts/validate-category.php
Executable file
325
skills/wordpress-ability-api/scripts/validate-category.php
Executable file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* WordPress Ability Category Validation Script (PHP)
|
||||
*
|
||||
* Validates PHP category registration code using PHP's built-in tokenizer.
|
||||
* For JavaScript validation, use validate-category.js instead.
|
||||
*
|
||||
* Usage:
|
||||
* php validate-category.php path/to/category-file.php
|
||||
*
|
||||
* Arguments:
|
||||
* file_path Path to the PHP file containing category registration code
|
||||
*
|
||||
* Exit Codes:
|
||||
* 0 - Validation passed
|
||||
* 1 - Validation failed
|
||||
* 2 - File not found or invalid usage
|
||||
*/
|
||||
|
||||
// Check for file argument
|
||||
if ($argc < 2) {
|
||||
fwrite(STDERR, "Usage: php validate-category.php path/to/category-file.php\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$file_path = $argv[1];
|
||||
|
||||
// Check if file exists
|
||||
if (!file_exists($file_path)) {
|
||||
fwrite(STDERR, "Error: File not found: {$file_path}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Check if file is PHP
|
||||
$file_extension = pathinfo($file_path, PATHINFO_EXTENSION);
|
||||
if ($file_extension !== 'php') {
|
||||
fwrite(STDERR, "Error: This validator only supports PHP files. For JavaScript validation, use validate-category.js\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
$content = file_get_contents($file_path);
|
||||
if ($content === false) {
|
||||
fwrite(STDERR, "Error: Unable to read file: {$file_path}\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Initialize validation results
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
// Parse and validate PHP categories
|
||||
$categories = parse_php_categories($content);
|
||||
|
||||
if (empty($categories)) {
|
||||
$errors[] = "No wp_register_ability_category() calls found in file";
|
||||
} else {
|
||||
foreach ($categories as $category) {
|
||||
echo "Validating category: {$category['name']}\n";
|
||||
validate_category($category, $errors, $warnings);
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Output final results
|
||||
output_results($file_path, $errors, $warnings);
|
||||
|
||||
// Exit with appropriate code
|
||||
exit(empty($errors) ? 0 : 1);
|
||||
|
||||
/**
|
||||
* Parse PHP file to extract category registrations using token_get_all().
|
||||
*
|
||||
* @param string $content PHP file content
|
||||
* @return array Array of category data
|
||||
*/
|
||||
function parse_php_categories($content) {
|
||||
$tokens = token_get_all($content);
|
||||
$categories = [];
|
||||
$i = 0;
|
||||
$count = count($tokens);
|
||||
|
||||
while ($i < $count) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
// Look for function call: wp_register_ability_category
|
||||
if (is_array($token) && $token[0] === T_STRING && $token[1] === 'wp_register_ability_category') {
|
||||
// Found the function call, now extract arguments
|
||||
$category = extract_category_from_tokens($tokens, $i);
|
||||
if ($category) {
|
||||
$categories[] = $category;
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract category data from tokens starting at function call position.
|
||||
*
|
||||
* @param array $tokens All tokens
|
||||
* @param int $start_pos Position of function name token
|
||||
* @return array|null Category data or null if parsing fails
|
||||
*/
|
||||
function extract_category_from_tokens($tokens, $start_pos) {
|
||||
$i = $start_pos + 1;
|
||||
$count = count($tokens);
|
||||
$category = ['name' => null, 'label' => null, 'description' => null];
|
||||
|
||||
// Skip whitespace to opening parenthesis
|
||||
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Should be at opening parenthesis
|
||||
if ($i >= $count || $tokens[$i] !== '(') {
|
||||
return null;
|
||||
}
|
||||
$i++;
|
||||
|
||||
// Skip whitespace to category name
|
||||
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Extract category name (first argument - string)
|
||||
if ($i < $count && is_array($tokens[$i]) && ($tokens[$i][0] === T_CONSTANT_ENCAPSED_STRING)) {
|
||||
$category['name'] = trim($tokens[$i][1], '\'"');
|
||||
$i++;
|
||||
} else {
|
||||
return null; // No category name found
|
||||
}
|
||||
|
||||
// Now look for the array with label and description
|
||||
// Skip to 'array' keyword or '['
|
||||
while ($i < $count) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (is_array($token) && $token[0] === T_ARRAY) {
|
||||
// Found 'array(' syntax
|
||||
$i++;
|
||||
// Skip to opening parenthesis
|
||||
while ($i < $count && $tokens[$i] !== '(') {
|
||||
$i++;
|
||||
}
|
||||
if ($i < $count) {
|
||||
$i++; // Move past '('
|
||||
$extracted = extract_array_contents($tokens, $i);
|
||||
$category = array_merge($category, $extracted);
|
||||
}
|
||||
break;
|
||||
} elseif ($token === '[') {
|
||||
// Found '[' short array syntax
|
||||
$i++;
|
||||
$extracted = extract_array_contents($tokens, $i);
|
||||
$category = array_merge($category, $extracted);
|
||||
break;
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract label and description from array tokens.
|
||||
*
|
||||
* @param array $tokens All tokens
|
||||
* @param int $start_pos Position after array opening
|
||||
* @return array Array with label and description keys
|
||||
*/
|
||||
function extract_array_contents($tokens, $start_pos) {
|
||||
$result = ['label' => null, 'description' => null];
|
||||
$i = $start_pos;
|
||||
$count = count($tokens);
|
||||
$depth = 1; // Track nested arrays/parentheses
|
||||
|
||||
while ($i < $count && $depth > 0) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
// Track depth for nested structures
|
||||
if ($token === '(' || $token === '[') {
|
||||
$depth++;
|
||||
} elseif ($token === ')' || $token === ']') {
|
||||
$depth--;
|
||||
if ($depth === 0) break;
|
||||
}
|
||||
|
||||
// Look for 'label' or 'description' keys
|
||||
if (is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING) {
|
||||
$key = trim($token[1], '\'"');
|
||||
|
||||
if ($key === 'label' || $key === 'description') {
|
||||
// Skip to the value (past '=>' and whitespace)
|
||||
$i++;
|
||||
// Skip whitespace
|
||||
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
|
||||
$i++;
|
||||
}
|
||||
// Skip '=>' (T_DOUBLE_ARROW)
|
||||
if ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_DOUBLE_ARROW) {
|
||||
$i++;
|
||||
}
|
||||
// Skip more whitespace
|
||||
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Check for __() translation function
|
||||
if ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_STRING && $tokens[$i][1] === '__') {
|
||||
// Skip to opening parenthesis of __()
|
||||
while ($i < $count && $tokens[$i] !== '(') {
|
||||
$i++;
|
||||
}
|
||||
if ($i < $count) {
|
||||
$depth++; // Track the opening paren
|
||||
$i++; // Move past '('
|
||||
}
|
||||
|
||||
// Skip whitespace
|
||||
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Extract string value
|
||||
if ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_CONSTANT_ENCAPSED_STRING) {
|
||||
$result[$key] = trim($tokens[$i][1], '\'"');
|
||||
}
|
||||
} elseif ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_CONSTANT_ENCAPSED_STRING) {
|
||||
// Direct string value
|
||||
$result[$key] = trim($tokens[$i][1], '\'"');
|
||||
}
|
||||
// Continue to next iteration without incrementing again
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate category data.
|
||||
*
|
||||
* @param array $category Category data
|
||||
* @param array &$errors Error messages array
|
||||
* @param array &$warnings Warning messages array
|
||||
*/
|
||||
function validate_category($category, &$errors, &$warnings) {
|
||||
$name = $category['name'];
|
||||
|
||||
// Validate category name format (kebab-case)
|
||||
if (!preg_match('/^[a-z0-9-]+$/', $name)) {
|
||||
$errors[] = "Invalid category name format: '{$name}'. Must be kebab-case (lowercase, hyphens allowed)";
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (empty($category['label'])) {
|
||||
$errors[] = "Missing required field 'label' in category '{$name}'";
|
||||
} else {
|
||||
// Validate label quality
|
||||
if (strlen(trim($category['label'])) < 2) {
|
||||
$warnings[] = "Label for category '{$name}' is too short. Provide a meaningful label.";
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($category['description'])) {
|
||||
$errors[] = "Missing required field 'description' in category '{$name}'";
|
||||
} else {
|
||||
// Validate description quality
|
||||
$desc_length = strlen(trim($category['description']));
|
||||
|
||||
if ($desc_length < 15) {
|
||||
$warnings[] = "Description for category '{$name}' is too short ({$desc_length} chars). Provide detailed information about what abilities belong in this category.";
|
||||
}
|
||||
|
||||
if (stripos($category['description'], 'TODO') !== false) {
|
||||
$warnings[] = "Description for category '{$name}' contains TODO placeholder. Replace with actual description.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output validation results.
|
||||
*
|
||||
* @param string $file_path File being validated
|
||||
* @param array $errors Error messages
|
||||
* @param array $warnings Warning messages
|
||||
*/
|
||||
function output_results($file_path, $errors, $warnings) {
|
||||
echo str_repeat('=', 70) . "\n";
|
||||
echo "Validation Results: {$file_path}\n";
|
||||
echo str_repeat('=', 70) . "\n\n";
|
||||
|
||||
if (!empty($errors)) {
|
||||
echo "ERRORS (" . count($errors) . "):\n";
|
||||
foreach ($errors as $error) {
|
||||
echo " ✗ {$error}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
if (!empty($warnings)) {
|
||||
echo "WARNINGS (" . count($warnings) . "):\n";
|
||||
foreach ($warnings as $warning) {
|
||||
echo " ⚠ {$warning}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
if (empty($errors) && empty($warnings)) {
|
||||
echo "✓ All validations passed! No issues found.\n\n";
|
||||
} elseif (empty($errors)) {
|
||||
echo "✓ Validation passed with warnings.\n\n";
|
||||
} else {
|
||||
echo "✗ Validation failed. Please fix the errors above.\n\n";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user