Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .well-known/agent-skills/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"type": "archive",
"description": "Phase-based WordPress project setup workflow with dry-run planning and confirmed apply for predictable scaffolding/standardization.",
"url": "https://soderlind.no/.well-known/agent-skills/prepare-wordpress.tar.gz",
"digest": "sha256:28fd8747a249eddb8308c4d3eb5bfe730db6a3470ab169092fe506b6a1fd26f2"
"digest": "sha256:c0c7545687f6f8e7cb6e322b2ffde7ea87eface20008592031e7ba47f06d7b1a"
},
{
"name": "wp-bump",
Expand Down
Binary file modified .well-known/agent-skills/prepare-wordpress.tar.gz
Binary file not shown.
13 changes: 13 additions & 0 deletions plugins/wordpress-skills/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to `wordpress-skills` are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2026-08-08

### Added

- prepare-wordpress: unit tests mock WordPress with Brain Monkey (brain/monkey); added references/php-testing.md and PHPUnit test scaffolding (phpunit.xml.dist, tests/bootstrap.php, tests/TestCase.php).
- prepare-wordpress: ESLint (@wordpress/eslint-plugin) and a committed phpcs.xml ruleset via a new linting-setup.md reference.
- prepare-wordpress: new instructions phase downloads the github/awesome-copilot WordPress coding instructions.

### Changed

- prepare-wordpress: Composer and npm dependencies install the latest compatible releases (unpinned).
- prepare-wordpress: composer.json is written as a file instead of via composer init with user-provided strings.

## [1.0.0] - 2026-08-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion plugins/wordpress-skills/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "wordpress-skills",
"version": "1.0.0",
"version": "1.1.0",
"description": "WordPress development, testing, and release skills.",
"author": {
"name": "Per Soderlind",
Expand Down
59 changes: 53 additions & 6 deletions plugins/wordpress-skills/skills/prepare-wordpress/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: prepare-wordpress
description: "Phase-based WordPress project setup workflow with dry-run planning and confirmed apply for predictable scaffolding/standardization."
compatibility: "macOS/Linux with Node.js 18+, Composer 2+, PHP 8.3+, git. Optional: WP-CLI for i18n commands, curl for downloading coding instructions."
version: "1.2.0"
version: "1.3.0"
---

# Prepare WordPress Project
Expand Down Expand Up @@ -61,6 +61,7 @@ Hard gate: Do not run any `--apply` command until dry-run output is shown and th
- `composer` — install PHP dev deps and merge scripts
- `config` — create/merge `.editorconfig` and `.gitignore`
- `vitest` — install and scaffold Vitest (JavaScript test runner)
- `eslint` — install and configure ESLint for JavaScript
- `i18n` — scaffold i18n files/scripts
- `instructions` — download WordPress Copilot coding instructions
- `cleanup` — remove stray `yarn.lock`
Expand Down Expand Up @@ -205,10 +206,20 @@ If `package.json` does not exist:
npm init -y
```

If `composer.json` does not exist, create it using the plugin metadata:
If `composer.json` does not exist, create it as a JSON file using the plugin
metadata. Do **not** pass user-provided metadata to `composer init` on the
command line — write the file directly to avoid shell-quoting and injection
issues:

```sh
composer init --no-interaction --name=<author>/<plugin-slug> --description="{Description}" --license={License}
```json
{
"name": "<author>/<plugin-slug>",
"description": "{Description}",
"type": "wordpress-plugin",
"license": "{License}",
"require": {},
"require-dev": {}
}
```

If `.git/` does not exist:
Expand Down Expand Up @@ -261,17 +272,26 @@ Replace `<plugin-slug>` with the actual plugin slug (folder name / text domain).
{
"scripts": {
"test": "phpunit",
"lint": "phpcs --standard=WordPress --extensions=php .",
"lint": "phpcs",
"check": "wp plugin check <plugin-slug> --format=text"
}
}
```

> **Note:** The `check` script requires [Plugin Check (PCP)](https://wordpress.org/plugins/plugin-check/) installed and activated in WordPress, and WP-CLI available. Install with `wp plugin install plugin-check --activate`.

**PHP lint ruleset.** Create `phpcs.xml` so the `lint` script (`phpcs`) uses a
committed WordPress ruleset and excludes `vendor/` and `node_modules/`. Skip if
`phpcs.xml` (or `phpcs.xml.dist`) already exists. See: `references/linting-setup.md`.

**PHP test scaffolding.** Skip any file that already exists. Create
`phpunit.xml.dist`, `tests/bootstrap.php`, and `tests/TestCase.php` (wires Brain
Monkey `setUp`/`tearDown`) so `composer test` runs out of the box. See:
`references/php-testing.md`.

See: `references/composer-setup.md`

Completion criterion: Composer dev dependencies are installed and `scripts.test`, `scripts.lint`, and `scripts.check` exist without overwriting unrelated scripts.
Completion criterion: Composer dev dependencies are installed; `scripts.test`, `scripts.lint`, and `scripts.check` exist without overwriting unrelated scripts; `phpcs.xml` and the PHPUnit test scaffolding exist (or were already present).

### 4) Config files

Expand All @@ -285,6 +305,31 @@ See: `references/config-files.md`

Completion criterion: `.editorconfig` and `.gitignore` are present, and existing `.gitignore` entries were merged non-destructively.

### 4b) ESLint (JavaScript linting)

**Skip if an ESLint config (`.eslintrc.json`, `.eslintrc.js`, or `eslint.config.js`) already exists.**

Install the latest releases and use WordPress' shared config:

```sh
npm install --save-dev eslint @wordpress/eslint-plugin
```

Create `.eslintrc.json` and `.eslintignore`, then merge a `lint:js` script into
`package.json`:

```json
{
"scripts": {
"lint:js": "eslint ."
}
}
```

See: `references/linting-setup.md`

Completion criterion: `.eslintrc.json`, `.eslintignore`, and `scripts.lint:js` are present, or a skip reason is recorded.

### 5) Vitest setup

**Skip if `vitest.config.js` already exists.**
Expand Down Expand Up @@ -379,6 +424,8 @@ Completion criterion: Final report includes per-phase status, skipped reasons, a
- `composer validate` passes.
- `npm ls` shows no missing peer dependencies for vitest.
- Agent skills are present under `~/.copilot/skills/` or `~/.agents/skills/`.
- `phpcs.xml`, `phpunit.xml.dist`, and `tests/` scaffolding exist when the `composer` phase ran.
- `.eslintrc.json` and `scripts.lint:js` exist when the `eslint` phase ran.
- `.github/instructions/wordpress.instructions.md` exists when the `instructions` phase ran.

## Failure modes / debugging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ Replace `<plugin-slug>` with the actual plugin slug (folder name / text domain).
{
"scripts": {
"test": "phpunit",
"lint": "phpcs --standard=WordPress --extensions=php .",
"lint": "phpcs",
"check": "wp plugin check <plugin-slug> --format=text"
}
}
```

> **Note:** The `check` script requires [Plugin Check (PCP)](https://wordpress.org/plugins/plugin-check/) to be installed and activated in the WordPress environment, and WP-CLI to be available. Install it with `wp plugin install plugin-check --activate`.

> **`lint` uses `phpcs.xml`:** The `lint` script runs plain `phpcs`, which reads
> the committed `phpcs.xml` ruleset. Create it (and the ESLint config) per
> `linting-setup.md`. Scaffold PHPUnit test files per `php-testing.md` so
> `composer test` runs out of the box.

### How to merge via PHP (if jq is unavailable)

```sh
Expand All @@ -50,7 +55,7 @@ $f = "composer.json";
$c = json_decode(file_get_contents($f), true);
$c["scripts"] = array_merge($c["scripts"] ?? [], [
"test" => "phpunit",
"lint" => "phpcs --standard=WordPress --extensions=php .",
"lint" => "phpcs",
"check" => "wp plugin check <plugin-slug> --format=text"
]);
file_put_contents($f, json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Linting Setup

Two linters: **PHPCS** (WordPress Coding Standards) for PHP and **ESLint**
(`@wordpress/eslint-plugin`) for JavaScript. Do not pin versions — install the
latest compatible releases.

## PHP: phpcs.xml

Committing a `phpcs.xml` ruleset lets the `lint` composer script run as plain
`phpcs` (no `--standard` flag) and excludes build/vendor paths. Requires
`wp-coding-standards/wpcs` and `dealerdirect/phpcodesniffer-composer-installer`
(installed in the Composer phase).

Create `phpcs.xml` at the project root. Skip if it already exists.

```xml
<?xml version="1.0"?>
<ruleset name="Project">
<description>WordPress Coding Standards for this project.</description>

<file>.</file>
<exclude-pattern>vendor/*</exclude-pattern>
<exclude-pattern>node_modules/*</exclude-pattern>
<exclude-pattern>build/*</exclude-pattern>

<arg name="extensions" value="php"/>
<arg value="ps"/><!-- p = progress, s = show sniff codes -->

<rule ref="WordPress"/>
</ruleset>
```

Run with `composer lint` (i.e. `phpcs`) and auto-fix with `phpcbf`.

## JavaScript: ESLint

Install:

```sh
npm install --save-dev eslint @wordpress/eslint-plugin
```

Create `.eslintrc.json`:

```json
{
"root": true,
"extends": ["plugin:@wordpress/eslint-plugin/recommended"]
}
```

Create `.eslintignore`:

```gitignore
vendor/
node_modules/
build/
```

Merge into `package.json` scripts (do not overwrite existing):

```json
{
"scripts": {
"lint:js": "eslint ."
}
}
```

Run with `npm run lint:js`. Auto-fix with `eslint . --fix`.

## Notes

- Skip PHP or JS linting independently if a config already exists.
- `@wordpress/eslint-plugin/recommended` covers React/JSX and WordPress globals;
use `.../recommended-with-formatting` if you are not running Prettier.
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ export function detectProjectState(targetRoot = process.cwd()) {
check: composerHasScript("check"),
},

// PHP lint/test config
phpcsXml: exists("phpcs.xml") || exists("phpcs.xml.dist"),
phpunitConfig: exists("phpunit.xml.dist") || exists("phpunit.xml"),

// Config files
editorconfig: exists(".editorconfig"),
gitignore: exists(".gitignore"),
Expand All @@ -145,6 +149,13 @@ export function detectProjectState(targetRoot = process.cwd()) {
testScript: packageJsonHasScript("test:js"),
},

// ESLint
eslint: {
devDep: packageJsonHasDevDep("eslint"),
config: exists(".eslintrc.json") || exists(".eslintrc.js") || exists("eslint.config.js") || exists(".eslintrc"),
script: packageJsonHasScript("lint:js"),
},

// i18n
i18n: {
mapJson: exists("i18n-map.json"),
Expand Down Expand Up @@ -197,6 +208,11 @@ export function buildDetectionSummary(state, repoRoot = process.cwd()) {
const missingComposerScripts = Object.entries(state.composerScripts).filter(([, v]) => !v).map(([k]) => k);
if (missingComposerScripts.length > 0) lines.push(`📦 Composer scripts to add: ${missingComposerScripts.join(", ")}`);

if (!state.phpcsXml) lines.push("📦 phpcs.xml — will create");
else lines.push("⏭ phpcs.xml exists");
if (!state.phpunitConfig) lines.push("📦 PHPUnit test scaffolding — will create");
else lines.push("⏭ PHPUnit config exists");

// Config files
if (!state.editorconfig) lines.push("📦 .editorconfig — will create");
else lines.push("⏭ .editorconfig exists");
Expand All @@ -208,6 +224,10 @@ export function buildDetectionSummary(state, repoRoot = process.cwd()) {
if (!state.vitest.config) lines.push("\n📦 Vitest — will install and configure");
else lines.push("\n⏭ Vitest already configured");

// ESLint
if (!state.eslint.config) lines.push("📦 ESLint — will install and configure");
else lines.push("⏭ ESLint already configured");

// i18n
if (!state.i18n.mapJson || !state.i18n.npmScripts) lines.push("📦 i18n — will scaffold");
else lines.push("⏭ i18n already configured");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execSync } from "node:child_process";
import { detectProjectState } from "./detect_project.mjs";

const ALL_PHASES = ["plugin", "readme", "init", "skills", "composer", "config", "vitest", "i18n", "instructions", "cleanup"];
const ALL_PHASES = ["plugin", "readme", "init", "skills", "composer", "config", "vitest", "eslint", "i18n", "instructions", "cleanup"];

function parseArgs(argv) {
const opts = {
Expand Down Expand Up @@ -96,6 +96,8 @@ function buildPlan(state, phases) {
if (!state.composerScripts.test || !state.composerScripts.lint || !state.composerScripts.check) {
composerNotes.push("Manual: merge composer scripts test/lint/check into composer.json without overwriting existing scripts.");
}
if (!state.phpcsXml) composerNotes.push("Manual: create phpcs.xml from references/linting-setup.md.");
if (!state.phpunitConfig) composerNotes.push("Manual: create phpunit.xml.dist, tests/bootstrap.php, and tests/TestCase.php from references/php-testing.md.");
add("composer", composerCommands.length > 0 || composerNotes.length > 0 ? "Configure Composer tooling" : "Composer phase already satisfied", composerCommands, composerNotes);

const configNotes = [];
Expand All @@ -114,6 +116,13 @@ function buildPlan(state, phases) {
}
add("vitest", vitestCommands.length > 0 || vitestNotes.length > 0 ? "Configure Vitest" : "Vitest phase already satisfied", vitestCommands, vitestNotes);

const eslintCommands = [];
const eslintNotes = [];
if (!state.eslint.config || !state.eslint.devDep) eslintCommands.push("npm install --save-dev eslint @wordpress/eslint-plugin");
if (!state.eslint.config) eslintNotes.push("Manual: create .eslintrc.json and .eslintignore from references/linting-setup.md.");
if (!state.eslint.script) eslintNotes.push("Manual: merge lint:js script into package.json without overwriting existing scripts.");
add("eslint", eslintCommands.length > 0 || eslintNotes.length > 0 ? "Configure ESLint" : "ESLint phase already satisfied", eslintCommands, eslintNotes);

const i18nNotes = [];
if (!state.i18n.mapJson) i18nNotes.push("Manual: create i18n-map.json (or empty {} placeholder) from references/i18n-setup.md.");
if (!state.i18n.languagesDir) i18nNotes.push("Manual: create languages/ directory.");
Expand Down
15 changes: 14 additions & 1 deletion scripts/build-agent-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const PLUGINS = [
distRepo: 'https://github.com/soderlind/wordpress-agent-plugin',
manifest: {
name: 'wordpress-skills',
version: '1.0.0',
version: '1.1.0',
description: 'WordPress development, testing, and release skills.',
author: { name: 'Per Soderlind', url: 'https://soderlind.no' },
homepage: 'https://github.com/soderlind/wordpress-agent-plugin',
Expand All @@ -34,6 +34,19 @@ const PLUGINS = [
},
// Newest first. Each entry becomes a section in CHANGELOG.md.
changelog: [
{
version: '1.1.0',
date: '2026-08-08',
added: [
'prepare-wordpress: unit tests mock WordPress with Brain Monkey (brain/monkey); added references/php-testing.md and PHPUnit test scaffolding (phpunit.xml.dist, tests/bootstrap.php, tests/TestCase.php).',
'prepare-wordpress: ESLint (@wordpress/eslint-plugin) and a committed phpcs.xml ruleset via a new linting-setup.md reference.',
'prepare-wordpress: new instructions phase downloads the github/awesome-copilot WordPress coding instructions.',
],
changed: [
'prepare-wordpress: Composer and npm dependencies install the latest compatible releases (unpinned).',
'prepare-wordpress: composer.json is written as a file instead of via composer init with user-provided strings.',
],
},
{
version: '1.0.0',
date: '2026-08-07',
Expand Down
Loading
Loading