Initial commit
This commit is contained in:
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "wordpress-ability-api",
|
||||||
|
"description": "Create, validate, and register WordPress Abilities (server-side PHP and client-side JavaScript) with comprehensive scaffolding and validation tools",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Em"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/wordpress-ability-api"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# wordpress-ability-api
|
||||||
|
|
||||||
|
Create, validate, and register WordPress Abilities (server-side PHP and client-side JavaScript) with comprehensive scaffolding and validation tools
|
||||||
125
plugin.lock.json
Normal file
125
plugin.lock.json
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:emdashcodes/wp-ability-toolkit:claude-code-plugins/wordpress-ability-api",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "b6a7bad8a96eeb26e08f742f47ee84eb151d01fc",
|
||||||
|
"treeHash": "c0405c887de8c97ee27d08cabba4e39e3679071c186716d49a1460d884166703",
|
||||||
|
"generatedAt": "2025-11-28T10:16:46.771418Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "wordpress-ability-api",
|
||||||
|
"description": "Create, validate, and register WordPress Abilities (server-side PHP and client-side JavaScript) with comprehensive scaffolding and validation tools",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "a0d74add41d6ac9cd3a7c670bf8509ad0d6e74a5f9df6a3e084b8fee74b9e901"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "ba68aeb34960442e79b1a37da1cc5143f103f540d1b4708b57caaa3d40b13f90"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/SKILL.md",
|
||||||
|
"sha256": "89718dd4ed8ab3f5fcc4bb66afa81aaa5351ba0e833641845d66144de6c9597d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/6.hooks.md",
|
||||||
|
"sha256": "fb730c49c7e926df9025f3d71b3eece5d30391078f1c56fcb29bb081097436f0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/4.using-abilities.md",
|
||||||
|
"sha256": "1cab3c53221039caeb3861d6f4827ee55c0876023c1049c2b7ccd5d86a6835f0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/2.getting-started.md",
|
||||||
|
"sha256": "23a78d5098968235ab4511a0e9ed5197a99165f36e7223bcf1aa3fa2c6176bc7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/7.javascript-client.md",
|
||||||
|
"sha256": "291de8e0bc873872ba840d2c01c68825fd82added0f07868a79c954bed0aefb7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/1.intro.md",
|
||||||
|
"sha256": "4da0f6d3b3d4c9367153483e6a7acdb12f34e61d4c24000c84ab5602c7e18b52"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/5.rest-api.md",
|
||||||
|
"sha256": "d4964942d7b3ff3a26df4cd616de6edbbf421a68ed46d9d3f79a21438428eb9e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/7.registering-categories.md",
|
||||||
|
"sha256": "11e5c502402820c7d5a5aef50d06a1fc87e12ca39c253bf0fae3244a8c3a8b0f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/references/3.registering-abilities.md",
|
||||||
|
"sha256": "2bcc92eb327295590f560ea7dbe9593f9e1cd77a8fd8510b816330a0a05b3462"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/scaffold-category.php",
|
||||||
|
"sha256": "153d07efe45458facf363a155d6e125cbccbec8c5868128d327037a528a3db4a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/README.md",
|
||||||
|
"sha256": "05c0657af47d3f682c424f073eabc7f404284aa66f87dd527ab4df3c56313fc1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/package.json",
|
||||||
|
"sha256": "7d9f14eef9866d834e9248865dab67f3fd4ebd3ac1b26e71a2da31d90e159244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/validate-category.php",
|
||||||
|
"sha256": "a1a7a6e481bb9ba98578bd83b74e6d49badc7e795587fa6b114811450f250f17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/validate-ability.php",
|
||||||
|
"sha256": "994c5621f20794c7fdff9c7b5a242769717e393a94e78ee82abb9ab8b368a279"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/scaffold-ability.php",
|
||||||
|
"sha256": "6ee7467f426b75b0112b32dc6897e1fd36d435a531d0c36dc0bdb66526d60a35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/validate-ability.js",
|
||||||
|
"sha256": "d6c3d8253d9ea4737b1d68777de25de158acf6b65f82fc4897e25bc2e180ca80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/scripts/validate-category.js",
|
||||||
|
"sha256": "5058d00da5e7a46092ba078f0a49da73054f00cfa93a5aaa9377159a5e377e9e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/assets/client-category-template.js",
|
||||||
|
"sha256": "fc99e82c3da6bfbfd765818feb3c39f2596ab458e70b9e144994e5d9617d7d92"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/assets/server-ability-template.php",
|
||||||
|
"sha256": "941c375168336ba9d49fb8584adfa2935f4dea675354d623d3bb35c4b2b482f1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/assets/category-template.php",
|
||||||
|
"sha256": "900791794e3237b0cba428da193e3164786a2a0504e094efb56e52f158c8024b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/wordpress-ability-api/assets/client-ability-template.js",
|
||||||
|
"sha256": "100995b83daa5d723c483d63b8e4469b8a2c14bc6a12bbdf93ec0b41b63bb327"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "c0405c887de8c97ee27d08cabba4e39e3679071c186716d49a1460d884166703"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
485
skills/wordpress-ability-api/SKILL.md
Normal file
485
skills/wordpress-ability-api/SKILL.md
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
---
|
||||||
|
name: wordpress-ability-api
|
||||||
|
description: This skill should be used when helping users create, edit, or register WordPress Abilities (both server-side PHP and client-side JavaScript), register ability categories, or set up the WordPress Abilities API as a dependency. Use when users say things like "create an ability", "help me build an ability", "set up the Ability API", or "register a category".
|
||||||
|
---
|
||||||
|
|
||||||
|
# WordPress Ability API
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill helps users work with the WordPress Abilities API - a standardized system for registering and discovering distinct units of functionality within WordPress. Use this skill to help users create server-side PHP abilities, client-side JavaScript abilities, register categories, and set up the API as a dependency.
|
||||||
|
|
||||||
|
The Abilities API makes WordPress functionality discoverable by AI agents and automation tools through machine-readable schemas, permissions, and validation.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Activate this skill when the user requests:
|
||||||
|
|
||||||
|
- **Creating abilities**: "Help me create an ability to...", "I want to make an ability that...", "Build me an ability for..."
|
||||||
|
- **Editing abilities**: "Update my ability", "Fix this ability code", "Modify the ability to..."
|
||||||
|
- **Setting up the API**: "Set up the Ability API", "Add the Ability API as a dependency", "Install the Abilities API"
|
||||||
|
- **Registering categories**: "Create a category for my abilities", "Register an ability category"
|
||||||
|
|
||||||
|
## Workflow Decision Tree
|
||||||
|
|
||||||
|
When this skill activates, determine what the user needs:
|
||||||
|
|
||||||
|
1. **Are they creating/editing an ability?**
|
||||||
|
→ Follow the "Creating Abilities" workflow below
|
||||||
|
|
||||||
|
2. **Do they need to set up the API first?**
|
||||||
|
→ Follow the "Setting Up the API" workflow below
|
||||||
|
|
||||||
|
3. **Are they just learning about the API?**
|
||||||
|
→ Reference the documentation in `references/` and explain concepts
|
||||||
|
|
||||||
|
## Using the Scaffold Script
|
||||||
|
|
||||||
|
The skill includes a `scripts/scaffold-ability.php` script designed for programmatic ability generation. Use this when creating abilities to quickly generate validated boilerplate code.
|
||||||
|
|
||||||
|
### Script Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php scripts/scaffold-ability.php \
|
||||||
|
--name="plugin/ability-name" \
|
||||||
|
--type="server" \
|
||||||
|
--category="data-retrieval" \
|
||||||
|
--label="Optional Label" \
|
||||||
|
--description="Optional description"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required Arguments:**
|
||||||
|
|
||||||
|
- `--name` - Ability name in format "namespace/ability-name"
|
||||||
|
- `--type` - Either "server" (PHP) or "client" (JavaScript)
|
||||||
|
- `--category` - Category slug (e.g., "data-retrieval", "data-modification")
|
||||||
|
|
||||||
|
**Optional Arguments:**
|
||||||
|
|
||||||
|
- `--label` - Human-readable label (auto-generated from name if omitted)
|
||||||
|
- `--description` - Detailed description (placeholder if omitted)
|
||||||
|
- `--readonly` - "true" or "false" (default: false)
|
||||||
|
- `--destructive` - "true" or "false" (default: true)
|
||||||
|
- `--idempotent` - "true" or "false" (default: false)
|
||||||
|
|
||||||
|
**Output:** Complete ability code printed to stdout, ready to be written to plugin files.
|
||||||
|
|
||||||
|
### When to Use the Scaffold Script
|
||||||
|
|
||||||
|
Use the scaffold script after gathering requirements from the user (Steps 1-3 of the workflow below). The script generates clean, validated boilerplate that follows best practices.
|
||||||
|
|
||||||
|
**Important:** Always gather required information from the user BEFORE calling the script, or generate sensible defaults based on context.
|
||||||
|
|
||||||
|
## Creating Abilities
|
||||||
|
|
||||||
|
When helping users create or edit abilities, follow this conversational workflow:
|
||||||
|
|
||||||
|
### Step 1: Understand the Functionality
|
||||||
|
|
||||||
|
Ask clarifying questions to understand what the ability should do:
|
||||||
|
|
||||||
|
- **What should this ability do?** (e.g., "Get site analytics", "Send notifications", "Update settings")
|
||||||
|
- **Is this a server-side or client-side ability?**
|
||||||
|
- **Server-side (PHP)**: Runs on the WordPress backend, accesses database, uses WordPress functions
|
||||||
|
- **Client-side (JavaScript)**: Runs in the browser, manipulates DOM, handles UI interactions
|
||||||
|
- **What input parameters does it need?** (e.g., user ID, date range, options)
|
||||||
|
- **What should it return?** (e.g., array of data, success boolean, error messages)
|
||||||
|
- **Who should be able to use it?** (permissions: any user, logged-in users, administrators, custom capability)
|
||||||
|
|
||||||
|
### Step 2: Determine Plugin Location
|
||||||
|
|
||||||
|
Ask where the ability code should live:
|
||||||
|
|
||||||
|
- **"Which plugin should this ability be registered in?"**
|
||||||
|
- If they have an existing plugin, use that
|
||||||
|
- If they need a new plugin, offer to create one using the `wordpress-plugin-scaffold` skill
|
||||||
|
|
||||||
|
### Step 3: Check for Category
|
||||||
|
|
||||||
|
Abilities must belong to a category. Ask:
|
||||||
|
|
||||||
|
- **"Which category should this ability belong to?"**
|
||||||
|
- Example categories: `data-retrieval`, `data-modification`, `communication`, `ecommerce`, `user-management`
|
||||||
|
- If they need a custom category, use `scripts/scaffold-category.php` to generate it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php scripts/scaffold-category.php \
|
||||||
|
--name="custom-category" \
|
||||||
|
--label="Custom Category" \
|
||||||
|
--description="Description of what abilities belong here" \
|
||||||
|
--type="server|client"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Alternatively, manually customize `assets/category-template.php`
|
||||||
|
- See `references/7.registering-categories.md` for detailed category registration information
|
||||||
|
|
||||||
|
### Step 4: Generate the Ability Code
|
||||||
|
|
||||||
|
Use the scaffold script to generate complete, validated code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php scripts/scaffold-ability.php \
|
||||||
|
--name="namespace/ability-name" \
|
||||||
|
--type="server|client" \
|
||||||
|
--category="category-slug" \
|
||||||
|
--label="Human Label" \
|
||||||
|
--description="Detailed description" \
|
||||||
|
--readonly="true|false" \
|
||||||
|
--destructive="true|false" \
|
||||||
|
--idempotent="true|false"
|
||||||
|
```
|
||||||
|
|
||||||
|
**For server-side (PHP):** Outputs complete PHP code with callback and registration functions
|
||||||
|
**For client-side (JavaScript):** Outputs complete JavaScript code with async callback
|
||||||
|
|
||||||
|
Alternatively, manually copy and customize templates from `assets/` directory:
|
||||||
|
|
||||||
|
- `server-ability-template.php` - PHP abilities
|
||||||
|
- `client-ability-template.js` - JavaScript abilities
|
||||||
|
|
||||||
|
Replace all `{{PLACEHOLDERS}}` with actual values. See templates for full list of placeholders.
|
||||||
|
|
||||||
|
### Step 5: Validate the Code
|
||||||
|
|
||||||
|
Before adding the code to plugin files, validate it using the appropriate validation script:
|
||||||
|
|
||||||
|
#### Validate Abilities
|
||||||
|
|
||||||
|
**For PHP abilities:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php scripts/validate-ability.php path/to/ability-file.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**For JavaScript abilities:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/validate-ability.js path/to/ability-file.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Validate Categories
|
||||||
|
|
||||||
|
**For PHP categories:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php scripts/validate-category.php path/to/category-file.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**For JavaScript categories:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/validate-category.js path/to/category-file.js
|
||||||
|
```
|
||||||
|
|
||||||
|
All validators check:
|
||||||
|
|
||||||
|
- **Structure**: Required fields (name, label, description, schemas/callbacks) are present
|
||||||
|
- **JSON Schema validity**: Schemas follow JSON Schema specification (abilities only)
|
||||||
|
- **Best practices**: Proper naming (kebab-case), annotations, permission callbacks
|
||||||
|
- **Common mistakes**: TODO placeholders, empty descriptions, security concerns
|
||||||
|
|
||||||
|
**Exit codes:**
|
||||||
|
|
||||||
|
- `0` - Validation passed
|
||||||
|
- `1` - Validation failed (errors found)
|
||||||
|
- `2` - File not found or invalid usage
|
||||||
|
|
||||||
|
Fix any errors reported by the validator before proceeding.
|
||||||
|
|
||||||
|
### Step 6: Add to Plugin Files
|
||||||
|
|
||||||
|
Write the validated code to the appropriate plugin files:
|
||||||
|
|
||||||
|
- **PHP abilities**: Add to plugin's main PHP file or create a dedicated `includes/abilities.php` file and hook into `abilities_api_init`
|
||||||
|
- **JavaScript abilities**: Add to enqueued JavaScript file with `@wordpress/abilities` dependency
|
||||||
|
- **Categories**: Register on `abilities_api_categories_init` hook before abilities
|
||||||
|
|
||||||
|
Use the Write tool to add the code to the correct location in the plugin.
|
||||||
|
|
||||||
|
### Step 7: Test the Ability
|
||||||
|
|
||||||
|
After code is added to plugin files, suggest testing approaches:
|
||||||
|
|
||||||
|
**Server-side (PHP):**
|
||||||
|
|
||||||
|
- Use `wp_get_ability('namespace/name')` to verify registration
|
||||||
|
- Test execution via REST API: `POST /wp-json/abilities/v1/abilities/{namespace}/{ability-name}/execute`
|
||||||
|
- Or test directly: `$ability->execute( $input_data )`
|
||||||
|
- Verify permissions work as expected
|
||||||
|
- Check input validation catches invalid data
|
||||||
|
|
||||||
|
**Client-side (JavaScript):**
|
||||||
|
|
||||||
|
- Open browser DevTools console
|
||||||
|
- Check registration: `wp.data.select('core/abilities').getAbility('namespace/ability-name')`
|
||||||
|
- Execute ability: `wp.data.dispatch('core/abilities').executeAbility('namespace/ability-name', inputData)`
|
||||||
|
- Check available abilities: `wp.data.select('core/abilities').getAbilities()`
|
||||||
|
- Verify permissions and error handling work correctly
|
||||||
|
|
||||||
|
## Setting Up the API
|
||||||
|
|
||||||
|
The WordPress Abilities API must be available before abilities can be registered. Help users set up the API based on their WordPress version:
|
||||||
|
|
||||||
|
### Check WordPress Version
|
||||||
|
|
||||||
|
**WordPress 6.9+**: The Abilities API is included in core - no setup needed!
|
||||||
|
|
||||||
|
**WordPress < 6.9**: The API must be installed as a plugin or dependency.
|
||||||
|
|
||||||
|
### Installation Options
|
||||||
|
|
||||||
|
Reference `references/2.getting-started.md` for detailed setup instructions. Guide users through the most appropriate method:
|
||||||
|
|
||||||
|
#### Option 1: As a Plugin (Easiest for Testing)
|
||||||
|
|
||||||
|
If they're using **wp-env** (check for `.wp-env.json`), help them add it via the `wp-env` skill:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": ["WordPress/abilities-api"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If they're using **WP-CLI**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp plugin install https://github.com/WordPress/abilities-api/releases/latest/download/abilities-api.zip
|
||||||
|
wp plugin activate abilities-api
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: As a Plugin Dependency (Recommended for Plugins)
|
||||||
|
|
||||||
|
Help them add to their plugin header:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Plugin Name: My Plugin
|
||||||
|
* Requires Plugins: abilities-api
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add availability check:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if ( ! class_exists( 'WP_Ability' ) ) {
|
||||||
|
add_action( 'admin_notices', function() {
|
||||||
|
wp_admin_notice(
|
||||||
|
__( 'This plugin requires the Abilities API. Please install and activate it.', 'my-plugin' ),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: As a Composer Dependency
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require wordpress/abilities-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Installation
|
||||||
|
|
||||||
|
Help users verify the API is loaded:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if ( class_exists( 'WP_Ability' ) ) {
|
||||||
|
// API is available
|
||||||
|
echo 'Abilities API version: ' . WP_ABILITIES_API_VERSION;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Other Skills
|
||||||
|
|
||||||
|
This skill works well with:
|
||||||
|
|
||||||
|
- **wordpress-plugin-scaffold**: Use when users need a new plugin to register abilities in
|
||||||
|
- **wp-env**: Use for local development environment setup when installing the Abilities API
|
||||||
|
|
||||||
|
## Reference Documentation
|
||||||
|
|
||||||
|
The `references/` directory contains complete API documentation:
|
||||||
|
|
||||||
|
- **1.intro.md**: Core concepts, goals, and benefits of the Abilities API
|
||||||
|
- **2.getting-started.md**: Installation methods and basic usage examples
|
||||||
|
- **3.registering-abilities.md**: Comprehensive guide to `wp_register_ability()` with all parameters and examples
|
||||||
|
- **4.using-abilities.md**: How to retrieve and execute abilities in PHP
|
||||||
|
- **5.rest-api.md**: REST API endpoints for abilities
|
||||||
|
- **6.hooks.md**: Available WordPress hooks in the Abilities API
|
||||||
|
- **7.javascript-client.md**: Complete client-side JavaScript API reference
|
||||||
|
- **7.registering-categories.md**: How to register ability categories
|
||||||
|
|
||||||
|
**When to read references:**
|
||||||
|
|
||||||
|
- For specific parameter details → `3.registering-abilities.md`
|
||||||
|
- For setup instructions → `2.getting-started.md`
|
||||||
|
- For client-side abilities → `7.javascript-client.md`
|
||||||
|
- For category registration → `7.registering-categories.md`
|
||||||
|
- For conceptual understanding → `1.intro.md`
|
||||||
|
|
||||||
|
## Scripts and Templates
|
||||||
|
|
||||||
|
### Scripts (`scripts/`)
|
||||||
|
|
||||||
|
The skill includes automation scripts for generating and validating both abilities and categories:
|
||||||
|
|
||||||
|
#### Ability Scripts
|
||||||
|
|
||||||
|
- **scaffold-ability.php**: Programmatically generate ability code from CLI arguments
|
||||||
|
- Accepts: name, type, category, label, description, and annotation flags
|
||||||
|
- Outputs: Complete, validated ability code to stdout
|
||||||
|
- Usage: `php scripts/scaffold-ability.php --name="plugin/ability" --type="server" --category="data-retrieval"`
|
||||||
|
- Use when: Creating new abilities after gathering requirements
|
||||||
|
|
||||||
|
- **validate-ability.php**: Validate PHP ability code independently of WordPress
|
||||||
|
- Checks: Required fields, JSON Schema validity, best practices
|
||||||
|
- Outputs: Detailed validation results with errors and warnings
|
||||||
|
- Usage: `php scripts/validate-ability.php path/to/ability-file.php`
|
||||||
|
- Use when: Validating server-side PHP ability code before registration
|
||||||
|
|
||||||
|
- **validate-ability.js**: Validate JavaScript ability registration code
|
||||||
|
- Requirements: Node.js and acorn parser (`npm install acorn`)
|
||||||
|
- Parser: Uses acorn AST parser for accurate JavaScript parsing
|
||||||
|
- Checks: Required fields (name, category, callback), name format, schemas, permissions, annotations
|
||||||
|
- Outputs: Detailed validation results with errors and warnings
|
||||||
|
- Usage: `node scripts/validate-ability.js path/to/ability-file.js`
|
||||||
|
- Use when: Validating client-side JavaScript ability code before registration
|
||||||
|
|
||||||
|
#### Category Scripts
|
||||||
|
|
||||||
|
- **scaffold-category.php**: Programmatically generate category registration code
|
||||||
|
- Accepts: name, label, description, type (server/client)
|
||||||
|
- Outputs: Complete category registration code to stdout
|
||||||
|
- Usage: `php scripts/scaffold-category.php --name="category-slug" --label="Label" --description="Description" --type="server"`
|
||||||
|
- Use when: Creating custom categories for abilities
|
||||||
|
- Includes helpful comments about registration hooks and timing
|
||||||
|
|
||||||
|
- **validate-category.php**: Validate PHP category registration code
|
||||||
|
- Parser: Uses PHP's `token_get_all()` for accurate parsing
|
||||||
|
- Checks: Name format (kebab-case), required fields, description quality
|
||||||
|
- Outputs: Detailed validation results with errors and warnings
|
||||||
|
- Usage: `php scripts/validate-category.php path/to/category-file.php`
|
||||||
|
- Use when: Validating server-side PHP category code
|
||||||
|
|
||||||
|
- **validate-category.js**: Validate JavaScript category registration code
|
||||||
|
- Requirements: Node.js and acorn parser (`npm install acorn`)
|
||||||
|
- Parser: Uses acorn AST parser for accurate JavaScript parsing
|
||||||
|
- Checks: Same validations as PHP validator
|
||||||
|
- Outputs: Detailed validation results with errors and warnings
|
||||||
|
- Usage: `node scripts/validate-category.js path/to/category-file.js`
|
||||||
|
- Use when: Validating client-side JavaScript category code
|
||||||
|
|
||||||
|
### Template Assets (`assets/`)
|
||||||
|
|
||||||
|
The skill also includes manual starter templates:
|
||||||
|
|
||||||
|
- **server-ability-template.php**: Complete boilerplate for server-side PHP abilities
|
||||||
|
- **client-ability-template.js**: Complete boilerplate for client-side JavaScript abilities
|
||||||
|
- **category-template.php**: Template for server-side PHP category registration
|
||||||
|
- **client-category-template.js**: Template for client-side JavaScript category registration
|
||||||
|
|
||||||
|
**Note:** Templates use `{{PLACEHOLDER}}` syntax and are read by scaffold scripts. You can also manually copy and replace placeholders for custom needs. Scaffold scripts generate code by loading these templates and replacing placeholders with actual values.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
When helping users create abilities:
|
||||||
|
|
||||||
|
1. **Use descriptive names**: `my-plugin/get-user-analytics` not `my-plugin/analytics`
|
||||||
|
2. **Write detailed descriptions**: Help AI agents understand WHEN and HOW to use the ability
|
||||||
|
3. **Define complete schemas**: Use JSON Schema to validate inputs and document outputs
|
||||||
|
4. **Implement proper permissions**: Never use `__return_true` for sensitive operations
|
||||||
|
5. **Use appropriate annotations**:
|
||||||
|
- `readonly: true` for data retrieval abilities
|
||||||
|
- `destructive: false` for abilities that only add/update (never delete)
|
||||||
|
- `idempotent: true` for abilities that can be called repeatedly safely
|
||||||
|
6. **Handle errors gracefully**: Return `WP_Error` objects with clear error messages
|
||||||
|
7. **Test thoroughly**: Verify schema validation, permissions, and error cases
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Read-Only Data Retrieval
|
||||||
|
|
||||||
|
```php
|
||||||
|
'meta' => array(
|
||||||
|
'annotations' => array(
|
||||||
|
'readonly' => true,
|
||||||
|
'destructive' => false,
|
||||||
|
'idempotent' => true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Modification (Non-Destructive)
|
||||||
|
|
||||||
|
```php
|
||||||
|
'meta' => array(
|
||||||
|
'annotations' => array(
|
||||||
|
'readonly' => false,
|
||||||
|
'destructive' => false,
|
||||||
|
'idempotent' => false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Potentially Destructive Operations
|
||||||
|
|
||||||
|
```php
|
||||||
|
'meta' => array(
|
||||||
|
'annotations' => array(
|
||||||
|
'readonly' => false,
|
||||||
|
'destructive' => true,
|
||||||
|
'idempotent' => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'permission_callback' => function() {
|
||||||
|
return current_user_can( 'manage_options' );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Client-Side Category Registration
|
||||||
|
|
||||||
|
When registering client-side abilities with custom categories, use `registerAbilityCategory()` before registering the ability:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { registerAbilityCategory, registerAbility } from '@wordpress/abilities';
|
||||||
|
|
||||||
|
// Register the category first
|
||||||
|
await registerAbilityCategory('my-custom-category', {
|
||||||
|
label: 'My Custom Category',
|
||||||
|
description: 'Description of what abilities belong in this category',
|
||||||
|
meta: {
|
||||||
|
icon: 'dashicons-admin-customizer',
|
||||||
|
priority: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then register abilities using that category
|
||||||
|
await registerAbility({
|
||||||
|
name: 'my-plugin/my-ability',
|
||||||
|
category: 'my-custom-category', // Uses the client-registered category
|
||||||
|
// ... other properties
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Ability Classes (Advanced)
|
||||||
|
|
||||||
|
For advanced use cases requiring custom behavior, specify a custom class that extends `WP_Ability` using the `ability_class` parameter:
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Custom_Ability extends WP_Ability {
|
||||||
|
// Override methods for custom behavior
|
||||||
|
public function execute( $input = array() ) {
|
||||||
|
// Custom execution logic
|
||||||
|
return parent::execute( $input );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_register_ability( 'my-plugin/custom-ability', array(
|
||||||
|
// ... standard parameters
|
||||||
|
'ability_class' => Custom_Ability::class, // Use custom class instead of WP_Ability
|
||||||
|
) );
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** This is an advanced feature. Most abilities should use the default `WP_Ability` class.
|
||||||
20
skills/wordpress-ability-api/assets/category-template.php
Normal file
20
skills/wordpress-ability-api/assets/category-template.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Register ability category: {{LABEL}}
|
||||||
|
*
|
||||||
|
* {{DESCRIPTION}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: abilities_api_categories_init
|
||||||
|
* Categories must be registered BEFORE abilities that use them.
|
||||||
|
* This hook fires before 'abilities_api_init'.
|
||||||
|
*/
|
||||||
|
add_action( 'abilities_api_categories_init', '{{REGISTER_FUNCTION}}' );
|
||||||
|
|
||||||
|
function {{REGISTER_FUNCTION}}() {
|
||||||
|
wp_register_ability_category( '{{CATEGORY_NAME}}', array(
|
||||||
|
'label' => __( '{{LABEL}}', '{{NAMESPACE}}' ),
|
||||||
|
'description' => __( '{{DESCRIPTION}}', '{{NAMESPACE}}' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Client-Side Ability: {{LABEL}}
|
||||||
|
*
|
||||||
|
* {{DESCRIPTION}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerAbility } from '@wordpress/abilities';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the {{ABILITY_NAME}} ability.
|
||||||
|
*/
|
||||||
|
registerAbility( {
|
||||||
|
name: '{{ABILITY_NAME}}',
|
||||||
|
label: '{{LABEL}}',
|
||||||
|
description: '{{DESCRIPTION}}',
|
||||||
|
category: '{{CATEGORY}}',
|
||||||
|
|
||||||
|
// Define expected input structure using JSON Schema
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
// TODO: Define input parameters
|
||||||
|
// param1: {
|
||||||
|
// type: 'string',
|
||||||
|
// description: 'Description of parameter',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
required: [], // TODO: List required parameters
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Define output structure using JSON Schema
|
||||||
|
outputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
result: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The result of the operation',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// The async callback function to execute
|
||||||
|
callback: async ( input ) => {
|
||||||
|
// TODO: Implement your ability logic here
|
||||||
|
|
||||||
|
// Example error handling:
|
||||||
|
// if ( ! someValidation( input ) ) {
|
||||||
|
// throw new Error( 'The provided input is invalid.' );
|
||||||
|
// }
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: 'TODO: Implement logic',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Permission check callback
|
||||||
|
permissionCallback: ( input ) => {
|
||||||
|
// TODO: Implement appropriate permission checks
|
||||||
|
// Examples:
|
||||||
|
// return window.wp?.data?.select('core')?.getCurrentUser() !== null;
|
||||||
|
return true; // Everyone can access (use with caution!)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
meta: {
|
||||||
|
annotations: {
|
||||||
|
readonly: {{READONLY}},
|
||||||
|
destructive: {{DESTRUCTIVE}},
|
||||||
|
idempotent: {{IDEMPOTENT}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Register ability category: {{LABEL}}
|
||||||
|
*
|
||||||
|
* {{DESCRIPTION}}
|
||||||
|
*
|
||||||
|
* Note: This must be called BEFORE registering abilities that use this category.
|
||||||
|
* Typically placed at the top of your abilities registration file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerAbilityCategory } from '@wordpress/abilities';
|
||||||
|
|
||||||
|
await registerAbilityCategory('{{CATEGORY_NAME}}', {
|
||||||
|
label: '{{LABEL}}',
|
||||||
|
description: '{{DESCRIPTION}}',
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ability: {{LABEL}}
|
||||||
|
*
|
||||||
|
* {{DESCRIPTION}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute callback for {{ABILITY_NAME}} ability.
|
||||||
|
*
|
||||||
|
* @param array|mixed $input Input data matching the input_schema.
|
||||||
|
* @return mixed Output data matching the output_schema, or WP_Error on failure.
|
||||||
|
*/
|
||||||
|
function {{CALLBACK_FUNCTION}}( $input ) {
|
||||||
|
// TODO: Implement your ability logic here
|
||||||
|
|
||||||
|
// Example error handling:
|
||||||
|
// if ( ! some_validation( $input ) ) {
|
||||||
|
// return new WP_Error(
|
||||||
|
// 'invalid_input',
|
||||||
|
// __( 'The provided input is invalid.', '{{NAMESPACE}}' )
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'result' => 'TODO: Implement logic',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the {{ABILITY_NAME}} ability.
|
||||||
|
*/
|
||||||
|
add_action( 'abilities_api_init', '{{REGISTER_FUNCTION}}' );
|
||||||
|
|
||||||
|
function {{REGISTER_FUNCTION}}() {
|
||||||
|
wp_register_ability( '{{ABILITY_NAME}}', array(
|
||||||
|
'label' => __( '{{LABEL}}', '{{NAMESPACE}}' ),
|
||||||
|
'description' => __( '{{DESCRIPTION}}', '{{NAMESPACE}}' ),
|
||||||
|
'category' => '{{CATEGORY}}',
|
||||||
|
|
||||||
|
// Define expected input structure using JSON Schema
|
||||||
|
'input_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
// TODO: Define input parameters
|
||||||
|
// 'param1' => array(
|
||||||
|
// 'type' => 'string',
|
||||||
|
// 'description' => __( 'Description of parameter', '{{NAMESPACE}}' ),
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
'required' => array(), // TODO: List required parameters
|
||||||
|
'additionalProperties' => false,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Define output structure using JSON Schema
|
||||||
|
'output_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'result' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'The result of the operation', '{{NAMESPACE}}' ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// The callback function to execute
|
||||||
|
'execute_callback' => '{{CALLBACK_FUNCTION}}',
|
||||||
|
|
||||||
|
// Permission check callback
|
||||||
|
'permission_callback' => function( $input ) {
|
||||||
|
// TODO: Implement appropriate permission checks
|
||||||
|
// Examples:
|
||||||
|
// return current_user_can( 'manage_options' );
|
||||||
|
// return is_user_logged_in();
|
||||||
|
return __return_true(); // Everyone can access (use with caution!)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
'meta' => array(
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'annotations' => array(
|
||||||
|
'readonly' => {{READONLY}},
|
||||||
|
'destructive' => {{DESTRUCTIVE}},
|
||||||
|
'idempotent' => {{IDEMPOTENT}},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) );
|
||||||
|
}
|
||||||
99
skills/wordpress-ability-api/references/1.intro.md
Normal file
99
skills/wordpress-ability-api/references/1.intro.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# 1. Introduction & Overview
|
||||||
|
|
||||||
|
## What is the Abilities API?
|
||||||
|
|
||||||
|
The WordPress Abilities API provides a standardized way to register and discover distinct units of functionality within a WordPress site. These units, called "Abilities", represent specific actions or capabilities that components can perform, with clearly defined inputs, outputs, and permissions.
|
||||||
|
|
||||||
|
It acts as a central registry, making it easier for different parts of WordPress, third-party plugins, themes, and external systems (like AI agents) to understand and interact with the capabilities available on a specific site.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
- **Ability:** A distinct piece of functionality with a unique name following the `namespace/ability-name` pattern. Each ability has a human-readable name and description, input/output definitions (using JSON Schema), a category assignment, optional permissions, and an associated callback function for execution. Each registered Ability is an instance of the `WP_Ability` class.
|
||||||
|
- **Category:** A way to organize related abilities. Each ability must belong to exactly one category. Categories have a slug, label, and description. Each registered category is an instance of the `WP_Ability_Category` class.
|
||||||
|
- **Registry:** A central, singleton object (`WP_Abilities_Registry`) that holds all registered abilities. It provides methods for registering, unregistering, finding, and querying abilities. Similarly, `WP_Abilities_Category_Registry` manages all registered categories.
|
||||||
|
- **Callback:** The PHP function or method executed when an ability is called via `WP_Ability::execute()`.
|
||||||
|
- **Schema:** JSON Schema definitions for an ability's expected input (`input_schema`) and its returned output (`output_schema`). This allows for validation and helps agents understand how to use the ability.
|
||||||
|
- **Permission Callback:** An optional function that determines if the current user can execute a specific ability.
|
||||||
|
- **Namespace:** The first part of an ability name (before the slash), typically matching the plugin or component name that registers the ability.
|
||||||
|
|
||||||
|
## Goals and Benefits
|
||||||
|
|
||||||
|
- **Standardization:** Provides a single, consistent way to expose site capabilities.
|
||||||
|
- **Discoverability:** Makes functionality easily discoverable by AI systems and automation tools.
|
||||||
|
- **Validation:** Built-in input/output validation using JSON Schema ensures data integrity.
|
||||||
|
- **Security:** Permission callbacks provide fine-grained access control.
|
||||||
|
- **Extensibility:** Simple registration pattern allows any plugin or theme to expose their capabilities.
|
||||||
|
- **AI-Friendly:** Machine-readable format enables intelligent automation and AI agent interactions.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **AI Integration:** Allow AI agents to discover and interact with site capabilities.
|
||||||
|
- **Plugin Interoperability:** Enable plugins to discover and use each other's functionality.
|
||||||
|
- **Automation Tools:** Provide programmatic access to site features.
|
||||||
|
- **API Documentation:** Self-documenting capabilities with schema validation.
|
||||||
|
- **Developer Tools:** Standardized way to expose plugin functionality.
|
||||||
|
|
||||||
|
## Registration Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
// First, register a category, or use one of the existing categories.
|
||||||
|
add_action( 'abilities_api_categories_init', 'my_plugin_register_category');
|
||||||
|
function my_plugin_register_category(){
|
||||||
|
wp_register_ability_category( 'site-information', array(
|
||||||
|
'label' => __( 'Site Information', 'my-plugin' ),
|
||||||
|
'description' => __( 'Abilities that provide information about the WordPress site.', 'my-plugin' ),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, register an ability in that category
|
||||||
|
add_action( 'abilities_api_init', 'my_plugin_register_ability');
|
||||||
|
function my_plugin_register_ability(){
|
||||||
|
wp_register_ability( 'my-plugin/site-info', array(
|
||||||
|
'label' => __( 'Site Info', 'my-plugin' ),
|
||||||
|
'description' => __( 'Returns information about this WordPress site', 'my-plugin' ),
|
||||||
|
'category' => 'site-information',
|
||||||
|
'input_schema' => array(),
|
||||||
|
'output_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'site_name' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'The name of the WordPress site', 'my-plugin' )
|
||||||
|
),
|
||||||
|
'site_url' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'The URL of the WordPress site', 'my-plugin' )
|
||||||
|
),
|
||||||
|
'active_theme' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'The active theme of the WordPress site', 'my-plugin' )
|
||||||
|
),
|
||||||
|
'active_plugins' => array(
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'string'
|
||||||
|
),
|
||||||
|
'description' => __( 'List of active plugins on the WordPress site', 'my-plugin' )
|
||||||
|
),
|
||||||
|
'php_version' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'The PHP version of the WordPress site', 'my-plugin' )
|
||||||
|
),
|
||||||
|
'wordpress_version' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'The WordPress version of the site', 'my-plugin' )
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'execute_callback' => 'my_plugin_get_siteinfo',
|
||||||
|
'permission_callback' => function( $input ) {
|
||||||
|
return current_user_can( 'manage_options' );
|
||||||
|
},
|
||||||
|
'meta' => array(
|
||||||
|
'show_in_rest' => true,
|
||||||
|
),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a machine-readable capability that AI systems and automation tools can discover, understand, and execute safely within the bounds of WordPress permissions and validation rules.
|
||||||
150
skills/wordpress-ability-api/references/2.getting-started.md
Normal file
150
skills/wordpress-ability-api/references/2.getting-started.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# 2. Getting Started
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Until the Abilities API is merged into WordPress core, it must be installed before it can be used.
|
||||||
|
|
||||||
|
### As a plugin
|
||||||
|
|
||||||
|
The easiest way to try and use the Abilities API is to install it as a plugin by downloading the latest release from the [GitHub Releases page](https://github.com/WordPress/abilities-api/releases/latest).
|
||||||
|
|
||||||
|
#### With WP-CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp plugin install https://github.com/WordPress/abilities-api/releases/latest/download/abilities-api.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With WP-Env
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// .wp-env.json
|
||||||
|
{
|
||||||
|
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
|
||||||
|
// ... other config ...
|
||||||
|
"plugins": [
|
||||||
|
"WordPress/abilities-api"
|
||||||
|
// ... other plugins ...
|
||||||
|
]
|
||||||
|
// ... more config ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### As a dependency
|
||||||
|
|
||||||
|
Plugin authors and developers may wish to rely on the Abilities API as a dependency in their own projects, before it is merged into core. You can do that in one of the following ways.
|
||||||
|
|
||||||
|
#### As a Plugin Dependency (Recommended)
|
||||||
|
|
||||||
|
The best way to ensure the Abilities API is available for your plugins is to include it as one of your `Requires Plugins` in your [Plugin header](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/). For example:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
# my-plugin.php
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Plugin Name: My Plugin
|
||||||
|
* Plugin URI: https://example.com/plugins/the-basics/
|
||||||
|
* Description: Handle the basics with this plugin.
|
||||||
|
* {all the other plugin header fields...}
|
||||||
|
+ * Requires Plugins: abilities-api
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
While this is enough to ensure the Abilities API is loaded before your plugin, if you need to ensure specific version requirements or provide users guidance on installing the plugin, you can use the methods described [later on](#checking-availability-with-code)
|
||||||
|
|
||||||
|
#### As a Composer dependency
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require wordpress/abilities-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking availability with code
|
||||||
|
|
||||||
|
To ensure the Abilities API is loaded in your plugin:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if ( ! class_exists( 'WP_Ability' ) ) {
|
||||||
|
// E.g. add an admin notice about the missing dependency.
|
||||||
|
add_action( 'admin_notices', static function() {
|
||||||
|
wp_admin_notice(
|
||||||
|
// If it's a Composer dependency, you might want to suggest running `composer install` instead.
|
||||||
|
esc_html__( 'This plugin requires the Abilities API to use. Please install and activate it.', 'my-plugin' ),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also check for specific versions of the Abilities API using the `WP_ABILITIES_API_VERSION` constant:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if ( ! defined( 'WP_ABILITIES_API_VERSION' ) || version_compare( WP_ABILITIES_API_VERSION, '0.1.0', '<' ) ) {
|
||||||
|
// E.g. add an admin notice about the required version.
|
||||||
|
add_action( 'admin_notices', static function() {
|
||||||
|
wp_admin_notice(
|
||||||
|
esc_html__( 'This plugin requires Abilities API version 0.1.0 or higher. Please update the plugin dependency.', 'my-plugin' ),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage Example
|
||||||
|
|
||||||
|
The below example is for a plugin implementation, but it could also be adapted for a theme's functions.php
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// 1. Define a callback function for your ability.
|
||||||
|
function my_plugin_get_site_title( array $input = array() ): string {
|
||||||
|
return get_bloginfo( 'name' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Register the ability when the Abilities API is initialized.
|
||||||
|
// Using `abilities_api_init` ensures the API is fully loaded.
|
||||||
|
add_action( 'abilities_api_init', 'my_plugin_register_abilities' );
|
||||||
|
|
||||||
|
function my_plugin_register_abilities() {
|
||||||
|
wp_register_ability( 'my-plugin/get-site-title', array(
|
||||||
|
'label' => __( 'Get Site Title', 'my-plugin' ),
|
||||||
|
'description' => __( 'Retrieves the title of the current WordPress site.', 'my-plugin' ),
|
||||||
|
'input_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(),
|
||||||
|
'additionalProperties' => false,
|
||||||
|
),
|
||||||
|
'output_schema' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'The site title.',
|
||||||
|
),
|
||||||
|
'execute_callback' => 'my_plugin_get_site_title',
|
||||||
|
'permission_callback' => '__return_true', // Everyone can access this
|
||||||
|
'meta' => array(
|
||||||
|
'category' => 'site-info',
|
||||||
|
'show_in_rest' => true, // Optional: expose via REST API
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Later, you can retrieve and execute the ability.
|
||||||
|
add_action( 'admin_init', 'my_plugin_use_ability' );
|
||||||
|
|
||||||
|
function my_plugin_use_ability() {
|
||||||
|
$ability = wp_get_ability( 'my-plugin/get-site-title' );
|
||||||
|
if ( ! $ability ) {
|
||||||
|
// Ability not found.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$site_title = $ability->execute();
|
||||||
|
if ( is_wp_error( $site_title ) ) {
|
||||||
|
// Handle execution error
|
||||||
|
error_log( 'Execution error: ' . $site_title->get_error_message() );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `$site_title` now holds the result of `get_bloginfo( 'name' )`.
|
||||||
|
echo 'Site Title: ' . esc_html( $site_title );
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
# 3. Registering Abilities (`wp_register_ability`)
|
||||||
|
|
||||||
|
The primary way to add functionality to the Abilities API is by using the `wp_register_ability()` function, typically hooked into the `abilities_api_init` action.
|
||||||
|
|
||||||
|
## Function Signature
|
||||||
|
|
||||||
|
```php
|
||||||
|
wp_register_ability( string $id, array $args ): ?\WP_Ability
|
||||||
|
```
|
||||||
|
|
||||||
|
- `$id` (`string`): A unique identifier for the ability.
|
||||||
|
- `$args` (`array`): An array of arguments defining the ability configuration.
|
||||||
|
- **Return:** (`?\WP_Ability`) An instance of the registered ability if it was successfully registered, `null` on failure (e.g., invalid arguments, duplicate ID).
|
||||||
|
|
||||||
|
## Parameters Explained
|
||||||
|
|
||||||
|
The `$args` array accepts the following keys:
|
||||||
|
|
||||||
|
- `label` (`string`, **Required**): A human-readable name for the ability. Used for display purposes. Should be translatable.
|
||||||
|
- `description` (`string`, **Required**): A detailed description of what the ability does, its purpose, and its parameters or return values. This is crucial for AI agents to understand how and when to use the ability. Should be translatable.
|
||||||
|
- `category` (`string`, **Required**): The slug of the category this ability belongs to. The category must be registered before registering the ability using `wp_register_ability_category()`. Categories help organize and filter abilities by their purpose. See [Registering Categories](7.registering-categories.md) for details.
|
||||||
|
- `input_schema` (`array`, **Required**): A JSON Schema definition describing the expected input parameters for the ability's execute callback. Used for validation and documentation.
|
||||||
|
- `output_schema` (`array`, **Required**): A JSON Schema definition describing the expected format of the data returned by the ability. Used for validation and documentation.
|
||||||
|
- `execute_callback` (`callable`, **Required**): The PHP function or method to execute when this ability is called.
|
||||||
|
- The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.).
|
||||||
|
- The callback should return the result of the ability's operation or return a `WP_Error` object on failure.
|
||||||
|
- `permission_callback` (`callable`, **Required**): A callback function to check if the current user has permission to execute this ability.
|
||||||
|
- The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.).
|
||||||
|
- The callback should return a boolean (`true` if the user has permission, `false` otherwise), or a `WP_Error` object on failure.
|
||||||
|
- If the input does not validate against the input schema, the permission callback will not be called, and a `WP_Error` will be returned instead.
|
||||||
|
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the ability.
|
||||||
|
- `annotations` (`array`, **Optional**): An associative array of annotations providing hints about the ability's behavior characteristics. Supports the following keys:
|
||||||
|
- `instructions` (`string`, **Optional**): Custom instructions or guidance for using the ability (default: `''`).
|
||||||
|
- `readonly` (`boolean`, **Optional**): Whether the ability only reads data without modifying its environment (default: `false`).
|
||||||
|
- `destructive` (`boolean`, **Optional**): Whether the ability may perform destructive updates to its environment. If `true`, the ability may perform any type of modification, including deletions or other destructive changes. If `false`, the ability performs only additive updates (default: `true`).
|
||||||
|
- `idempotent` (`boolean`, **Optional**): Whether calling the ability repeatedly with the same arguments will have no additional effect on its environment (default: `false`).
|
||||||
|
- `show_in_rest` (`boolean`, **Optional**): Whether to expose this ability via the REST API. Default: `false`.
|
||||||
|
- When `true`, the ability will be listed in REST API responses and can be executed via REST endpoints.
|
||||||
|
- When `false`, the ability will be hidden from REST API listings and cannot be executed via REST endpoints, but remains available for internal PHP usage.
|
||||||
|
|
||||||
|
## Ability ID Convention
|
||||||
|
|
||||||
|
The `$id` parameter must follow the pattern `namespace/ability-name`:
|
||||||
|
|
||||||
|
- **Format:** Must contain only lowercase alphanumeric characters (`a-z`, `0-9`), hyphens (`-`), and one forward slash (`/`) for namespacing.
|
||||||
|
- **Convention:** Use your plugin slug as the namespace, like `my-plugin/ability-name`.
|
||||||
|
- **Examples:** `my-plugin/update-settings`, `woocommerce/get-product`, `contact-form/send-message`, `analytics/track-event`
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Registering a Simple Data Retrieval Ability
|
||||||
|
|
||||||
|
```php
|
||||||
|
add_action( 'abilities_api_init', 'my_plugin_register_site_info_ability' );
|
||||||
|
function my_plugin_register_site_info_ability() {
|
||||||
|
wp_register_ability( 'my-plugin/get-site-info', array(
|
||||||
|
'label' => __( 'Get Site Information', 'my-plugin' ),
|
||||||
|
'description' => __( 'Retrieves basic information about the WordPress site including name, description, and URL.', 'my-plugin' ),
|
||||||
|
'category' => 'data-retrieval',
|
||||||
|
'input_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(),
|
||||||
|
'additionalProperties' => false
|
||||||
|
),
|
||||||
|
'output_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'name' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Site name'
|
||||||
|
),
|
||||||
|
'description' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Site tagline'
|
||||||
|
),
|
||||||
|
'url' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'uri',
|
||||||
|
'description' => 'Site URL'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'execute_callback' => function( $input ) {
|
||||||
|
return array(
|
||||||
|
'name' => get_bloginfo( 'name' ),
|
||||||
|
'description' => get_bloginfo( 'description' ),
|
||||||
|
'url' => home_url()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
'meta' => array(
|
||||||
|
'annotations' => array(
|
||||||
|
'readonly' => true,
|
||||||
|
'destructive' => false
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering an Ability with Input Parameters
|
||||||
|
|
||||||
|
```php
|
||||||
|
add_action( 'abilities_api_init', 'my_plugin_register_update_option_ability' );
|
||||||
|
function my_plugin_register_update_option_ability() {
|
||||||
|
wp_register_ability( 'my-plugin/update-option', array(
|
||||||
|
'label' => __( 'Update WordPress Option', 'my-plugin' ),
|
||||||
|
'description' => __( 'Updates the value of a WordPress option in the database. Requires manage_options capability.', 'my-plugin' ),
|
||||||
|
'category' => 'data-modification',
|
||||||
|
'input_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'option_name' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'The name of the option to update',
|
||||||
|
'minLength' => 1
|
||||||
|
),
|
||||||
|
'option_value' => array(
|
||||||
|
'description' => 'The new value for the option'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'required' => array( 'option_name', 'option_value' ),
|
||||||
|
'additionalProperties' => false
|
||||||
|
),
|
||||||
|
'output_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'success' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'description' => 'Whether the option was successfully updated'
|
||||||
|
),
|
||||||
|
'previous_value' => array(
|
||||||
|
'description' => 'The previous value of the option'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'execute_callback' => function( $input ) {
|
||||||
|
$option_name = $input['option_name'];
|
||||||
|
$new_value = $input['option_value'];
|
||||||
|
|
||||||
|
$previous_value = get_option( $option_name );
|
||||||
|
$success = update_option( $option_name, $new_value );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'success' => $success,
|
||||||
|
'previous_value' => $previous_value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'permission_callback' => function() {
|
||||||
|
return current_user_can( 'manage_options' );
|
||||||
|
},
|
||||||
|
'meta' => array(
|
||||||
|
'annotations' => array(
|
||||||
|
'destructive' => false,
|
||||||
|
'idempotent' => true
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering an Ability with Plugin Dependencies
|
||||||
|
|
||||||
|
```php
|
||||||
|
add_action( 'abilities_api_init', 'my_plugin_register_woo_stats_ability' );
|
||||||
|
function my_plugin_register_woo_stats_ability() {
|
||||||
|
// Only register if WooCommerce is active
|
||||||
|
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_register_ability( 'my-plugin/get-woo-stats', array(
|
||||||
|
'label' => __( 'Get WooCommerce Statistics', 'my-plugin' ),
|
||||||
|
'description' => __( 'Retrieves basic WooCommerce store statistics including total orders and revenue.', 'my-plugin' ),
|
||||||
|
'category' => 'ecommerce',
|
||||||
|
'input_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'period' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'today', 'week', 'month', 'year' ),
|
||||||
|
'default' => 'month',
|
||||||
|
'description' => 'Time period for statistics'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'additionalProperties' => false
|
||||||
|
),
|
||||||
|
'output_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'total_orders' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
'description' => 'Number of orders in period'
|
||||||
|
),
|
||||||
|
'total_revenue' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'description' => 'Total revenue in period'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'execute_callback' => function( $input ) {
|
||||||
|
$period = $input['period'] ?? 'month';
|
||||||
|
|
||||||
|
// Implementation would calculate stats based on period
|
||||||
|
return array(
|
||||||
|
'total_orders' => 42,
|
||||||
|
'total_revenue' => 1250.50
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'permission_callback' => function() {
|
||||||
|
return current_user_can( 'manage_woocommerce' );
|
||||||
|
},
|
||||||
|
'meta' => array(
|
||||||
|
'requires_plugin' => 'woocommerce',
|
||||||
|
'category' => 'ecommerce'
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering an Ability That May Fail
|
||||||
|
|
||||||
|
```php
|
||||||
|
add_action( 'abilities_api_init', 'my_plugin_register_send_email_ability' );
|
||||||
|
function my_plugin_register_send_email_ability() {
|
||||||
|
wp_register_ability( 'my-plugin/send-email', array(
|
||||||
|
'label' => __( 'Send Email', 'my-plugin' ),
|
||||||
|
'description' => __( 'Sends an email to the specified recipient using WordPress mail functions.', 'my-plugin' ),
|
||||||
|
'category' => 'communication',
|
||||||
|
'input_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'to' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'email',
|
||||||
|
'description' => 'Recipient email address'
|
||||||
|
),
|
||||||
|
'subject' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'minLength' => 1,
|
||||||
|
'description' => 'Email subject'
|
||||||
|
),
|
||||||
|
'message' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'minLength' => 1,
|
||||||
|
'description' => 'Email message body'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'required' => array( 'to', 'subject', 'message' ),
|
||||||
|
'additionalProperties' => false
|
||||||
|
),
|
||||||
|
'output_schema' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'sent' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'description' => 'Whether the email was successfully sent'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'execute_callback' => function( $input ) {
|
||||||
|
$sent = wp_mail(
|
||||||
|
$input['to'],
|
||||||
|
$input['subject'],
|
||||||
|
$input['message']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $sent ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'email_send_failed',
|
||||||
|
sprintf( __( 'Failed to send email' ), 'my-plugin' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array( 'sent' => true );
|
||||||
|
},
|
||||||
|
'permission_callback' => function() {
|
||||||
|
return current_user_can( 'publish_posts' );
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
211
skills/wordpress-ability-api/references/4.using-abilities.md
Normal file
211
skills/wordpress-ability-api/references/4.using-abilities.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# 4. Using Abilities (`wp_get_ability`, `wp_get_abilities`)
|
||||||
|
|
||||||
|
Once abilities are registered, they can be retrieved and executed using global functions from the Abilities API.
|
||||||
|
|
||||||
|
## Getting a Specific Ability (`wp_get_ability`)
|
||||||
|
|
||||||
|
To get a single ability object by its name (namespace/ability-name):
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Retrieves a registered ability using Abilities API.
|
||||||
|
*
|
||||||
|
* @param string $name The name of the registered ability, with its namespace.
|
||||||
|
* @return ?WP_Ability The registered ability instance, or null if it is not registered.
|
||||||
|
*/
|
||||||
|
function wp_get_ability( string $name ): ?WP_Ability
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
$site_info_ability = wp_get_ability( 'my-plugin/get-site-info' );
|
||||||
|
|
||||||
|
if ( $site_info_ability ) {
|
||||||
|
// Ability exists and is registered
|
||||||
|
$site_info = $site_info_ability->execute();
|
||||||
|
if ( is_wp_error( $site_info ) ) {
|
||||||
|
// Handle WP_Error
|
||||||
|
echo 'Error: ' . $site_info->get_error_message();
|
||||||
|
} else {
|
||||||
|
// Use $site_info array
|
||||||
|
echo 'Site Name: ' . $site_info['name'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ability not found or not registered
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting All Registered Abilities (`wp_get_abilities`)
|
||||||
|
|
||||||
|
To get an array of all registered abilities:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Retrieves all registered abilities using Abilities API.
|
||||||
|
*
|
||||||
|
* @return WP_Ability[] The array of registered abilities.
|
||||||
|
*/
|
||||||
|
function wp_get_abilities(): array
|
||||||
|
|
||||||
|
// Example: Get all registered abilities
|
||||||
|
$all_abilities = wp_get_abilities();
|
||||||
|
|
||||||
|
foreach ( $all_abilities as $name => $ability ) {
|
||||||
|
echo 'Ability Name: ' . esc_html( $ability->get_name() ) . "\n";
|
||||||
|
echo 'Label: ' . esc_html( $ability->get_label() ) . "\n";
|
||||||
|
echo 'Description: ' . esc_html( $ability->get_description() ) . "\n";
|
||||||
|
echo "---\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Executing an Ability (`$ability->execute()`)
|
||||||
|
|
||||||
|
Once you have a `WP_Ability` object (usually from `wp_get_ability`), you execute it using the `execute()` method.
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Executes the ability after input validation and running a permission check.
|
||||||
|
*
|
||||||
|
* @param mixed $input Optional. The input data for the ability. Defaults to `null`.
|
||||||
|
* @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
|
||||||
|
*/
|
||||||
|
// public function execute( $input = null )
|
||||||
|
|
||||||
|
// Example 1: Ability with no input parameters
|
||||||
|
$ability = wp_get_ability( 'my-plugin/get-site-info' );
|
||||||
|
if ( $ability ) {
|
||||||
|
$site_info = $ability->execute(); // No input required
|
||||||
|
if ( is_wp_error( $site_info ) ) {
|
||||||
|
// Handle WP_Error
|
||||||
|
echo 'Error: ' . $site_info->get_error_message();
|
||||||
|
} else {
|
||||||
|
// Use $site_info array
|
||||||
|
echo 'Site Name: ' . $site_info['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Ability with input parameters
|
||||||
|
$ability = wp_get_ability( 'my-plugin/update-option' );
|
||||||
|
if ( $ability ) {
|
||||||
|
$input = array(
|
||||||
|
'option_name' => 'blogname',
|
||||||
|
'option_value' => 'My Updated Site Name',
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $ability->execute( $input );
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
// Handle WP_Error
|
||||||
|
echo 'Error: ' . $result->get_error_message();
|
||||||
|
} else {
|
||||||
|
// Use $result
|
||||||
|
if ( $result['success'] ) {
|
||||||
|
echo 'Option updated successfully!';
|
||||||
|
echo 'Previous value: ' . $result['previous_value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 3: Ability with complex input validation
|
||||||
|
$ability = wp_get_ability( 'my-plugin/send-email' );
|
||||||
|
if ( $ability ) {
|
||||||
|
$input = array(
|
||||||
|
'to' => 'user@example.com',
|
||||||
|
'subject' => 'Hello from WordPress',
|
||||||
|
'message' => 'This is a test message from the Abilities API.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $ability->execute( $input );
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
// Handle WP_Error
|
||||||
|
echo 'Error: ' . $result->get_error_message();
|
||||||
|
} elseif ( $result['sent'] ) {
|
||||||
|
echo 'Email sent successfully!';
|
||||||
|
} else {
|
||||||
|
echo 'Email failed to send.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking Permissions (`$ability->check_permissions()`)
|
||||||
|
|
||||||
|
You can check if the current user has permissions to execute the ability, also without executing it. The `check_permissions()` method returns either `true`, `false`, or a `WP_Error` object. `true` means permission is granted, `false` means the user simply lacks permission, and a `WP_Error` return value typically indicates a failure in the permission check process (such as an internal error or misconfiguration). You must use `is_wp_error()` to handle errors properly and distinguish between permission denial and actual errors:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$ability = wp_get_ability( 'my-plugin/update-option' );
|
||||||
|
if ( $ability ) {
|
||||||
|
$input = array(
|
||||||
|
'option_name' => 'blogname',
|
||||||
|
'option_value' => 'New Site Name',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check permission before execution - always use is_wp_error() first
|
||||||
|
$has_permissions = $ability->check_permissions( $input );
|
||||||
|
if ( true === $has_permissions ) {
|
||||||
|
// Permissions granted – safe to execute.
|
||||||
|
echo 'You have permissions to execute this ability.';
|
||||||
|
} else {
|
||||||
|
// Don't leak permission errors to unauthenticated users.
|
||||||
|
if ( is_wp_error( $has_permissions ) ) {
|
||||||
|
error_log( 'Permissions check failed: ' . $has_permissions->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
echo 'You do not have permissions to execute this ability.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inspecting Ability Properties
|
||||||
|
|
||||||
|
The `WP_Ability` class provides several getter methods to inspect ability properties:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$ability = wp_get_ability( 'my-plugin/get-site-info' );
|
||||||
|
if ( $ability ) {
|
||||||
|
// Basic properties
|
||||||
|
echo 'Name: ' . $ability->get_name() . "\n";
|
||||||
|
echo 'Label: ' . $ability->get_label() . "\n";
|
||||||
|
echo 'Description: ' . $ability->get_description() . "\n";
|
||||||
|
|
||||||
|
// Schema information
|
||||||
|
$input_schema = $ability->get_input_schema();
|
||||||
|
$output_schema = $ability->get_output_schema();
|
||||||
|
|
||||||
|
echo 'Input Schema: ' . json_encode( $input_schema, JSON_PRETTY_PRINT ) . "\n";
|
||||||
|
echo 'Output Schema: ' . json_encode( $output_schema, JSON_PRETTY_PRINT ) . "\n";
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
$meta = $ability->get_meta();
|
||||||
|
if ( ! empty( $meta ) ) {
|
||||||
|
echo 'Metadata: ' . json_encode( $meta, JSON_PRETTY_PRINT ) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Patterns
|
||||||
|
|
||||||
|
The Abilities API uses several error handling mechanisms:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$ability = wp_get_ability( 'my-plugin/some-ability' );
|
||||||
|
|
||||||
|
if ( ! $ability ) {
|
||||||
|
// Ability not registered
|
||||||
|
echo 'Ability not found';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $ability->execute( $input );
|
||||||
|
|
||||||
|
// Check for WP_Error (validation, permission, or callback errors)
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
echo 'WP_Error: ' . $result->get_error_message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for null result (permission denied, invalid callback, or validation failure)
|
||||||
|
if ( is_null( $result ) ) {
|
||||||
|
echo 'Execution returned null - check permissions and callback validity';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - use the result
|
||||||
|
// Process $result based on the ability's output schema
|
||||||
|
```
|
||||||
345
skills/wordpress-ability-api/references/5.rest-api.md
Normal file
345
skills/wordpress-ability-api/references/5.rest-api.md
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# 5. REST API Reference
|
||||||
|
|
||||||
|
The WordPress Abilities API provides REST endpoints that allow external systems to discover and execute abilities via HTTP requests.
|
||||||
|
|
||||||
|
## User access
|
||||||
|
|
||||||
|
Access to all Abilities REST API endpoints requires an authenticated user (see the [Authentication](#authentication) section). Access to execute individual Abilities is restricted based on the `permission_callback()` of the Ability.
|
||||||
|
|
||||||
|
## Controlling REST API Exposure
|
||||||
|
|
||||||
|
By default, registered abilities are **not** exposed via the REST API. You can control whether an individual ability appears in the REST API by using the `show_in_rest` meta when registering the ability:
|
||||||
|
|
||||||
|
- `show_in_rest => true`: The ability is listed in REST API responses and can be executed via REST endpoints.
|
||||||
|
- `show_in_rest => false` (default): The ability is hidden from REST API listings and cannot be executed via REST endpoints. The ability remains available for internal PHP usage via `wp_execute_ability()`.
|
||||||
|
|
||||||
|
Abilities with meta `show_in_rest => false` will return a `rest_ability_not_found` error if accessed via REST endpoints.
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
The Abilities API endpoints are available under the `/wp/v2/abilities` namespace.
|
||||||
|
|
||||||
|
### Ability Object
|
||||||
|
|
||||||
|
Abilities are represented in JSON with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-plugin/get-site-info",
|
||||||
|
"label": "Get Site Information",
|
||||||
|
"description": "Retrieves basic information about the WordPress site.",
|
||||||
|
"category": "site-information",
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Site name"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Site URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"annotations": {
|
||||||
|
"instructions": "",
|
||||||
|
"readonly": true,
|
||||||
|
"destructive": false,
|
||||||
|
"idempotent": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## List Abilities
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
`GET /wp/v2/abilities`
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
- `page` _(integer)_: Current page of the collection. Default: `1`.
|
||||||
|
- `per_page` _(integer)_: Maximum number of items to return per page. Default: `50`, Maximum: `100`.
|
||||||
|
- `category` _(string)_: Filter abilities by category slug.
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://example.com/wp-json/wp/v2/abilities
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "my-plugin/get-site-info",
|
||||||
|
"label": "Get Site Information",
|
||||||
|
"description": "Retrieves basic information about the WordPress site.",
|
||||||
|
"category": "site-information",
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Site name"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Site URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"annotations": {
|
||||||
|
"instructions": "",
|
||||||
|
"readonly": false,
|
||||||
|
"destructive": true,
|
||||||
|
"idempotent": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## List Categories
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
`GET /wp/v2/abilities/categories`
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
- `page` _(integer)_: Current page of the collection. Default: `1`.
|
||||||
|
- `per_page` _(integer)_: Maximum number of items to return per page. Default: `50`, Maximum: `100`.
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -u 'USERNAME:APPLICATION_PASSWORD' \
|
||||||
|
https://example.com/wp-json/wp/v2/abilities/categories
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"slug": "data-retrieval",
|
||||||
|
"label": "Data Retrieval",
|
||||||
|
"description": "Abilities that retrieve and return data from the WordPress site.",
|
||||||
|
"meta": {},
|
||||||
|
"_links": {
|
||||||
|
"self": [
|
||||||
|
{
|
||||||
|
"href": "https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collection": [
|
||||||
|
{
|
||||||
|
"href": "https://example.com/wp-json/wp/v2/abilities/categories"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abilities": [
|
||||||
|
{
|
||||||
|
"href": "https://example.com/wp-json/wp/v2/abilities?category=data-retrieval"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retrieve a Category
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
`GET /wp/v2/abilities/categories/{slug}`
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
- `slug` _(string)_: The unique slug of the category.
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -u 'USERNAME:APPLICATION_PASSWORD' \
|
||||||
|
https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"slug": "data-retrieval",
|
||||||
|
"label": "Data Retrieval",
|
||||||
|
"description": "Abilities that retrieve and return data from the WordPress site.",
|
||||||
|
"meta": {},
|
||||||
|
"_links": {
|
||||||
|
"self": [
|
||||||
|
{
|
||||||
|
"href": "https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collection": [
|
||||||
|
{
|
||||||
|
"href": "https://example.com/wp-json/wp/v2/abilities/categories"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abilities": [
|
||||||
|
{
|
||||||
|
"href": "https://example.com/wp-json/wp/v2/abilities?category=data-retrieval"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retrieve an Ability
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
`GET /wp/v2/abilities/(?P<namespace>[a-z0-9-]+)/(?P<ability>[a-z0-9-]+)`
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
- `namespace` _(string)_: The namespace part of the ability name.
|
||||||
|
- `ability` _(string)_: The ability name part.
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-plugin/get-site-info",
|
||||||
|
"label": "Get Site Information",
|
||||||
|
"description": "Retrieves basic information about the WordPress site.",
|
||||||
|
"category": "site-information",
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Site name"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Site URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"annotations": {
|
||||||
|
"instructions": "",
|
||||||
|
"readonly": true,
|
||||||
|
"destructive": false,
|
||||||
|
"idempotent": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execute an Ability
|
||||||
|
|
||||||
|
Abilities are executed via the `/run` endpoint. The required HTTP method depends on the ability's `readonly` annotation:
|
||||||
|
|
||||||
|
- **Read-only abilities** (`readonly: true`) must use **GET**
|
||||||
|
- **Regular abilities** (default) must use **POST**
|
||||||
|
|
||||||
|
This distinction ensures read-only operations use safe HTTP methods that can be cached and don't modify server state.
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
`GET|POST /wp/v2/abilities/(?P<namespace>[a-z0-9-]+)/(?P<ability>[a-z0-9-]+)/run`
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
- `namespace` _(string)_: The namespace part of the ability name.
|
||||||
|
- `ability` _(string)_: The ability name part.
|
||||||
|
- `input` _(integer|number|boolean|string|array|object|null)_: Optional input data for the ability as defined by its input schema.
|
||||||
|
- For **GET requests**: pass as `input` query parameter (URL-encoded JSON)
|
||||||
|
- For **POST requests**: pass in JSON body
|
||||||
|
|
||||||
|
### Example Request (Read-only, GET)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# No input
|
||||||
|
curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info/run
|
||||||
|
|
||||||
|
# With input (URL-encoded)
|
||||||
|
curl "https://example.com/wp-json/wp/v2/abilities/my-plugin/get-user-info/run?input=%7B%22user_id%22%3A1%7D"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Request (Regular, POST)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# No input
|
||||||
|
curl -X POST https://example.com/wp-json/wp/v2/abilities/my-plugin/create-draft/run
|
||||||
|
|
||||||
|
# With input
|
||||||
|
curl -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"input":{"option_name":"blogname","option_value":"New Site Name"}}' \
|
||||||
|
https://example.com/wp-json/wp/v2/abilities/my-plugin/update-option/run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (Success)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "My WordPress Site",
|
||||||
|
"url": "https://example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (Error)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "ability_invalid_permissions",
|
||||||
|
"message": "Ability \"my-plugin/update-option\" does not have necessary permission.",
|
||||||
|
"data": {
|
||||||
|
"status": 403
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The Abilities API supports all WordPress REST API authentication methods:
|
||||||
|
|
||||||
|
- Cookie authentication (same-origin requests)
|
||||||
|
- Application passwords (recommended for external access)
|
||||||
|
- Custom authentication plugins
|
||||||
|
|
||||||
|
### Using Application Passwords
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -u 'USERNAME:APPLICATION_PASSWORD' \
|
||||||
|
https://example.com/wp-json/wp/v2/abilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
The API returns standard WordPress REST API error responses with these common codes:
|
||||||
|
|
||||||
|
- `ability_missing_input_schema` – the ability requires input but none was provided.
|
||||||
|
- `ability_invalid_input` - input validation failed according to the ability's schema.
|
||||||
|
- `ability_invalid_permissions` - current user lacks permission to execute the ability.
|
||||||
|
- `ability_invalid_output` - output validation failed according to the ability's schema.
|
||||||
|
- `ability_invalid_execute_callback` - the ability's execute callback is not callable.
|
||||||
|
- `rest_ability_not_found` - the requested ability is not registered.
|
||||||
|
- `rest_category_not_found` - the requested category is not registered.
|
||||||
|
- `rest_ability_invalid_method` - the requested HTTP method is not allowed for executing the selected ability (e.g., using GET on a read-only ability, or POST on a regular ability).
|
||||||
|
- `rest_ability_cannot_execute` - the ability cannot be executed due to insufficient permissions.
|
||||||
204
skills/wordpress-ability-api/references/6.hooks.md
Normal file
204
skills/wordpress-ability-api/references/6.hooks.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# 6. Hooks
|
||||||
|
|
||||||
|
The Abilities API provides [WordPress Action and Filter Hooks](https://developer.wordpress.org/apis/hooks/) that allow developers to monitor and respond to ability execution events.
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Actions](#actions)
|
||||||
|
- [`abilities_api_categories_init`](#abilities_api_categories_init)
|
||||||
|
- [`before_execute_ability`](#before_execute_ability)
|
||||||
|
- [`after_execute_ability`](#after_execute_ability)
|
||||||
|
- [Filters](#filters)
|
||||||
|
- [`register_ability_args`](#register_ability_args)
|
||||||
|
- [`register_ability_category_args`](#register_ability_category_args)
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
### `abilities_api_categories_init`
|
||||||
|
|
||||||
|
Fires when the category registry is first initialized. This is the proper hook to use when registering categories.
|
||||||
|
|
||||||
|
```php
|
||||||
|
do_action( 'abilities_api_categories_init', $registry );
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `$registry` (`\WP_Abilities_Category_Registry`): The category registry instance.
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Register custom ability categories.
|
||||||
|
*
|
||||||
|
* @param \WP_Abilities_Category_Registry $registry The category registry instance.
|
||||||
|
*/
|
||||||
|
function my_plugin_register_categories( $registry ) {
|
||||||
|
wp_register_ability_category( 'ecommerce', array(
|
||||||
|
'label' => __( 'E-commerce', 'my-plugin' ),
|
||||||
|
'description' => __( 'Abilities related to e-commerce functionality.', 'my-plugin' ),
|
||||||
|
));
|
||||||
|
|
||||||
|
wp_register_ability_category( 'analytics', array(
|
||||||
|
'label' => __( 'Analytics', 'my-plugin' ),
|
||||||
|
'description' => __( 'Abilities that provide analytical data and insights.', 'my-plugin' ),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `before_execute_ability`
|
||||||
|
|
||||||
|
Fires immediately before an ability gets executed, after permission checks have passed but before the execution callback is called.
|
||||||
|
|
||||||
|
```php
|
||||||
|
do_action( 'before_execute_ability', $ability_name, $input );
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `$ability_name` (`string`): The namespaced name of the ability being executed (e.g., `my-plugin/get-posts`).
|
||||||
|
- `$input` (`mixed`): The input data passed to the ability.
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Log each ability execution attempt.
|
||||||
|
* @param string $ability_name The name of the ability being executed.
|
||||||
|
* @param mixed $input The input data passed to the ability.
|
||||||
|
*/
|
||||||
|
function log_ability_execution( string $ability_name, $input ) {
|
||||||
|
error_log( 'About to execute ability: ' . $ability_name );
|
||||||
|
if ( $input !== null ) {
|
||||||
|
error_log( 'Input: ' . wp_json_encode( $input ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'before_execute_ability', 'log_ability_execution', 10, 2 );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `after_execute_ability`
|
||||||
|
|
||||||
|
Fires immediately after an ability has finished executing successfully, after output validation has passed.
|
||||||
|
|
||||||
|
```php
|
||||||
|
do_action( 'after_execute_ability', string $ability_name, $input, $result );
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `$ability_name` (`string`): The namespaced name of the ability that was executed.
|
||||||
|
- `$input` (`mixed`): The input data that was passed to the ability.
|
||||||
|
- `$result` (`mixed`): The validated result returned by the ability's execution callback.
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Log the result of each ability execution.
|
||||||
|
*
|
||||||
|
* @param string $ability_name The name of the executed ability.
|
||||||
|
* @param mixed $input The input data passed to the ability.
|
||||||
|
* @param mixed $result The result returned by the ability.
|
||||||
|
*/
|
||||||
|
function log_ability_result( string $ability_name, $input, $result ) {
|
||||||
|
error_log( 'Completed ability: ' . $ability_name );
|
||||||
|
error_log( 'Result: ' . wp_json_encode( $result ) );
|
||||||
|
}
|
||||||
|
add_action( 'after_execute_ability', 'log_ability_result', 10, 3 );
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filters
|
||||||
|
|
||||||
|
### `register_ability_args`
|
||||||
|
|
||||||
|
Allows modification of an Ability's args before they are validated and used to instantiate the Ability.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$args = apply_filters( 'register_ability_args', array $args, string $ability_name );
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `$args` (`array<string,mixed>`): The arguments used to instantiate the ability. See [wp_register_ability()](./3.registering-abilities.md#wp_register_ability) for the full list of args.
|
||||||
|
- `$ability_name` (`string`): The namespaced name of the ability being registered (e.g., `my-plugin/get-posts`).
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Modify ability args before validation.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $args The arguments used to instantiate the ability.
|
||||||
|
* @param string $ability_name The name of the ability, with its namespace.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed> The modified ability arguments.
|
||||||
|
*/
|
||||||
|
function my_modify_ability_args( array $args, string $ability_name ): array {
|
||||||
|
// Check if the ability name matches what you're looking for.
|
||||||
|
if ( 'my-namespace/my-ability' !== $ability_name ) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the args as needed.
|
||||||
|
$args['label'] = __('My Custom Ability Label');
|
||||||
|
|
||||||
|
// You can use the old args to build new ones.
|
||||||
|
$args['description'] = sprintf(
|
||||||
|
/* translators: 1: Ability name 2: Previous description */
|
||||||
|
__('This is a custom description for the ability %s. Previously the description was %s', 'text-domain'),
|
||||||
|
$ability_name,
|
||||||
|
$args['description'] ?? 'N/A'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Even if they're callbacks.
|
||||||
|
$args['permission_callback' ] = static function ( $input = null ) use ( $args, $ability_name ) {
|
||||||
|
$previous_check = is_callable( $args['permission_callback'] ) ? $args['permission_callback']( $input ) : true;
|
||||||
|
|
||||||
|
// If we already failed, no need for stricter checks.
|
||||||
|
if ( ! $previous_check || is_wp_error( $previous_check ) ) {
|
||||||
|
return $previous_check;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current_user_can( 'my_custom_ability_cap', $ability_name );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
add_filter( 'register_ability_args', 'my_modify_ability_args', 10, 2 );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `register_ability_category_args`
|
||||||
|
|
||||||
|
Allows modification of a category's arguments before validation.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$args = apply_filters( 'register_ability_category_args', array $args, string $slug );
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `$args` (`array<string,mixed>`): The arguments used to instantiate the category (label, description).
|
||||||
|
- `$slug` (`string`): The slug of the category being registered.
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Modify category args before validation.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $args The arguments used to instantiate the category.
|
||||||
|
* @param string $slug The slug of the category being registered.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed> The modified category arguments.
|
||||||
|
*/
|
||||||
|
function my_modify_category_args( array $args, string $slug ): array {
|
||||||
|
if ( 'my-category' === $slug ) {
|
||||||
|
$args['label'] = __( 'My Custom Label', 'my-plugin' );
|
||||||
|
$args['description'] = __( 'My custom description for this category.', 'my-plugin' );
|
||||||
|
}
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
add_filter( 'register_ability_category_args', 'my_modify_category_args', 10, 2 );
|
||||||
|
```
|
||||||
298
skills/wordpress-ability-api/references/7.javascript-client.md
Normal file
298
skills/wordpress-ability-api/references/7.javascript-client.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# 7. JavaScript/TypeScript Client
|
||||||
|
|
||||||
|
The JavaScript client provides an interface for discovering and executing WordPress Abilities from the browser.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The JavaScript client enables frontend code to interact with the Abilities API system. It can:
|
||||||
|
|
||||||
|
- Discover all registered abilities on your WordPress site
|
||||||
|
- Execute server-side PHP abilities
|
||||||
|
- Register and execute client-side JavaScript abilities
|
||||||
|
|
||||||
|
You can read more about installation and setup in the [package readme](../packages/client/README.md).
|
||||||
|
|
||||||
|
## Core API Functions
|
||||||
|
|
||||||
|
### `getAbilities( args = {} )`
|
||||||
|
|
||||||
|
Returns an array of all registered abilities (both server-side and client-side).
|
||||||
|
|
||||||
|
**Parameters:** `args` (object, optional) - Query arguments to filter abilities. Supported arguments:
|
||||||
|
|
||||||
|
- `category` (string) - Filter abilities by category slug
|
||||||
|
|
||||||
|
**Returns:** `Promise<Array>` - Array of ability objects
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getAbilities } from `@wordpress/abilities`;
|
||||||
|
|
||||||
|
const abilities = await getAbilities();
|
||||||
|
console.log(`Found ${abilities.length} abilities`);
|
||||||
|
|
||||||
|
// List all abilities
|
||||||
|
abilities.forEach(ability => {
|
||||||
|
console.log(`${ability.name}: ${ability.description}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get abilities in a specific category
|
||||||
|
const dataAbilities = await getAbilities( { category: 'data-retrieval' } );
|
||||||
|
|
||||||
|
console.log( `Found ${ dataAbilities.length } data retrieval abilities` );
|
||||||
|
```
|
||||||
|
|
||||||
|
### getAbility( name )
|
||||||
|
|
||||||
|
Retrieves a specific ability by name.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `name` (string) - The ability name (e.g., 'my-plugin/get-posts')
|
||||||
|
|
||||||
|
**Returns:** `Promise<Object|null>` - The ability object or null if not found
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ability = await getAbility( 'my-plugin/get-site-info' );
|
||||||
|
if ( ability ) {
|
||||||
|
console.log( 'Label:', ability.label );
|
||||||
|
console.log( 'Description:', ability.description );
|
||||||
|
console.log( 'Input Schema:', ability.input_schema );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `getAbilityCategories()`
|
||||||
|
|
||||||
|
Returns an array of all registered ability categories.
|
||||||
|
|
||||||
|
**Parameters:** None
|
||||||
|
|
||||||
|
**Returns:** `Promise<Array>` - Array of category objects
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const categories = await getAbilityCategories();
|
||||||
|
console.log( `Found ${ categories.length } categories` );
|
||||||
|
|
||||||
|
// List all categories
|
||||||
|
categories.forEach( ( category ) => {
|
||||||
|
console.log( `${ category.label }: ${ category.description }` );
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `getAbilityCategory( slug )`
|
||||||
|
|
||||||
|
Retrieves a specific category by slug.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `slug` (string) - The category slug (e.g., 'data-retrieval')
|
||||||
|
|
||||||
|
**Returns:** `Promise<Object|null>` - The category object or null if not found
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const category = await getAbilityCategory( 'data-retrieval' );
|
||||||
|
if ( category ) {
|
||||||
|
console.log( 'Label:', category.label );
|
||||||
|
console.log( 'Description:', category.description );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `executeAbility( name, input = null )`
|
||||||
|
|
||||||
|
Executes an ability with the provided input data.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `name` (string) - The ability name
|
||||||
|
- `input` (any, optional) - Input data for the ability
|
||||||
|
|
||||||
|
**Returns:** `Promise<any>` - The ability's output
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Execute without input
|
||||||
|
const siteTitle = await executeAbility( 'my-plugin/get-site-title' );
|
||||||
|
console.log( 'Site:', siteTitle );
|
||||||
|
|
||||||
|
// Execute with input parameters
|
||||||
|
const posts = await executeAbility( 'my-plugin/get-posts', {
|
||||||
|
category: 'news',
|
||||||
|
limit: 5,
|
||||||
|
} );
|
||||||
|
posts.forEach( ( post ) => console.log( post.title ) );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `registerAbility( ability )`
|
||||||
|
|
||||||
|
Registers a client-side ability that runs in the browser.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `ability` (object) - The ability configuration object
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// showNotification function
|
||||||
|
const showNotification = ( message ) => {
|
||||||
|
new Notification( message );
|
||||||
|
return { success: true, displayed: message };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register a notification ability which calls the showNotification function
|
||||||
|
await registerAbility( {
|
||||||
|
name: 'my-plugin/show-notification',
|
||||||
|
label: 'Show Notification',
|
||||||
|
description: 'Display a notification message to the user',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: { type: 'string' },
|
||||||
|
type: { type: 'string', enum: [ 'success', 'error', 'warning', 'info' ] },
|
||||||
|
},
|
||||||
|
required: [ 'message' ],
|
||||||
|
},
|
||||||
|
callback: async ( { message, type = 'info' } ) => {
|
||||||
|
// Show browser notification
|
||||||
|
if ( ! ( 'Notification' in window ) ) {
|
||||||
|
alert( 'This browser does not support desktop notification' );
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Browser does not support notifications',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if ( Notification.permission !== 'granted' ) {
|
||||||
|
Notification.requestPermission().then( ( permission ) => {
|
||||||
|
if ( permission === 'granted' ) {
|
||||||
|
return showNotification( message );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
return showNotification( message );
|
||||||
|
},
|
||||||
|
permissionCallback: () => {
|
||||||
|
return !! wp.data.select( 'core' ).getCurrentUser();
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Use the registered ability
|
||||||
|
const result = await executeAbility( 'my-plugin/show-notification', {
|
||||||
|
message: 'Hello World!',
|
||||||
|
type: 'success',
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `unregisterAbility( name )`
|
||||||
|
|
||||||
|
Removes a previously registered client-side ability.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `name` (string) - The ability name to unregister
|
||||||
|
|
||||||
|
**Returns:** `void`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Unregister an ability
|
||||||
|
unregisterAbility( 'my-plugin/old-ability' );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `registerAbilityCategory( slug, args )`
|
||||||
|
|
||||||
|
Registers a client-side ability category. This is useful when registering client-side abilities that introduce new categories not defined by the server.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `slug` (string) - The category slug (lowercase alphanumeric with dashes only)
|
||||||
|
- `args` (object) - Category configuration object
|
||||||
|
- `label` (string) - Human-readable label for the category
|
||||||
|
- `description` (string) - Detailed description of the category
|
||||||
|
- `meta` (object, optional) - Optional metadata about the category
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Register a new category
|
||||||
|
await registerAbilityCategory( 'block-editor', {
|
||||||
|
label: 'Block Editor',
|
||||||
|
description: 'Abilities for interacting with the WordPress block editor',
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Register a category with metadata
|
||||||
|
await registerAbilityCategory( 'custom-category', {
|
||||||
|
label: 'Custom Category',
|
||||||
|
description: 'A category for custom abilities',
|
||||||
|
meta: {
|
||||||
|
priority: 'high',
|
||||||
|
icon: 'dashicons-admin-customizer',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Then register abilities using the new category
|
||||||
|
await registerAbility( {
|
||||||
|
name: 'my-plugin/insert-block',
|
||||||
|
label: 'Insert Block',
|
||||||
|
description: 'Inserts a block into the editor',
|
||||||
|
category: 'block-editor', // Uses the client-registered category
|
||||||
|
callback: async ( { blockType } ) => {
|
||||||
|
// Implementation
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
### `unregisterAbilityCategory( slug )`
|
||||||
|
|
||||||
|
Removes a previously registered client-side category.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `slug` (string) - The category slug to unregister
|
||||||
|
|
||||||
|
**Returns:** `void`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Unregister a category
|
||||||
|
unregisterAbilityCategory( 'block-editor' );
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All functions return promises that may reject with specific error codes:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
const result = await executeAbility( 'my-plugin/restricted-action', input );
|
||||||
|
console.log( 'Success:', result );
|
||||||
|
} catch ( error ) {
|
||||||
|
switch ( error.code ) {
|
||||||
|
case 'ability_permission_denied':
|
||||||
|
console.error( 'Permission denied:', error.message );
|
||||||
|
break;
|
||||||
|
case 'ability_invalid_input':
|
||||||
|
console.error( 'Invalid input:', error.message );
|
||||||
|
break;
|
||||||
|
case 'rest_ability_not_found':
|
||||||
|
console.error( 'Ability not found:', error.message );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error( 'Execution failed:', error.message );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 7. Registering Categories
|
||||||
|
|
||||||
|
Before registering abilities, you must register at least one category. Categories help organize abilities and make them easier to discover and filter.
|
||||||
|
|
||||||
|
## Function Signature
|
||||||
|
|
||||||
|
```php
|
||||||
|
wp_register_ability_category( string $slug, array $args ): ?\WP_Ability_Category
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `$slug` (`string`): A unique identifier for the category. Must contain only lowercase alphanumeric characters and dashes (no underscores, no uppercase).
|
||||||
|
- `$args` (`array`): Category configuration with these keys:
|
||||||
|
- `label` (`string`, **Required**): Human-readable name for the category. Should be translatable.
|
||||||
|
- `description` (`string`, **Required**): Detailed description of the category's purpose. Should be translatable.
|
||||||
|
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the category.
|
||||||
|
|
||||||
|
**Return:** (`?\WP_Ability_Category`) An instance of the registered category if it was successfully registered, `null` on failure (e.g., invalid arguments, duplicate slug).
|
||||||
|
|
||||||
|
**Note:** Categories must be registered during the `abilities_api_categories_init` action hook.
|
||||||
|
|
||||||
|
## Code Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
|
||||||
|
function my_plugin_register_categories() {
|
||||||
|
wp_register_ability_category( 'data-retrieval', array(
|
||||||
|
'label' => __( 'Data Retrieval', 'my-plugin' ),
|
||||||
|
'description' => __( 'Abilities that retrieve and return data from the WordPress site.', 'my-plugin' ),
|
||||||
|
));
|
||||||
|
|
||||||
|
wp_register_ability_category( 'data-modification', array(
|
||||||
|
'label' => __( 'Data Modification', 'my-plugin' ),
|
||||||
|
'description' => __( 'Abilities that modify data on the WordPress site.', 'my-plugin' ),
|
||||||
|
));
|
||||||
|
|
||||||
|
wp_register_ability_category( 'communication', array(
|
||||||
|
'label' => __( 'Communication', 'my-plugin' ),
|
||||||
|
'description' => __( 'Abilities that send messages or notifications.', 'my-plugin' ),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Category Slug Convention
|
||||||
|
|
||||||
|
The `$slug` parameter must follow these rules:
|
||||||
|
|
||||||
|
- **Format:** Must contain only lowercase alphanumeric characters (`a-z`, `0-9`) and hyphens (`-`).
|
||||||
|
- **Valid examples:** `data-retrieval`, `ecommerce`, `site-information`, `user-management`, `category-123`
|
||||||
|
- **Invalid examples:**
|
||||||
|
- Uppercase: `Data-Retrieval`, `MyCategory`
|
||||||
|
- Underscores: `data_retrieval`
|
||||||
|
- Special characters: `data.retrieval`, `data/retrieval`, `data retrieval`
|
||||||
|
- Leading/trailing dashes: `-data`, `data-`
|
||||||
|
- Double dashes: `data--retrieval`
|
||||||
|
|
||||||
|
## Other Category Functions
|
||||||
|
|
||||||
|
- `wp_unregister_ability_category( string $slug )` - Remove a registered category. Returns the unregistered category instance or `null` on failure.
|
||||||
|
- `wp_get_ability_category( string $slug )` - Retrieve a specific category by slug. Returns the category instance or `null` if not found.
|
||||||
|
- `wp_get_ability_categories()` - Get all registered categories as an associative array keyed by slug.
|
||||||
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